diff --git a/lib/game/behaviors/behaviors.dart b/lib/game/behaviors/behaviors.dart index 44cce1df..c7ad6880 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -1,4 +1,5 @@ export 'ball_spawning_behavior.dart'; +export 'bonus_noise_behavior.dart'; export 'bumper_noise_behavior.dart'; export 'camera_focusing_behavior.dart'; export 'scoring_behavior.dart'; diff --git a/lib/game/behaviors/bonus_noise_behavior.dart b/lib/game/behaviors/bonus_noise_behavior.dart new file mode 100644 index 00000000..061982f5 --- /dev/null +++ b/lib/game/behaviors/bonus_noise_behavior.dart @@ -0,0 +1,41 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Behavior that handles playing a bonus sound effect +class BonusNoiseBehavior extends Component { + @override + Future onLoad() async { + await add( + FlameBlocListener( + listenWhen: (previous, current) { + return previous.bonusHistory.length != current.bonusHistory.length; + }, + onNewState: (state) { + final bonus = state.bonusHistory.last; + final audioPlayer = readProvider(); + + switch(bonus) { + case GameBonus.googleWord: + audioPlayer.play(PinballAudio.google); + break; + case GameBonus.sparkyTurboCharge: + audioPlayer.play(PinballAudio.sparky); + break; + case GameBonus.dinoChomp: + // TODO(erickzanardo): Add sound + break; + case GameBonus.androidSpaceship: + // TODO(erickzanardo): Add sound + break; + case GameBonus.dashNest: + // TODO(erickzanardo): Add sound + break; + } + }, + ), + ); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 63321974..b96b6a65 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -12,4 +12,4 @@ export 'google_word/google_word.dart'; export 'launcher.dart'; export 'multiballs/multiballs.dart'; export 'multipliers/multipliers.dart'; -export 'sparky_scorch/sparky_scorch.dart'; +export 'sparky_scorch.dart'; diff --git a/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart index e49d4537..586b8547 100644 --- a/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart +++ b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart @@ -1,7 +1,6 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -22,7 +21,6 @@ class GoogleWordBonusBehavior extends Component .every((letter) => letter.bloc.state == GoogleLetterState.lit); if (achievedBonus) { - readProvider().play(PinballAudio.google); bloc.add(const BonusActivated(GameBonus.googleWord)); for (final letter in googleLetters) { letter.bloc.onReset(); diff --git a/lib/game/components/sparky_scorch/sparky_scorch.dart b/lib/game/components/sparky_scorch.dart similarity index 96% rename from lib/game/components/sparky_scorch/sparky_scorch.dart rename to lib/game/components/sparky_scorch.dart index af49c548..b820e89d 100644 --- a/lib/game/components/sparky_scorch/sparky_scorch.dart +++ b/lib/game/components/sparky_scorch.dart @@ -6,8 +6,6 @@ import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/components/components.dart'; import 'package:pinball_components/pinball_components.dart'; -export 'sparky_scorch_noise_behavior.dart'; - /// {@template sparky_scorch} /// Area positioned at the top left of the board containing the /// [SparkyComputer], [SparkyAnimatronic], and [SparkyBumper]s. @@ -54,7 +52,6 @@ class SparkyComputerSensor extends BodyComponent renderBody: false, children: [ ScoringContactBehavior(points: Points.twentyThousand), - SparkyScorchNoiseBehavior(), ], ); diff --git a/lib/game/components/sparky_scorch/sparky_scorch_noise_behavior.dart b/lib/game/components/sparky_scorch/sparky_scorch_noise_behavior.dart deleted file mode 100644 index 5ac61747..00000000 --- a/lib/game/components/sparky_scorch/sparky_scorch_noise_behavior.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// Plays the sparky animation sound when in contact with a [Ball] -class SparkyScorchNoiseBehavior extends ContactBehavior { - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is Ball) { - readProvider().play(PinballAudio.sparky); - } - } -} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index b4886e4c..62e2976e 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -64,6 +64,7 @@ class PinballGame extends PinballForge2DGame FlameProvider.value(_l10n), ], children: [ + BonusNoiseBehavior(), GameBlocStatusListener(), BallSpawningBehavior(), CameraFocusingBehavior(), diff --git a/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart b/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart index e097f359..5e250285 100644 --- a/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart +++ b/packages/pinball_flame/lib/src/canvas/z_canvas_component.dart @@ -56,7 +56,9 @@ class _ZCanvas extends CanvasWrapper { final List _zBuffer = []; /// Postpones the rendering of [ZIndex] component and its children. - void buffer(ZIndex component) => _zBuffer.add(component); + void buffer(ZIndex component) => _zBuffer + ..add(component) + ..sort((a, b) => a.zIndex.compareTo(b.zIndex)); /// Renders all [ZIndex] components and their children. /// @@ -69,8 +71,7 @@ class _ZCanvas extends CanvasWrapper { /// before the second one. /// {@endtemplate} void render() => _zBuffer - ..sort((a, b) => a.zIndex.compareTo(b.zIndex)) - ..whereType().forEach(_render) + ..forEach(_render) ..clear(); void _render(Component component) => component.renderTree(canvas); diff --git a/test/game/behaviors/bonus_noise_behavior_test.dart b/test/game/behaviors/bonus_noise_behavior_test.dart new file mode 100644 index 00000000..163f344e --- /dev/null +++ b/test/game/behaviors/bonus_noise_behavior_test.dart @@ -0,0 +1,187 @@ +// ignore_for_file: cascade_invocations + +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/game/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + BonusNoiseBehavior child, { + required PinballPlayer player, + required GameBloc bloc, + }) { + return ensureAdd( + FlameBlocProvider.value( + value: bloc, + children: [ + FlameProvider.value( + player, + children: [ + child, + ], + ), + ], + ), + ); + } +} + +class _MockPinballPlayer extends Mock implements PinballPlayer {} + +class _MockGameBloc extends Mock implements GameBloc {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('BonusNoiseBehavior', () { + late PinballPlayer player; + late GameBloc bloc; + final flameTester = FlameTester(_TestGame.new); + + setUpAll(() { + registerFallbackValue(PinballAudio.google); + }); + + setUp(() { + player = _MockPinballPlayer(); + when(() => player.play(any())) + .thenAnswer((_) { }); + bloc = _MockGameBloc(); + }); + + flameTester.testGameWidget( + 'plays google sound', + setUp: (game, _) async { + const state = GameState( + totalScore: 0, + roundScore: 0, + multiplier: 1, + rounds: 0, + bonusHistory: [GameBonus.googleWord], + status: GameStatus.playing, + ); + const initialState = GameState.initial(); + whenListen( + bloc, + Stream.fromIterable([initialState, state]), + initialState: initialState, + ); + final behavior = BonusNoiseBehavior(); + await game.pump(behavior, player: player, bloc: bloc); + }, + verify: (_, __) async { + verify(() => player.play(PinballAudio.google)).called(1); + }, + ); + + flameTester.testGameWidget( + 'plays sparky sound', + setUp: (game, _) async { + const state = GameState( + totalScore: 0, + roundScore: 0, + multiplier: 1, + rounds: 0, + bonusHistory: [GameBonus.sparkyTurboCharge], + status: GameStatus.playing, + ); + const initialState = GameState.initial(); + whenListen( + bloc, + Stream.fromIterable([initialState, state]), + initialState: initialState, + ); + final behavior = BonusNoiseBehavior(); + await game.pump(behavior, player: player, bloc: bloc); + }, + verify: (_, __) async { + verify(() => player.play(PinballAudio.sparky)).called(1); + }, + ); + + flameTester.testGameWidget( + 'plays dino chomp sound', + setUp: (game, _) async { + const state = GameState( + totalScore: 0, + roundScore: 0, + multiplier: 1, + rounds: 0, + bonusHistory: [GameBonus.dinoChomp], + status: GameStatus.playing, + ); + const initialState = GameState.initial(); + whenListen( + bloc, + Stream.fromIterable([initialState, state]), + initialState: initialState, + ); + final behavior = BonusNoiseBehavior(); + await game.pump(behavior, player: player, bloc: bloc); + }, + verify: (_, __) async { + // TODO(erickzanardo): Change when the sound is implemented + verifyNever(() => player.play(any())); + }, + ); + + flameTester.testGameWidget( + 'plays android spaceship sound', + setUp: (game, _) async { + const state = GameState( + totalScore: 0, + roundScore: 0, + multiplier: 1, + rounds: 0, + bonusHistory: [GameBonus.androidSpaceship], + status: GameStatus.playing, + ); + const initialState = GameState.initial(); + whenListen( + bloc, + Stream.fromIterable([initialState, state]), + initialState: initialState, + ); + final behavior = BonusNoiseBehavior(); + await game.pump(behavior, player: player, bloc: bloc); + }, + verify: (_, __) async { + // TODO(erickzanardo): Change when the sound is implemented + verifyNever(() => player.play(any())); + }, + ); + + flameTester.testGameWidget( + 'plays dash nest sound', + setUp: (game, _) async { + const state = GameState( + totalScore: 0, + roundScore: 0, + multiplier: 1, + rounds: 0, + bonusHistory: [GameBonus.dashNest], + status: GameStatus.playing, + ); + const initialState = GameState.initial(); + whenListen( + bloc, + Stream.fromIterable([initialState, state]), + initialState: initialState, + ); + final behavior = BonusNoiseBehavior(); + await game.pump(behavior, player: player, bloc: bloc); + }, + verify: (_, __) async { + // TODO(erickzanardo): Change when the sound is implemented + verifyNever(() => player.play(any())); + }, + ); + }); +} diff --git a/test/game/components/sparky_scorch/sparky_scorch_noise_behavior_test.dart b/test/game/components/sparky_scorch/sparky_scorch_noise_behavior_test.dart deleted file mode 100644 index 14cb39e8..00000000 --- a/test/game/components/sparky_scorch/sparky_scorch_noise_behavior_test.dart +++ /dev/null @@ -1,75 +0,0 @@ -// ignore_for_file: cascade_invocations - -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/game/components/components.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(_TestBodyComponent child, {required PinballPlayer player}) { - return ensureAdd( - FlameProvider.value( - player, - children: [ - child, - ], - ), - ); - } -} - -class _TestBodyComponent extends BodyComponent { - @override - Body createBody() => world.createBody(BodyDef()); -} - -class _MockPinballPlayer extends Mock implements PinballPlayer {} - -class _MockContact extends Mock implements Contact {} - -class _MockBall extends Mock implements Ball {} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('SparkyScorchNoiseBehavior', () {}); - - late PinballPlayer player; - final flameTester = FlameTester(_TestGame.new); - - setUp(() { - player = _MockPinballPlayer(); - }); - - flameTester.testGameWidget( - 'plays sparky sound', - setUp: (game, _) async { - final behavior = SparkyScorchNoiseBehavior(); - final parent = _TestBodyComponent(); - await game.pump(parent, player: player); - await parent.ensureAdd(behavior); - behavior.beginContact(_MockBall(), _MockContact()); - }, - verify: (_, __) async { - verify(() => player.play(PinballAudio.sparky)).called(1); - }, - ); - - flameTester.testGameWidget( - 'plays nothing when it is not a ball', - setUp: (game, _) async { - final behavior = SparkyScorchNoiseBehavior(); - final parent = _TestBodyComponent(); - await game.pump(parent, player: player); - await parent.ensureAdd(behavior); - behavior.beginContact(Object(), _MockContact()); - }, - verify: (_, __) async { - verifyNever(() => player.play(PinballAudio.sparky)); - }, - ); -} diff --git a/test/game/components/sparky_scorch/sparky_scorch_test.dart b/test/game/components/sparky_scorch_test.dart similarity index 100% rename from test/game/components/sparky_scorch/sparky_scorch_test.dart rename to test/game/components/sparky_scorch_test.dart