diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index efd085a5..6ffd2ad3 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -61,6 +61,11 @@ class GameBlocStatusListener extends Component .single .bloc .onReset(); + gameRef + .descendants() + .whereType() + .forEach((bumper) => bumper.bloc.onReset()); + gameRef.descendants().whereType().single.bloc.onReset(); } void _addPlungerBehaviors(Plunger plunger) { diff --git a/packages/pinball_audio/assets/sfx/flipper.mp3 b/packages/pinball_audio/assets/sfx/flipper.mp3 new file mode 100644 index 00000000..c4d31a47 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/flipper.mp3 differ diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart index 0b8fb20b..915573b4 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -20,6 +20,7 @@ class $AssetsSfxGen { String get cowMoo => 'assets/sfx/cow_moo.mp3'; String get dash => 'assets/sfx/dash.mp3'; String get dino => 'assets/sfx/dino.mp3'; + String get flipper => 'assets/sfx/flipper.mp3'; String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3'; String get google => 'assets/sfx/google.mp3'; String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 1e1a7688..c62f197d 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -39,14 +39,17 @@ enum PinballAudio { /// Sparky. sparky, - /// Android + /// Android. android, - /// Dino + /// Dino. dino, - /// Dash + /// Dash. dash, + + /// Flipper. + flipper, } /// Defines the contract of the creation of an [AudioPool]. @@ -147,6 +150,31 @@ class _SingleLoopAudio extends _LoopAudio { } } +class _SingleAudioPool extends _Audio { + _SingleAudioPool({ + required this.path, + required this.createAudioPool, + required this.maxPlayers, + }); + + final String path; + final CreateAudioPool createAudioPool; + final int maxPlayers; + late AudioPool pool; + + @override + Future load() async { + pool = await createAudioPool( + prefixFile(path), + maxPlayers: maxPlayers, + prefix: '', + ); + } + + @override + void play() => pool.start(); +} + class _RandomABAudio extends _Audio { _RandomABAudio({ required this.createAudioPool, @@ -277,6 +305,11 @@ class PinballAudioPlayer { path: Assets.sfx.rollover, volume: 0.3, ), + PinballAudio.flipper: _SingleAudioPool( + path: Assets.sfx.flipper, + createAudioPool: _createAudioPool, + maxPlayers: 2, + ), PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( preCacheSingleAudio: _preCacheSingleAudio, playSingleAudio: _playSingleAudio, diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 47d788bf..554e9752 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -145,6 +145,20 @@ void main() { ).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 { await Future.wait( audioPlayer.load().map((loadableBuilder) => loadableBuilder()), @@ -327,6 +341,33 @@ void main() { }); }); + 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 { + when(seed.nextBool).thenReturn(true); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); + audioPlayer.play(PinballAudio.flipper); + + verify(() => pool.start()).called(1); + }); + }); + group('cow moo', () { test('plays the correct file', () async { await Future.wait( diff --git a/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart index d3743ae9..2325227d 100644 --- a/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart @@ -1,3 +1,4 @@ export 'flipper_jointing_behavior.dart'; export 'flipper_key_controlling_behavior.dart'; export 'flipper_moving_behavior.dart'; +export 'flipper_noise_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_moving_behavior.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_moving_behavior.dart index 13989192..e0189fdb 100644 --- a/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_moving_behavior.dart +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_moving_behavior.dart @@ -15,7 +15,9 @@ class FlipperMovingBehavior extends Component late final Flipper _flipper; - void _moveUp() => _flipper.body.linearVelocity = Vector2(0, -_strength); + void _moveUp() { + _flipper.body.linearVelocity = Vector2(0, -_strength); + } void _moveDown() => _flipper.body.linearVelocity = Vector2(0, _strength); diff --git a/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_noise_behavior.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_noise_behavior.dart new file mode 100644 index 00000000..6dc4a88e --- /dev/null +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_noise_behavior.dart @@ -0,0 +1,18 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class FlipperNoiseBehavior extends Component + with + FlameBlocListenable, + FlameBlocReader { + @override + void onNewState(FlipperState state) { + super.onNewState(state); + if (bloc.state.isMovingUp) { + readProvider().play(PinballAudio.flipper); + } + } +} diff --git a/packages/pinball_components/lib/src/components/flipper/flipper.dart b/packages/pinball_components/lib/src/components/flipper/flipper.dart index 265e7924..cc6ef13a 100644 --- a/packages/pinball_components/lib/src/components/flipper/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper/flipper.dart @@ -25,7 +25,10 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { FlipperJointingBehavior(), FlameBlocProvider( create: FlipperCubit.new, - children: [FlipperMovingBehavior(strength: 90)], + children: [ + FlipperMovingBehavior(strength: 90), + FlipperNoiseBehavior(), + ], ), ], ); diff --git a/packages/pinball_components/lib/src/components/signpost/cubit/signpost_cubit.dart b/packages/pinball_components/lib/src/components/signpost/cubit/signpost_cubit.dart index dc5bce9c..3beb638c 100644 --- a/packages/pinball_components/lib/src/components/signpost/cubit/signpost_cubit.dart +++ b/packages/pinball_components/lib/src/components/signpost/cubit/signpost_cubit.dart @@ -12,5 +12,7 @@ class SignpostCubit extends Cubit { ); } + void onReset() => emit(SignpostState.inactive); + bool isFullyProgressed() => state == SignpostState.active3; } diff --git a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_moving_behavior_test.dart b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_moving_behavior_test.dart index be48d795..de379201 100644 --- a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_moving_behavior_test.dart +++ b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_moving_behavior_test.dart @@ -8,12 +8,14 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; class _TestGame extends Forge2DGame { Future pump( FlipperMovingBehavior behavior, { FlipperCubit? flipperBloc, + PinballAudioPlayer? audioPlayer, }) async { final flipper = Flipper.test(side: BoardSide.left); await ensureAdd(flipper); diff --git a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_noise_behavior_test.dart b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_noise_behavior_test.dart new file mode 100644 index 00000000..2a046a0b --- /dev/null +++ b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_noise_behavior_test.dart @@ -0,0 +1,79 @@ +// ignore_for_file: avoid_dynamic_calls, cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + FlipperNoiseBehavior behavior, { + FlipperCubit? flipperBloc, + PinballAudioPlayer? audioPlayer, + }) async { + final flipper = Flipper.test(side: BoardSide.left); + await ensureAdd( + FlameProvider.value( + audioPlayer ?? _MockPinballAudioPlayer(), + children: [ + flipper, + ], + ), + ); + await flipper.ensureAdd( + FlameBlocProvider.value( + value: flipperBloc ?? FlipperCubit(), + children: [behavior], + ), + ); + } +} + +class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} + +class _MockFlipperCubit extends Mock implements FlipperCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + + group('FlipperNoiseBehavior', () { + test('can be instantiated', () { + expect( + FlipperNoiseBehavior(), + isA(), + ); + }); + + flameTester.test( + 'plays the flipper sound when moving up', + (game) async { + final audioPlayer = _MockPinballAudioPlayer(); + final bloc = _MockFlipperCubit(); + whenListen( + bloc, + Stream.fromIterable([FlipperState.movingUp]), + initialState: FlipperState.movingUp, + ); + + final behavior = FlipperNoiseBehavior(); + await game.pump( + behavior, + flipperBloc: bloc, + audioPlayer: audioPlayer, + ); + behavior.onNewState(FlipperState.movingUp); + game.update(0); + + verify(() => audioPlayer.play(PinballAudio.flipper)).called(1); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/signpost/cubit/signpost_cubit_test.dart b/packages/pinball_components/test/src/components/signpost/cubit/signpost_cubit_test.dart index 081beab2..bd41aa33 100644 --- a/packages/pinball_components/test/src/components/signpost/cubit/signpost_cubit_test.dart +++ b/packages/pinball_components/test/src/components/signpost/cubit/signpost_cubit_test.dart @@ -22,6 +22,13 @@ void main() { ], ); + blocTest( + 'onReset emits inactive', + build: SignpostCubit.new, + act: (bloc) => bloc.onReset(), + expect: () => [SignpostState.inactive], + ); + test('isFullyProgressed when on active3', () { final bloc = SignpostCubit(); expect(bloc.isFullyProgressed(), isFalse); diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart index 47a273ba..069731fd 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -86,6 +86,10 @@ class _MockPlungerCubit extends Mock implements PlungerCubit {} class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {} +class _MockDashBumperCubit extends Mock implements DashBumperCubit {} + +class _MockSignpostCubit extends Mock implements SignpostCubit {} + class _MockFlipperCubit extends Mock implements FlipperCubit {} class _MockAppLocalizations extends Mock implements AppLocalizations { @@ -332,7 +336,15 @@ void main() { (game) async { final audioPlayer = _MockPinballAudioPlayer(); final component = GameBlocStatusListener(); - await game.pump([component], pinballAudioPlayer: audioPlayer); + await game.pump( + [ + component, + Signpost.test( + bloc: _MockSignpostCubit(), + ), + ], + pinballAudioPlayer: audioPlayer, + ); expect(state.status, equals(GameStatus.playing)); component.onNewState(state); @@ -350,7 +362,15 @@ void main() { (game) async { final googleWordBloc = _MockGoogleWordCubit(); final component = GameBlocStatusListener(); - await game.pump([component], googleWordBloc: googleWordBloc); + await game.pump( + [ + component, + Signpost.test( + bloc: _MockSignpostCubit(), + ), + ], + googleWordBloc: googleWordBloc, + ); expect(state.status, equals(GameStatus.playing)); component.onNewState(state); @@ -359,6 +379,46 @@ void main() { }, ); + flameTester.test( + 'resets the DashBumperCubits', + (game) async { + final dashBumper1Bloc = _MockDashBumperCubit(); + final dashBumper2Bloc = _MockDashBumperCubit(); + final dashBumper1 = DashBumper.test(bloc: dashBumper1Bloc); + final dashBumper2 = DashBumper.test(bloc: dashBumper2Bloc); + final component = GameBlocStatusListener(); + await game.pump([ + component, + dashBumper1, + dashBumper2, + Signpost.test( + bloc: _MockSignpostCubit(), + ), + ]); + + expect(state.status, equals(GameStatus.playing)); + component.onNewState(state); + + verify(dashBumper1Bloc.onReset).called(1); + verify(dashBumper2Bloc.onReset).called(1); + }, + ); + + flameTester.test( + 'resets the SignpostCubit', + (game) async { + final signpostBloc = _MockSignpostCubit(); + final signpost = Signpost.test(bloc: signpostBloc); + final component = GameBlocStatusListener(); + await game.pump([component, signpost]); + + expect(state.status, equals(GameStatus.playing)); + component.onNewState(state); + + verify(signpostBloc.onReset).called(1); + }, + ); + flameTester.test( 'adds FlipperKeyControllingBehavior to Flippers', (game) async { @@ -372,7 +432,14 @@ void main() { ); final flipper = Flipper.test(side: BoardSide.left); - await game.pump([component, backbox, flipper]); + await game.pump([ + component, + backbox, + flipper, + Signpost.test( + bloc: _MockSignpostCubit(), + ), + ]); await flipper.ensureAdd( FlameBlocProvider( create: _MockFlipperCubit.new, @@ -407,7 +474,14 @@ void main() { ); final plunger = Plunger.test(); await game.pump( - [component, backbox, plunger], + [ + component, + backbox, + plunger, + Signpost.test( + bloc: _MockSignpostCubit(), + ), + ], platformHelper: platformHelper, ); await plunger.ensureAdd( @@ -446,7 +520,14 @@ void main() { ); final plunger = Plunger.test(); await game.pump( - [component, backbox, plunger], + [ + component, + backbox, + plunger, + Signpost.test( + bloc: _MockSignpostCubit(), + ), + ], platformHelper: platformHelper, ); await plunger.ensureAdd( @@ -482,7 +563,14 @@ void main() { ); final plunger = Plunger.test(); await game.pump( - [component, backbox, plunger], + [ + component, + backbox, + plunger, + Signpost.test( + bloc: _MockSignpostCubit(), + ), + ], platformHelper: platformHelper, ); await plunger.ensureAdd(