Merge branch 'main' into release

release
Tom Arra 2 years ago
commit 135bd31e29

@ -42,7 +42,8 @@
"unawaited", "unawaited",
"unfocus", "unfocus",
"unlayered", "unlayered",
"vsync" "vsync",
"microtask"
], ],
"ignorePaths": [ "ignorePaths": [
".github/workflows/**" ".github/workflows/**"

@ -51,7 +51,7 @@ class CameraFocusingBehavior extends Component
position: _foci[GameStatus.waiting]?.position ?? Vector2(0, -112), position: _foci[GameStatus.waiting]?.position ?? Vector2(0, -112),
), ),
GameStatus.playing: _FocusData( GameStatus.playing: _FocusData(
zoom: size.y / 165, zoom: size.y / 160,
position: _foci[GameStatus.playing]?.position ?? Vector2(0, -7.8), position: _foci[GameStatus.playing]?.position ?? Vector2(0, -7.8),
), ),
GameStatus.gameOver: _FocusData( GameStatus.gameOver: _FocusData(

@ -2,6 +2,8 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:platform_helper/platform_helper.dart';
/// Updates the [ArcadeBackground] and launch [Ball] to reflect character /// Updates the [ArcadeBackground] and launch [Ball] to reflect character
/// selections. /// selections.
@ -11,12 +13,14 @@ class CharacterSelectionBehavior extends Component
HasGameRef { HasGameRef {
@override @override
void onNewState(CharacterThemeState state) { void onNewState(CharacterThemeState state) {
if (!readProvider<PlatformHelper>().isMobile) {
gameRef gameRef
.descendants() .descendants()
.whereType<ArcadeBackground>() .whereType<ArcadeBackground>()
.single .single
.bloc .bloc
.onCharacterSelected(state.characterTheme); .onCharacterSelected(state.characterTheme);
}
gameRef gameRef
.descendants() .descendants()
.whereType<Ball>() .whereType<Ball>()

@ -42,7 +42,7 @@ class _BarrierBehindDino extends BodyComponent {
Body createBody() { Body createBody() {
final shape = EdgeShape() final shape = EdgeShape()
..set( ..set(
Vector2(25.3, -14.2), Vector2(24.2, -14.8),
Vector2(25.3, -7.7), Vector2(25.3, -7.7),
); );

@ -42,7 +42,7 @@ class FlutterForest extends Component with ZIndex {
)..initialPosition = Vector2(21.8, -46.75), )..initialPosition = Vector2(21.8, -46.75),
DashAnimatronic( DashAnimatronic(
children: [ children: [
AnimatronicLoopingBehavior(animationCoolDown: 4), AnimatronicLoopingBehavior(animationCoolDown: 11),
], ],
)..position = Vector2(20, -66), )..position = Vector2(20, -66),
FlutterForestBonusBehavior(), FlutterForestBonusBehavior(),

@ -5,7 +5,6 @@ import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:platform_helper/platform_helper.dart';
/// Listens to the [GameBloc] and updates the game accordingly. /// Listens to the [GameBloc] and updates the game accordingly.
class GameBlocStatusListener extends Component class GameBlocStatusListener extends Component
@ -68,31 +67,25 @@ class GameBlocStatusListener extends Component
gameRef.descendants().whereType<Signpost>().single.bloc.onReset(); gameRef.descendants().whereType<Signpost>().single.bloc.onReset();
} }
void _addPlungerBehaviors(Plunger plunger) { void _addPlungerBehaviors(Plunger plunger) => plunger
final platformHelper = readProvider<PlatformHelper>(); .firstChild<FlameBlocProvider<PlungerCubit, PlungerState>>()!
const pullingStrength = 7.0; .addAll(
final provider =
plunger.firstChild<FlameBlocProvider<PlungerCubit, PlungerState>>()!;
if (platformHelper.isMobile) {
provider.add(
PlungerAutoPullingBehavior(strength: pullingStrength),
);
} else {
provider.addAll(
[ [
PlungerKeyControllingBehavior(), PlungerPullingBehavior(strength: 7),
PlungerPullingBehavior(strength: pullingStrength), PlungerAutoPullingBehavior(),
PlungerKeyControllingBehavior()
], ],
); );
}
}
void _removePlungerBehaviors(Plunger plunger) { void _removePlungerBehaviors(Plunger plunger) {
plunger plunger
.descendants() .descendants()
.whereType<PlungerPullingBehavior>() .whereType<PlungerPullingBehavior>()
.forEach(plunger.remove); .forEach(plunger.remove);
plunger
.descendants()
.whereType<PlungerAutoPullingBehavior>()
.forEach(plunger.remove);
plunger plunger
.descendants() .descendants()
.whereType<PlungerKeyControllingBehavior>() .whereType<PlungerKeyControllingBehavior>()

@ -35,7 +35,7 @@ class SparkyScorch extends Component {
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
SparkyAnimatronic( SparkyAnimatronic(
children: [ children: [
AnimatronicLoopingBehavior(animationCoolDown: 3), AnimatronicLoopingBehavior(animationCoolDown: 8),
], ],
)..position = Vector2(-14, -58.2), )..position = Vector2(-14, -58.2),
SparkyComputer( SparkyComputer(

@ -155,19 +155,18 @@ class PinballGame extends PinballForge2DGame
@override @override
void onTapDown(int pointerId, TapDownInfo info) { void onTapDown(int pointerId, TapDownInfo info) {
if (info.raw.kind == PointerDeviceKind.touch) { if (info.raw.kind == PointerDeviceKind.touch &&
_gameBloc.state.status.isPlaying) {
final rocket = descendants().whereType<RocketSpriteComponent>().first; final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size; final bounds = rocket.topLeftPosition & rocket.size;
// NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586
// we need to check it at the highest level manually.
final tappedRocket = bounds.contains(info.eventPosition.game.toOffset()); final tappedRocket = bounds.contains(info.eventPosition.game.toOffset());
if (tappedRocket) { if (tappedRocket) {
descendants() descendants()
.whereType<FlameBlocProvider<PlungerCubit, PlungerState>>() .whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
.first .first
.bloc .bloc
.pulled(); .autoPulled();
} else { } else {
final tappedLeftSide = info.eventPosition.widget.x < canvasSize.x / 2; final tappedLeftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide[pointerId] = focusedBoardSide[pointerId] =

@ -2,10 +2,10 @@ import 'dart:math';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/gen/assets.gen.dart';
import 'package:pinball_audio/src/pinball_audio_pool.dart';
/// Sounds available to play. /// Sounds available to play.
enum PinballAudio { enum PinballAudio {
@ -52,17 +52,8 @@ enum PinballAudio {
flipper, flipper,
} }
/// Defines the contract of the creation of an [AudioPool].
typedef CreateAudioPool = Future<AudioPool> Function(
String sound, {
bool? repeating,
int? maxPlayers,
int? minPlayers,
String? prefix,
});
/// Defines the contract for playing a single audio. /// Defines the contract for playing a single audio.
typedef PlaySingleAudio = Future<void> Function(String, {double volume}); typedef PlaySingleAudio = Future<AudioPlayer> Function(String, {double volume});
/// Defines the contract for looping a single audio. /// Defines the contract for looping a single audio.
typedef LoopSingleAudio = Future<void> Function(String, {double volume}); typedef LoopSingleAudio = Future<void> Function(String, {double volume});
@ -94,13 +85,20 @@ class _SimplePlayAudio extends _Audio {
final PlaySingleAudio playSingleAudio; final PlaySingleAudio playSingleAudio;
final String path; final String path;
final double? volume; final double? volume;
AudioPlayer? _player;
@override @override
Future<void> load() => preCacheSingleAudio(prefixFile(path)); Future<void> load() => preCacheSingleAudio(prefixFile(path));
@override @override
void play() { Future<void> play() async {
playSingleAudio(prefixFile(path), volume: volume ?? 1); final url = prefixFile(path);
final volume = this.volume ?? 1;
if (_player == null) {
_player = await playSingleAudio(url, volume: volume);
} else {
await _player!.play(url, volume: volume);
}
} }
} }
@ -153,81 +151,94 @@ class _SingleLoopAudio extends _LoopAudio {
class _SingleAudioPool extends _Audio { class _SingleAudioPool extends _Audio {
_SingleAudioPool({ _SingleAudioPool({
required this.path, required this.path,
required this.createAudioPool, required this.duration,
required this.maxPlayers, required this.maxPlayers,
required this.preCacheSingleAudio,
required this.playSingleAudio,
}); });
final String path; final String path;
final CreateAudioPool createAudioPool;
final int maxPlayers; final int maxPlayers;
late AudioPool pool; final Duration duration;
final PreCacheSingleAudio preCacheSingleAudio;
final PlaySingleAudio playSingleAudio;
late PinballAudioPool pool;
@override @override
Future<void> load() async { Future<void> load() async {
pool = await createAudioPool( pool = PinballAudioPool(
prefixFile(path), path: prefixFile(path),
maxPlayers: maxPlayers, poolSize: maxPlayers,
prefix: '', preCacheSingleAudio: preCacheSingleAudio,
playSingleAudio: playSingleAudio,
duration: duration,
); );
await pool.load();
} }
@override @override
void play() => pool.start(); void play() => pool.play();
} }
class _RandomABAudio extends _Audio { class _RandomABAudio extends _Audio {
_RandomABAudio({ _RandomABAudio({
required this.createAudioPool, required this.preCacheSingleAudio,
required this.playSingleAudio,
required this.seed, required this.seed,
required this.audioAssetA, required this.audioAssetA,
required this.audioAssetB, required this.audioAssetB,
required this.duration,
this.volume, this.volume,
}); });
final CreateAudioPool createAudioPool; final PreCacheSingleAudio preCacheSingleAudio;
final PlaySingleAudio playSingleAudio;
final Random seed; final Random seed;
final String audioAssetA; final String audioAssetA;
final String audioAssetB; final String audioAssetB;
final Duration duration;
final double? volume; final double? volume;
late AudioPool audioA; late PinballAudioPool audioA;
late AudioPool audioB; late PinballAudioPool audioB;
@override @override
Future<void> load() async { Future<void> load() async {
await Future.wait( audioA = PinballAudioPool(
[ path: prefixFile(audioAssetA),
createAudioPool( poolSize: 4,
prefixFile(audioAssetA), preCacheSingleAudio: preCacheSingleAudio,
maxPlayers: 4, playSingleAudio: playSingleAudio,
prefix: '', duration: duration,
).then((pool) => audioA = pool),
createAudioPool(
prefixFile(audioAssetB),
maxPlayers: 4,
prefix: '',
).then((pool) => audioB = pool),
],
); );
audioB = PinballAudioPool(
path: prefixFile(audioAssetB),
poolSize: 4,
preCacheSingleAudio: preCacheSingleAudio,
playSingleAudio: playSingleAudio,
duration: duration,
);
await Future.wait([audioA.load(), audioB.load()]);
} }
@override @override
void play() { void play() {
(seed.nextBool() ? audioA : audioB).start(volume: volume ?? 1); (seed.nextBool() ? audioA : audioB).play(volume: volume ?? 1);
} }
} }
class _ThrottledAudio extends _Audio { class _ThrottledAudio extends _SimplePlayAudio {
_ThrottledAudio({ _ThrottledAudio({
required this.preCacheSingleAudio, required PreCacheSingleAudio preCacheSingleAudio,
required this.playSingleAudio, required PlaySingleAudio playSingleAudio,
required this.path, required String path,
required this.duration, required this.duration,
}); }) : super(
preCacheSingleAudio: preCacheSingleAudio,
playSingleAudio: playSingleAudio,
path: path,
);
final PreCacheSingleAudio preCacheSingleAudio;
final PlaySingleAudio playSingleAudio;
final String path;
final Duration duration; final Duration duration;
DateTime? _lastPlayed; DateTime? _lastPlayed;
@ -236,12 +247,12 @@ class _ThrottledAudio extends _Audio {
Future<void> load() => preCacheSingleAudio(prefixFile(path)); Future<void> load() => preCacheSingleAudio(prefixFile(path));
@override @override
void play() { Future<void> play() async {
final now = clock.now(); final now = clock.now();
if (_lastPlayed == null || if (_lastPlayed == null ||
(_lastPlayed != null && now.difference(_lastPlayed!) > duration)) { (_lastPlayed != null && now.difference(_lastPlayed!) > duration)) {
_lastPlayed = now; _lastPlayed = now;
playSingleAudio(prefixFile(path)); await super.play();
} }
} }
} }
@ -252,14 +263,12 @@ class _ThrottledAudio extends _Audio {
class PinballAudioPlayer { class PinballAudioPlayer {
/// {@macro pinball_audio_player} /// {@macro pinball_audio_player}
PinballAudioPlayer({ PinballAudioPlayer({
CreateAudioPool? createAudioPool,
PlaySingleAudio? playSingleAudio, PlaySingleAudio? playSingleAudio,
LoopSingleAudio? loopSingleAudio, LoopSingleAudio? loopSingleAudio,
PreCacheSingleAudio? preCacheSingleAudio, PreCacheSingleAudio? preCacheSingleAudio,
ConfigureAudioCache? configureAudioCache, ConfigureAudioCache? configureAudioCache,
Random? seed, Random? seed,
}) : _createAudioPool = createAudioPool ?? AudioPool.create, }) : _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play,
_playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play,
_loopSingleAudio = loopSingleAudio ?? FlameAudio.audioCache.loop, _loopSingleAudio = loopSingleAudio ?? FlameAudio.audioCache.loop,
_preCacheSingleAudio = _preCacheSingleAudio =
preCacheSingleAudio ?? FlameAudio.audioCache.load, preCacheSingleAudio ?? FlameAudio.audioCache.load,
@ -279,10 +288,11 @@ class PinballAudioPlayer {
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.sparky, path: Assets.sfx.sparky,
), ),
PinballAudio.dino: _SimplePlayAudio( PinballAudio.dino: _ThrottledAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.dino, path: Assets.sfx.dino,
duration: const Duration(seconds: 6),
), ),
PinballAudio.dash: _SimplePlayAudio( PinballAudio.dash: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
@ -307,8 +317,10 @@ class PinballAudioPlayer {
), ),
PinballAudio.flipper: _SingleAudioPool( PinballAudio.flipper: _SingleAudioPool(
path: Assets.sfx.flipper, path: Assets.sfx.flipper,
createAudioPool: _createAudioPool, maxPlayers: 4,
maxPlayers: 2, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
duration: const Duration(milliseconds: 200),
), ),
PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( PinballAudio.ioPinballVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
@ -321,17 +333,21 @@ class PinballAudioPlayer {
path: Assets.sfx.gameOverVoiceOver, path: Assets.sfx.gameOverVoiceOver,
), ),
PinballAudio.bumper: _RandomABAudio( PinballAudio.bumper: _RandomABAudio(
createAudioPool: _createAudioPool, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
seed: _seed, seed: _seed,
audioAssetA: Assets.sfx.bumperA, audioAssetA: Assets.sfx.bumperA,
audioAssetB: Assets.sfx.bumperB, audioAssetB: Assets.sfx.bumperB,
duration: const Duration(seconds: 1),
volume: 0.6, volume: 0.6,
), ),
PinballAudio.kicker: _RandomABAudio( PinballAudio.kicker: _RandomABAudio(
createAudioPool: _createAudioPool, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
seed: _seed, seed: _seed,
audioAssetA: Assets.sfx.kickerA, audioAssetA: Assets.sfx.kickerA,
audioAssetB: Assets.sfx.kickerB, audioAssetB: Assets.sfx.kickerB,
duration: const Duration(seconds: 1),
volume: 0.6, volume: 0.6,
), ),
PinballAudio.cowMoo: _ThrottledAudio( PinballAudio.cowMoo: _ThrottledAudio(
@ -349,8 +365,6 @@ class PinballAudioPlayer {
}; };
} }
final CreateAudioPool _createAudioPool;
final PlaySingleAudio _playSingleAudio; final PlaySingleAudio _playSingleAudio;
final LoopSingleAudio _loopSingleAudio; final LoopSingleAudio _loopSingleAudio;

@ -0,0 +1,87 @@
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:pinball_audio/pinball_audio.dart';
class _PlayerEntry {
_PlayerEntry({
required this.available,
required this.player,
});
bool available;
final AudioPlayer player;
}
/// {@template pinball_audio_pool}
/// Creates an audio player pool used to trigger many sounds at the same time.
/// {@endtemplate}
class PinballAudioPool {
/// {@macro pinball_audio_pool}
PinballAudioPool({
required this.path,
required this.poolSize,
required this.preCacheSingleAudio,
required this.playSingleAudio,
required this.duration,
});
/// Sounds path.
final String path;
/// Max size of this pool.
final int poolSize;
/// Function to cache audios.
final PreCacheSingleAudio preCacheSingleAudio;
/// Function to play audios.
final PlaySingleAudio playSingleAudio;
/// How long the sound lasts.
final Duration duration;
final List<_PlayerEntry> _players = [];
/// Loads the pool.
Future<void> load() async {
await preCacheSingleAudio(path);
}
/// Plays the pool.
Future<void> play({double volume = 1}) async {
AudioPlayer? player;
if (_players.length < poolSize) {
_players.add(
_PlayerEntry(
available: false,
player: player = await playSingleAudio(path, volume: volume),
),
);
} else {
final entries = _players.where((entry) => entry.available);
if (entries.isNotEmpty) {
final entry = entries.first..available = false;
player = entry.player;
unawaited(entry.player.play(path, volume: volume));
}
}
if (player != null) {
unawaited(
Future<void>.delayed(duration).then(
(_) {
_returnEntryAvailability(player!);
},
),
);
} else {}
}
void _returnEntryAvailability(
AudioPlayer player,
) {
_players.where((entry) => entry.player == player).single.available = true;
}
}

@ -0,0 +1,74 @@
// ignore_for_file: one_member_abstracts
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_audio/src/pinball_audio_pool.dart';
class _MockAudioPlayer extends Mock implements AudioPlayer {}
class _MockPlaySingleAudio extends Mock {
Future<AudioPlayer> onCall(String path, {double volume});
}
abstract class _PreCacheSingleAudio {
Future<void> onCall(String path);
}
class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {}
void main() {
group('PinballAudioPool', () {
late _PreCacheSingleAudio preCacheSingleAudio;
late _MockPlaySingleAudio playSingleAudio;
late PinballAudioPool pool;
late AudioPlayer audioPlayer;
setUp(() {
preCacheSingleAudio = _MockPreCacheSingleAudio();
when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {});
audioPlayer = _MockAudioPlayer();
when(() => audioPlayer.play(any(), volume: any(named: 'volume')))
.thenAnswer((_) async => 1);
playSingleAudio = _MockPlaySingleAudio();
when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume')))
.thenAnswer((_) async => audioPlayer);
pool = PinballAudioPool(
path: 'path',
poolSize: 1,
preCacheSingleAudio: preCacheSingleAudio.onCall,
playSingleAudio: playSingleAudio.onCall,
duration: const Duration(milliseconds: 10),
);
});
test('pre cache the sound', () async {
await pool.load();
verify(() => preCacheSingleAudio.onCall('path')).called(1);
});
test('plays a fresh sound', () async {
await pool.load();
await pool.play();
verify(
() => playSingleAudio.onCall(
'path',
volume: any(named: 'volume'),
),
).called(1);
});
test('plays from the pool after it returned', () async {
await pool.load();
await pool.play();
await Future<void>.delayed(const Duration(milliseconds: 12));
await pool.play();
verify(() => audioPlayer.play('path')).called(1);
});
});
}

@ -3,33 +3,22 @@ import 'dart:math';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
class _MockAudioPool extends Mock implements AudioPool {}
class _MockAudioCache extends Mock implements AudioCache {} class _MockAudioCache extends Mock implements AudioCache {}
class _MockCreateAudioPool extends Mock { class _MockAudioPlayer extends Mock implements AudioPlayer {}
Future<AudioPool> onCall(
String sound, {
bool? repeating,
int? maxPlayers,
int? minPlayers,
String? prefix,
});
}
class _MockConfigureAudioCache extends Mock { class _MockConfigureAudioCache extends Mock {
void onCall(AudioCache cache); void onCall(AudioCache cache);
} }
class _MockPlaySingleAudio extends Mock { class _MockPlaySingleAudio extends Mock {
Future<void> onCall(String path, {double volume}); Future<AudioPlayer> onCall(String path, {double volume});
} }
class _MockLoopSingleAudio extends Mock { class _MockLoopSingleAudio extends Mock {
@ -48,7 +37,6 @@ class _MockClock extends Mock implements Clock {}
void main() { void main() {
group('PinballAudio', () { group('PinballAudio', () {
late _MockCreateAudioPool createAudioPool;
late _MockConfigureAudioCache configureAudioCache; late _MockConfigureAudioCache configureAudioCache;
late _MockPlaySingleAudio playSingleAudio; late _MockPlaySingleAudio playSingleAudio;
late _MockLoopSingleAudio loopSingleAudio; late _MockLoopSingleAudio loopSingleAudio;
@ -61,21 +49,12 @@ void main() {
}); });
setUp(() { setUp(() {
createAudioPool = _MockCreateAudioPool();
when(
() => createAudioPool.onCall(
any(),
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => _MockAudioPool());
configureAudioCache = _MockConfigureAudioCache(); configureAudioCache = _MockConfigureAudioCache();
when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); when(() => configureAudioCache.onCall(any())).thenAnswer((_) {});
playSingleAudio = _MockPlaySingleAudio(); playSingleAudio = _MockPlaySingleAudio();
when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume'))) when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume')))
.thenAnswer((_) async {}); .thenAnswer((_) async => _MockAudioPlayer());
loopSingleAudio = _MockLoopSingleAudio(); loopSingleAudio = _MockLoopSingleAudio();
when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume'))) when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume')))
@ -88,7 +67,6 @@ void main() {
audioPlayer = PinballAudioPlayer( audioPlayer = PinballAudioPlayer(
configureAudioCache: configureAudioCache.onCall, configureAudioCache: configureAudioCache.onCall,
createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
loopSingleAudio: loopSingleAudio.onCall, loopSingleAudio: loopSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall,
@ -101,64 +79,6 @@ void main() {
}); });
group('load', () { group('load', () {
test('creates the bumpers pools', () async {
await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.bumperA}',
maxPlayers: 4,
prefix: '',
),
).called(1);
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.bumperB}',
maxPlayers: 4,
prefix: '',
),
).called(1);
});
test('creates the kicker pools', () async {
await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.kickerA}',
maxPlayers: 4,
prefix: '',
),
).called(1);
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.kickerB}',
maxPlayers: 4,
prefix: '',
),
).called(1);
});
test('creates the flipper pool', () async {
await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.flipper}',
maxPlayers: 2,
prefix: '',
),
).called(1);
});
test('configures the audio cache instance', () async { test('configures the audio cache instance', () async {
await Future.wait( await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()), audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
@ -170,7 +90,6 @@ void main() {
test('sets the correct prefix', () async { test('sets the correct prefix', () async {
audioPlayer = PinballAudioPlayer( audioPlayer = PinballAudioPlayer(
createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall,
); );
@ -186,6 +105,26 @@ void main() {
audioPlayer.load().map((loadableBuilder) => loadableBuilder()), audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
); );
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/bumper_a.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/bumper_b.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/kicker_a.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/kicker_b.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/flipper.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/google.mp3'), .onCall('packages/pinball_audio/assets/sfx/google.mp3'),
@ -236,33 +175,6 @@ void main() {
}); });
group('bumper', () { group('bumper', () {
late AudioPool bumperAPool;
late AudioPool bumperBPool;
setUp(() {
bumperAPool = _MockAudioPool();
when(() => bumperAPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.bumperA}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => bumperAPool);
bumperBPool = _MockAudioPool();
when(() => bumperBPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.bumperB}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => bumperBPool);
});
group('when seed is true', () { group('when seed is true', () {
test('plays the bumper A sound pool', () async { test('plays the bumper A sound pool', () async {
when(seed.nextBool).thenReturn(true); when(seed.nextBool).thenReturn(true);
@ -271,7 +183,12 @@ void main() {
); );
audioPlayer.play(PinballAudio.bumper); audioPlayer.play(PinballAudio.bumper);
verify(() => bumperAPool.start(volume: 0.6)).called(1); verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.bumperA}',
volume: 0.6,
),
).called(1);
}); });
}); });
@ -283,39 +200,17 @@ void main() {
); );
audioPlayer.play(PinballAudio.bumper); audioPlayer.play(PinballAudio.bumper);
verify(() => bumperBPool.start(volume: 0.6)).called(1); verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.bumperB}',
volume: 0.6,
),
).called(1);
}); });
}); });
}); });
group('kicker', () { group('kicker', () {
late AudioPool kickerAPool;
late AudioPool kickerBPool;
setUp(() {
kickerAPool = _MockAudioPool();
when(() => kickerAPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.kickerA}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => kickerAPool);
kickerBPool = _MockAudioPool();
when(() => kickerBPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.kickerB}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => kickerBPool);
});
group('when seed is true', () { group('when seed is true', () {
test('plays the kicker A sound pool', () async { test('plays the kicker A sound pool', () async {
when(seed.nextBool).thenReturn(true); when(seed.nextBool).thenReturn(true);
@ -324,7 +219,12 @@ void main() {
); );
audioPlayer.play(PinballAudio.kicker); audioPlayer.play(PinballAudio.kicker);
verify(() => kickerAPool.start(volume: 0.6)).called(1); verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.kickerA}',
volume: 0.6,
),
).called(1);
}); });
}); });
@ -336,27 +236,17 @@ void main() {
); );
audioPlayer.play(PinballAudio.kicker); audioPlayer.play(PinballAudio.kicker);
verify(() => kickerBPool.start(volume: 0.6)).called(1); verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.kickerB}',
volume: 0.6,
),
).called(1);
}); });
}); });
}); });
group('flipper', () { group('flipper', () {
late AudioPool pool;
setUp(() {
pool = _MockAudioPool();
when(() => pool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.flipper}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => pool);
});
test('plays the flipper sound pool', () async { test('plays the flipper sound pool', () async {
when(seed.nextBool).thenReturn(true); when(seed.nextBool).thenReturn(true);
await Future.wait( await Future.wait(
@ -364,7 +254,12 @@ void main() {
); );
audioPlayer.play(PinballAudio.flipper); audioPlayer.play(PinballAudio.flipper);
verify(() => pool.start()).called(1); verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.flipper}',
volume: any(named: 'volume'),
),
).called(1);
}); });
}); });
@ -376,14 +271,21 @@ void main() {
audioPlayer.play(PinballAudio.cowMoo); audioPlayer.play(PinballAudio.cowMoo);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), 'packages/pinball_audio/${Assets.sfx.cowMoo}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
test('only plays the sound again after 2 seconds', () async { test('only plays the sound again after 2 seconds', () async {
final clock = _MockClock(); final clock = _MockClock();
await withClock(clock, () async { await withClock(clock, () async {
final audioPlayerInstance = _MockAudioPlayer();
when(
() => playSingleAudio.onCall(any(), volume: any(named: 'volume')),
).thenAnswer((_) async => audioPlayerInstance);
when(clock.now).thenReturn(DateTime(2022)); when(clock.now).thenReturn(DateTime(2022));
await Future.wait( await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()), audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
@ -393,16 +295,20 @@ void main() {
..play(PinballAudio.cowMoo); ..play(PinballAudio.cowMoo);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), 'packages/pinball_audio/${Assets.sfx.cowMoo}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 2)); when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 2));
audioPlayer.play(PinballAudio.cowMoo); audioPlayer.play(PinballAudio.cowMoo);
verify( verify(
() => playSingleAudio () => playSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), 'packages/pinball_audio/${Assets.sfx.cowMoo}',
volume: any(named: 'volume'),
),
).called(1); ).called(1);
}); });
}); });
@ -422,6 +328,44 @@ void main() {
), ),
).called(1); ).called(1);
}); });
test('uses the cached player on the second time', () async {
final audioPlayerCache = _MockAudioPlayer();
when(() => audioPlayerCache.play(any(), volume: any(named: 'volume')))
.thenAnswer((_) async => 0);
when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume')))
.thenAnswer((_) async => audioPlayerCache);
audioPlayer = PinballAudioPlayer(
configureAudioCache: configureAudioCache.onCall,
playSingleAudio: playSingleAudio.onCall,
loopSingleAudio: loopSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall,
seed: seed,
);
await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer.play(PinballAudio.google);
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.google}',
volume: any(named: 'volume'),
),
).called(1);
await Future.microtask(() {});
audioPlayer.play(PinballAudio.google);
verify(
() => audioPlayerCache.play(
'packages/pinball_audio/${Assets.sfx.google}',
volume: any(named: 'volume'),
),
).called(1);
});
}); });
group('sparky', () { group('sparky', () {
@ -454,6 +398,36 @@ void main() {
), ),
).called(1); ).called(1);
}); });
test('only plays the sound again after 6 seconds', () async {
final clock = _MockClock();
await withClock(clock, () async {
when(clock.now).thenReturn(DateTime(2022));
await Future.wait(
audioPlayer.load().map((loadableBuilder) => loadableBuilder()),
);
audioPlayer
..play(PinballAudio.dino)
..play(PinballAudio.dino);
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.dino}',
volume: any(named: 'volume'),
),
).called(1);
when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 6));
audioPlayer.play(PinballAudio.dino);
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.dino}',
volume: any(named: 'volume'),
),
).called(1);
});
});
}); });
group('android', () { group('android', () {

@ -28,14 +28,19 @@ class PlungerPullingBehavior extends Component
} }
} }
class PlungerAutoPullingBehavior extends PlungerPullingBehavior { class PlungerAutoPullingBehavior extends Component
PlungerAutoPullingBehavior({ with FlameBlocReader<PlungerCubit, PlungerState> {
required double strength, late final Plunger _plunger;
}) : super(strength: strength);
@override
Future<void> onLoad() async {
await super.onLoad();
_plunger = parent!.parent! as Plunger;
}
@override @override
void update(double dt) { void update(double dt) {
super.update(dt); if (!bloc.state.isAutoPulling) return;
final joint = _plunger.body.joints.whereType<PrismaticJoint>().single; final joint = _plunger.body.joints.whereType<PrismaticJoint>().single;
final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit(); final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit();

@ -8,4 +8,6 @@ class PlungerCubit extends Cubit<PlungerState> {
void pulled() => emit(PlungerState.pulling); void pulled() => emit(PlungerState.pulling);
void released() => emit(PlungerState.releasing); void released() => emit(PlungerState.releasing);
void autoPulled() => emit(PlungerState.autoPulling);
} }

@ -4,9 +4,13 @@ enum PlungerState {
pulling, pulling,
releasing, releasing,
autoPulling,
} }
extension PlungerStateX on PlungerState { extension PlungerStateX on PlungerState {
bool get isPulling => this == PlungerState.pulling; bool get isPulling =>
this == PlungerState.pulling || this == PlungerState.autoPulling;
bool get isReleasing => this == PlungerState.releasing; bool get isReleasing => this == PlungerState.releasing;
bool get isAutoPulling => this == PlungerState.autoPulling;
} }

@ -132,6 +132,7 @@ class _PlungerSpriteAnimationGroupComponent
animations = { animations = {
PlungerState.releasing: pullAnimation.reversed(), PlungerState.releasing: pullAnimation.reversed(),
PlungerState.pulling: pullAnimation, PlungerState.pulling: pullAnimation,
PlungerState.autoPulling: pullAnimation,
}; };
current = readBloc<PlungerCubit, PlungerState>().state; current = readBloc<PlungerCubit, PlungerState>().state;

@ -3,6 +3,7 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
@ -12,7 +13,7 @@ import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame { class _TestGame extends Forge2DGame {
Future<void> pump( Future<void> pump(
PlungerPullingBehavior behavior, { Component behavior, {
PlungerCubit? plungerBloc, PlungerCubit? plungerBloc,
}) async { }) async {
final plunger = Plunger.test(); final plunger = Plunger.test();
@ -85,62 +86,28 @@ void main() {
group('PlungerAutoPullingBehavior', () { group('PlungerAutoPullingBehavior', () {
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
PlungerAutoPullingBehavior(strength: 0), PlungerAutoPullingBehavior(),
isA<PlungerAutoPullingBehavior>(), isA<PlungerAutoPullingBehavior>(),
); );
}); });
flameTester.test('can be loaded', (game) async { flameTester.test('can be loaded', (game) async {
final behavior = PlungerAutoPullingBehavior(strength: 0); final behavior = PlungerAutoPullingBehavior();
await game.pump(behavior); await game.pump(behavior);
expect(game.descendants(), contains(behavior)); expect(game.descendants(), contains(behavior));
}); });
flameTester.test(
"pulls while joint hasn't reached limit",
(game) async {
final plungerBloc = _MockPlungerCubit();
whenListen<PlungerState>(
plungerBloc,
Stream.value(PlungerState.pulling),
initialState: PlungerState.pulling,
);
const strength = 2.0;
final behavior = PlungerAutoPullingBehavior(
strength: strength,
);
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final plunger = behavior.ancestors().whereType<Plunger>().single;
final joint = _MockPrismaticJoint();
when(joint.getJointTranslation).thenReturn(2);
when(joint.getLowerLimit).thenReturn(0);
plunger.body.joints.add(joint);
game.update(0);
expect(plunger.body.linearVelocity.x, equals(0));
expect(plunger.body.linearVelocity.y, equals(strength));
},
);
flameTester.test( flameTester.test(
'releases when joint reaches limit', 'releases when joint reaches limit',
(game) async { (game) async {
final plungerBloc = _MockPlungerCubit(); final plungerBloc = _MockPlungerCubit();
whenListen<PlungerState>( whenListen<PlungerState>(
plungerBloc, plungerBloc,
Stream.value(PlungerState.pulling), Stream.value(PlungerState.autoPulling),
initialState: PlungerState.pulling, initialState: PlungerState.autoPulling,
); );
const strength = 2.0; final behavior = PlungerAutoPullingBehavior();
final behavior = PlungerAutoPullingBehavior(
strength: strength,
);
await game.pump( await game.pump(
behavior, behavior,
plungerBloc: plungerBloc, plungerBloc: plungerBloc,

@ -0,0 +1,25 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('PlungerCubit', () {
test('can be instantiated', () {
expect(PlungerCubit(), isA<PlungerCubit>());
});
blocTest<PlungerCubit, PlungerState>(
'overrides previous pulling state',
build: PlungerCubit.new,
act: (cubit) => cubit
..pulled()
..autoPulled()
..pulled(),
expect: () => [
PlungerState.pulling,
PlungerState.autoPulling,
PlungerState.pulling,
],
);
});
}

@ -77,8 +77,45 @@ void main() {
); );
flameTester.test( flameTester.test(
'onNewState calls onCharacterSelected on the arcade background bloc', 'onNewState does not call onCharacterSelected on the arcade background '
'bloc when platform is mobile',
(game) async { (game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenAnswer((_) => true);
final arcadeBackgroundBloc = _MockArcadeBackgroundCubit();
whenListen(
arcadeBackgroundBloc,
const Stream<ArcadeBackgroundState>.empty(),
initialState: const ArcadeBackgroundState.initial(),
);
final behavior = CharacterSelectionBehavior();
await game.pump(
[
behavior,
ZCanvasComponent(),
Plunger.test(),
Ball.test(),
],
platformHelper: platformHelper,
);
const dinoThemeState = CharacterThemeState(theme.DinoTheme());
behavior.onNewState(dinoThemeState);
await game.ready();
verifyNever(
() => arcadeBackgroundBloc
.onCharacterSelected(dinoThemeState.characterTheme),
);
},
);
flameTester.test(
'onNewState calls onCharacterSelected on the arcade background '
'bloc when platform is not mobile',
(game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenAnswer((_) => false);
final arcadeBackgroundBloc = _MockArcadeBackgroundCubit(); final arcadeBackgroundBloc = _MockArcadeBackgroundCubit();
whenListen( whenListen(
arcadeBackgroundBloc, arcadeBackgroundBloc,
@ -96,6 +133,7 @@ void main() {
Plunger.test(), Plunger.test(),
Ball.test(), Ball.test(),
], ],
platformHelper: platformHelper,
); );
const dinoThemeState = CharacterThemeState(theme.DinoTheme()); const dinoThemeState = CharacterThemeState(theme.DinoTheme());

@ -36,7 +36,6 @@ class _TestGame extends Forge2DGame with HasTappables {
Future<void> pump( Future<void> pump(
Iterable<Component> children, { Iterable<Component> children, {
PinballAudioPlayer? pinballAudioPlayer, PinballAudioPlayer? pinballAudioPlayer,
PlatformHelper? platformHelper,
GoogleWordCubit? googleWordBloc, GoogleWordCubit? googleWordBloc,
}) async { }) async {
return ensureAdd( return ensureAdd(
@ -62,7 +61,7 @@ class _TestGame extends Forge2DGame with HasTappables {
_MockAppLocalizations(), _MockAppLocalizations(),
), ),
FlameProvider<PlatformHelper>.value( FlameProvider<PlatformHelper>.value(
platformHelper ?? PlatformHelper(), PlatformHelper(),
), ),
], ],
children: children, children: children,
@ -80,8 +79,6 @@ class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
class _MockShareRepository extends Mock implements ShareRepository {} class _MockShareRepository extends Mock implements ShareRepository {}
class _MockPlatformHelper extends Mock implements PlatformHelper {}
class _MockPlungerCubit extends Mock implements PlungerCubit {} class _MockPlungerCubit extends Mock implements PlungerCubit {}
class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {} class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {}
@ -278,7 +275,7 @@ void main() {
create: PlungerCubit.new, create: PlungerCubit.new,
children: [ children: [
PlungerPullingBehavior(strength: 0), PlungerPullingBehavior(strength: 0),
PlungerAutoPullingBehavior(strength: 0) PlungerAutoPullingBehavior()
], ],
), ),
); );
@ -460,10 +457,8 @@ void main() {
); );
flameTester.test( flameTester.test(
'adds PlungerKeyControllingBehavior to Plunger when on desktop', 'adds PlungerKeyControllingBehavior to Plunger',
(game) async { (game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenReturn(false);
final component = GameBlocStatusListener(); final component = GameBlocStatusListener();
final leaderboardRepository = _MockLeaderboardRepository(); final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository(); final shareRepository = _MockShareRepository();
@ -482,7 +477,6 @@ void main() {
bloc: _MockSignpostCubit(), bloc: _MockSignpostCubit(),
), ),
], ],
platformHelper: platformHelper,
); );
await plunger.ensureAdd( await plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>( FlameBlocProvider<PlungerCubit, PlungerState>(
@ -506,10 +500,8 @@ void main() {
); );
flameTester.test( flameTester.test(
'adds PlungerPullingBehavior to Plunger when on desktop', 'adds PlungerPullingBehavior to Plunger',
(game) async { (game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenReturn(false);
final component = GameBlocStatusListener(); final component = GameBlocStatusListener();
final leaderboardRepository = _MockLeaderboardRepository(); final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository(); final shareRepository = _MockShareRepository();
@ -528,7 +520,6 @@ void main() {
bloc: _MockSignpostCubit(), bloc: _MockSignpostCubit(),
), ),
], ],
platformHelper: platformHelper,
); );
await plunger.ensureAdd( await plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>( FlameBlocProvider<PlungerCubit, PlungerState>(
@ -549,10 +540,8 @@ void main() {
); );
flameTester.test( flameTester.test(
'adds PlungerAutoPullingBehavior to Plunger when on mobile', 'adds PlungerAutoPullingBehavior to Plunger',
(game) async { (game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenReturn(true);
final component = GameBlocStatusListener(); final component = GameBlocStatusListener();
final leaderboardRepository = _MockLeaderboardRepository(); final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository(); final shareRepository = _MockShareRepository();
@ -571,7 +560,6 @@ void main() {
bloc: _MockSignpostCubit(), bloc: _MockSignpostCubit(),
), ),
], ],
platformHelper: platformHelper,
); );
await plunger.ensureAdd( await plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>( FlameBlocProvider<PlungerCubit, PlungerState>(

@ -246,9 +246,61 @@ void main() {
}); });
group('flipper control', () { group('flipper control', () {
flameTester.test('tap control only works if game is playing',
(game) async {
await game.ready();
final gameBloc = game
.descendants()
.whereType<FlameBlocProvider<GameBloc, GameState>>()
.first
.bloc;
final eventPosition = _MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.zero());
when(() => eventPosition.widget).thenReturn(Vector2.zero());
final raw = _MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final tapDownEvent = _MockTapDownInfo();
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final flipperBloc = game
.descendants()
.whereType<Flipper>()
.where((flipper) => flipper.side == BoardSide.left)
.single
.descendants()
.whereType<FlameBlocProvider<FlipperCubit, FlipperState>>()
.first
.bloc;
gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.gameOver));
game.onTapDown(0, tapDownEvent);
await Future<void>.delayed(Duration.zero);
expect(flipperBloc.state, FlipperState.movingDown);
gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing));
game.onTapDown(0, tapDownEvent);
await Future<void>.delayed(Duration.zero);
expect(flipperBloc.state, FlipperState.movingUp);
});
flameTester.test('tap down moves left flipper up', (game) async { flameTester.test('tap down moves left flipper up', (game) async {
await game.ready(); await game.ready();
final gameBloc = game
.descendants()
.whereType<FlameBlocProvider<GameBloc, GameState>>()
.first
.bloc;
gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing));
final eventPosition = _MockEventPosition(); final eventPosition = _MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.game).thenReturn(Vector2.zero());
when(() => eventPosition.widget).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero());
@ -278,6 +330,14 @@ void main() {
flameTester.test('tap down moves right flipper up', (game) async { flameTester.test('tap down moves right flipper up', (game) async {
await game.ready(); await game.ready();
final gameBloc = game
.descendants()
.whereType<FlameBlocProvider<GameBloc, GameState>>()
.first
.bloc;
gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing));
final eventPosition = _MockEventPosition(); final eventPosition = _MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.game).thenReturn(Vector2.zero());
when(() => eventPosition.widget).thenReturn(game.canvasSize); when(() => eventPosition.widget).thenReturn(game.canvasSize);
@ -307,6 +367,14 @@ void main() {
flameTester.test('tap up moves flipper down', (game) async { flameTester.test('tap up moves flipper down', (game) async {
await game.ready(); await game.ready();
final gameBloc = game
.descendants()
.whereType<FlameBlocProvider<GameBloc, GameState>>()
.first
.bloc;
gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing));
final eventPosition = _MockEventPosition(); final eventPosition = _MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.game).thenReturn(Vector2.zero());
when(() => eventPosition.widget).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero());
@ -332,6 +400,14 @@ void main() {
flameTester.test('tap cancel moves flipper down', (game) async { flameTester.test('tap cancel moves flipper down', (game) async {
await game.ready(); await game.ready();
final gameBloc = game
.descendants()
.whereType<FlameBlocProvider<GameBloc, GameState>>()
.first
.bloc;
gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing));
final eventPosition = _MockEventPosition(); final eventPosition = _MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.game).thenReturn(Vector2.zero());
when(() => eventPosition.widget).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero());
@ -363,6 +439,14 @@ void main() {
(game) async { (game) async {
await game.ready(); await game.ready();
final gameBloc = game
.descendants()
.whereType<FlameBlocProvider<GameBloc, GameState>>()
.first
.bloc;
gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing));
final raw = _MockTapDownDetails(); final raw = _MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch); when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
@ -416,6 +500,14 @@ void main() {
flameTester.test('plunger control tap down emits plunging', (game) async { flameTester.test('plunger control tap down emits plunging', (game) async {
await game.ready(); await game.ready();
final gameBloc = game
.descendants()
.whereType<FlameBlocProvider<GameBloc, GameState>>()
.first
.bloc;
gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing));
final eventPosition = _MockEventPosition(); final eventPosition = _MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2(40, 60)); when(() => eventPosition.game).thenReturn(Vector2(40, 60));
@ -434,7 +526,7 @@ void main() {
.single .single
.bloc; .bloc;
expect(plungerBloc.state, PlungerState.pulling); expect(plungerBloc.state, PlungerState.autoPulling);
}); });
}); });
}); });

Loading…
Cancel
Save