diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index cab754f6..e5b09d56 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -3,12 +3,12 @@ export 'board.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; -export 'controlled_multiball.dart'; export 'controlled_plunger.dart'; export 'flutter_forest/flutter_forest.dart'; export 'game_flow_controller.dart'; export 'google_word/google_word.dart'; export 'launcher.dart'; +export 'multiballs/multiballs.dart'; export 'scoring_behavior.dart'; export 'sparky_fire_zone.dart'; export 'wall.dart'; diff --git a/lib/game/components/controlled_multiball.dart b/lib/game/components/controlled_multiball.dart deleted file mode 100644 index 748761a5..00000000 --- a/lib/game/components/controlled_multiball.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template multiball_group_component} -/// A [SpriteGroupComponent] for the multiball over the board. -/// {@endtemplate} -class MultiballGroup extends Component - with Controls, HasGameRef { - /// {@macro multiball_group_component} - MultiballGroup() : super() { - controller = MultiballController(this); - } - - /// Bottom left multiball. - late final Multiball multiballA; - - /// Center left multiball. - late final Multiball multiballB; - - /// Center right multiball. - late final Multiball multiballC; - - /// Bottom right multiball. - late final Multiball multiballD; - - @override - Future onLoad() async { - await super.onLoad(); - - multiballA = Multiball.a(); - multiballB = Multiball.b(); - multiballC = Multiball.c(); - multiballD = Multiball.d(); - - await addAll([ - multiballA, - multiballB, - multiballC, - multiballD, - ]); - } -} - -/// {@template multiball_controller} -/// Controller attached to a [MultiballGroup] that handles its game related -/// logic. -/// {@endtemplate} -@visibleForTesting -class MultiballController extends ComponentController - with BlocComponent, HasGameRef { - /// {@macro multiball_controller} - MultiballController(MultiballGroup multiballGroup) : super(multiballGroup); - - @override - bool listenWhen(GameState? previousState, GameState newState) { - return previousState?.bonusHistory != newState.bonusHistory; - } - - @override - void onNewState(GameState state) { - final hasMultiball = state.bonusHistory.contains(GameBonus.dashNest); - - if (hasMultiball) { - // TODO(ruimiguel): change to animate every children without different - // properties using component.children.whereType().forEach - // once able to mock the children ComponentSet. - component.multiballA.animate(); - component.multiballB.animate(); - component.multiballC.animate(); - component.multiballD.animate(); - } - } -} diff --git a/lib/game/components/multiballs/behaviors/behaviors.dart b/lib/game/components/multiballs/behaviors/behaviors.dart new file mode 100644 index 00000000..921063dc --- /dev/null +++ b/lib/game/components/multiballs/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multiballs_behavior.dart'; diff --git a/lib/game/components/multiballs/behaviors/multiballs_behavior.dart b/lib/game/components/multiballs/behaviors/multiballs_behavior.dart new file mode 100644 index 00000000..6d555651 --- /dev/null +++ b/lib/game/components/multiballs/behaviors/multiballs_behavior.dart @@ -0,0 +1,24 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Toggle each [Multiball] when there is a bonus ball. +class MultiballsBehavior extends Component + with HasGameRef, ParentIsA { + @override + void onMount() { + super.onMount(); + + gameRef.read().stream.listen((state) { + final hasMultiball = state.bonusHistory.contains(GameBonus.dashNest); + + if (hasMultiball) { + final multiballs = parent.children.whereType(); + for (final multiball in multiballs) { + multiball.bloc.animate(); + } + } + }); + } +} diff --git a/lib/game/components/multiballs/multiballs.dart b/lib/game/components/multiballs/multiballs.dart new file mode 100644 index 00000000..47e753fd --- /dev/null +++ b/lib/game/components/multiballs/multiballs.dart @@ -0,0 +1,27 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template multiballs_component} +/// A [SpriteGroupComponent] for the multiball over the board. +/// {@endtemplate} +class Multiballs extends Component { + /// {@macro multiballs_component} + Multiballs() + : super( + children: [ + Multiball.a(), + Multiball.b(), + Multiball.c(), + Multiball.d(), + MultiballsBehavior(), + ], + ); + + /// Creates a [Multiballs] without any children. + /// + /// This can be used for testing [Multiballs]'s behaviors in isolation. + @visibleForTesting + Multiballs.test(); +} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 26987452..d824acaf 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -53,7 +53,7 @@ class PinballGame extends Forge2DGame final launcher = Launcher(); unawaited(addFromBlueprint(launcher)); unawaited(add(Board())); - unawaited(add(MultiballGroup())); + await add(Multiballs()); await addFromBlueprint(AlienZone()); await addFromBlueprint(SparkyFireZone()); diff --git a/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart index c266e123..257e0ce0 100644 --- a/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart +++ b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart @@ -37,3 +37,17 @@ class MultiballBlinkingBehavior extends TimerComponent parent.bloc.onBlinked(); } } + +/* +/// Animates the [Multiball]. + Future animate() async { + final spriteGroupComponent = firstChild(); + + for (var i = 0; i < 5; i++) { + spriteGroupComponent?.current = MultiballState.lit; + await Future.delayed(const Duration(milliseconds: 100)); + spriteGroupComponent?.current = MultiballState.dimmed; + await Future.delayed(const Duration(milliseconds: 100)); + } + } + */ \ No newline at end of file diff --git a/packages/pinball_components/lib/src/components/multiball/multiball.dart b/packages/pinball_components/lib/src/components/multiball/multiball.dart index 662e281e..db9468db 100644 --- a/packages/pinball_components/lib/src/components/multiball/multiball.dart +++ b/packages/pinball_components/lib/src/components/multiball/multiball.dart @@ -102,18 +102,6 @@ class Multiball extends Component { bloc.close(); super.onRemove(); } - - /// Animates the [Multiball]. - Future animate() async { - final spriteGroupComponent = firstChild(); - - for (var i = 0; i < 5; i++) { - spriteGroupComponent?.current = MultiballState.lit; - await Future.delayed(const Duration(milliseconds: 100)); - spriteGroupComponent?.current = MultiballState.dimmed; - await Future.delayed(const Duration(milliseconds: 100)); - } - } } /// {@template multiball_sprite_group_component} diff --git a/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart b/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart index f3802ed3..32202d22 100644 --- a/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart @@ -8,14 +8,14 @@ class MultiballGame extends BallGame with KeyboardEvents { MultiballGame() : super( imagesFileNames: [ - Assets.images.multiball.a.active.keyName, - Assets.images.multiball.a.inactive.keyName, - Assets.images.multiball.b.active.keyName, - Assets.images.multiball.b.inactive.keyName, - Assets.images.multiball.c.active.keyName, - Assets.images.multiball.c.inactive.keyName, - Assets.images.multiball.d.active.keyName, - Assets.images.multiball.d.inactive.keyName, + Assets.images.multiball.a.lit.keyName, + Assets.images.multiball.a.dimmed.keyName, + Assets.images.multiball.b.lit.keyName, + Assets.images.multiball.b.dimmed.keyName, + Assets.images.multiball.c.lit.keyName, + Assets.images.multiball.c.dimmed.keyName, + Assets.images.multiball.d.lit.keyName, + Assets.images.multiball.d.dimmed.keyName, ], ); @@ -23,13 +23,15 @@ class MultiballGame extends BallGame with KeyboardEvents { Shows how the Multiball are rendered. - Tap anywhere on the screen to spawn a ball into the game. - - Press space bar for animate state multiballs. + - Press space bar to animate multiballs. '''; - late final Multiball _multiballA; - late final Multiball _multiballB; - late final Multiball _multiballC; - late final Multiball _multiballD; + final List multiballs = [ + Multiball.a(), + Multiball.b(), + Multiball.c(), + Multiball.d(), + ]; @override Future onLoad() async { @@ -37,12 +39,7 @@ class MultiballGame extends BallGame with KeyboardEvents { camera.followVector2(Vector2.zero()); - await addAll([ - _multiballA = Multiball.a(), - _multiballB = Multiball.b(), - _multiballC = Multiball.c(), - _multiballD = Multiball.d(), - ]); + await addAll(multiballs); await traceAllBodies(); } @@ -53,10 +50,9 @@ class MultiballGame extends BallGame with KeyboardEvents { ) { if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.space) { - _multiballA.animate(); - _multiballB.animate(); - _multiballC.animate(); - _multiballD.animate(); + for (final multiball in multiballs) { + multiball.bloc.animate(); + } return KeyEventResult.handled; } diff --git a/test/game/components/controlled_multiball_test.dart b/test/game/components/controlled_multiball_test.dart deleted file mode 100644 index 89bd6067..00000000 --- a/test/game/components/controlled_multiball_test.dart +++ /dev/null @@ -1,124 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.multiball.a.active.keyName, - Assets.images.multiball.a.inactive.keyName, - Assets.images.multiball.b.active.keyName, - Assets.images.multiball.b.inactive.keyName, - Assets.images.multiball.c.active.keyName, - Assets.images.multiball.c.inactive.keyName, - Assets.images.multiball.d.active.keyName, - Assets.images.multiball.d.inactive.keyName, - ]; - final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); - - group('MultiballGroup', () { - flameTester.test( - 'loads correctly', - (game) async { - final multiballGroup = MultiballGroup(); - await game.ensureAdd(multiballGroup); - - expect(game.contains(multiballGroup), isTrue); - }, - ); - - group('loads', () { - flameTester.test( - 'four Multiball', - (game) async { - final multiballGroup = MultiballGroup(); - await game.ensureAdd(multiballGroup); - - expect( - multiballGroup.descendants().whereType().length, - equals(4), - ); - }, - ); - }); - }); - group('MultiballController', () { - group('controller', () { - group('listenWhen', () { - flameTester.test( - 'listens when obtain a multiball bonus', - (game) async { - const previous = GameState.initial(); - final state = previous.copyWith(bonusHistory: [GameBonus.dashNest]); - - final multiballGroup = MultiballGroup(); - await game.ensureAdd(multiballGroup); - - expect( - multiballGroup.controller.listenWhen(previous, state), - isTrue, - ); - }, - ); - - flameTester.test( - "doesn't listen when bonus is the same", - (game) async { - const previous = GameState.initial(); - - final multiballGroup = MultiballGroup(); - await game.ensureAdd(multiballGroup); - - expect( - multiballGroup.controller.listenWhen(previous, previous), - isFalse, - ); - }, - ); - }); - - group( - 'onNewState', - () { - flameTester.test( - 'blink multiballs when state changes', - (game) async { - // TODO(ruimiguel): search how to mock MultiballGroup children - // ComponentSet to improve this test. - final multiballGroup = MockMultiballGroup(); - final multiballA = MockMultiball(); - final multiballB = MockMultiball(); - final multiballC = MockMultiball(); - final multiballD = MockMultiball(); - final controller = MultiballController(multiballGroup); - when(() => multiballGroup.multiballA).thenReturn(multiballA); - when(() => multiballGroup.multiballB).thenReturn(multiballB); - when(() => multiballGroup.multiballC).thenReturn(multiballC); - when(() => multiballGroup.multiballD).thenReturn(multiballD); - when(multiballA.animate).thenAnswer((_) async => () {}); - when(multiballB.animate).thenAnswer((_) async => () {}); - when(multiballC.animate).thenAnswer((_) async => () {}); - when(multiballD.animate).thenAnswer((_) async => () {}); - - controller.onNewState( - const GameState.initial() - .copyWith(bonusHistory: [GameBonus.dashNest]), - ); - - verify(multiballA.animate).called(1); - verify(multiballB.animate).called(1); - verify(multiballC.animate).called(1); - verify(multiballD.animate).called(1); - }, - ); - }, - ); - }); - }); -} diff --git a/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart new file mode 100644 index 00000000..f60de2d1 --- /dev/null +++ b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiball.a.lit.keyName, + Assets.images.multiball.a.dimmed.keyName, + Assets.images.multiball.b.lit.keyName, + Assets.images.multiball.b.dimmed.keyName, + Assets.images.multiball.c.lit.keyName, + Assets.images.multiball.c.dimmed.keyName, + Assets.images.multiball.d.lit.keyName, + Assets.images.multiball.d.dimmed.keyName, + ]; + + group('MultiballsBehavior', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + }); +} diff --git a/test/game/components/multiballs/multiballs_test.dart b/test/game/components/multiballs/multiballs_test.dart new file mode 100644 index 00000000..8e830a9f --- /dev/null +++ b/test/game/components/multiballs/multiballs_test.dart @@ -0,0 +1,61 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiball.a.lit.keyName, + Assets.images.multiball.a.dimmed.keyName, + Assets.images.multiball.b.lit.keyName, + Assets.images.multiball.b.dimmed.keyName, + Assets.images.multiball.c.lit.keyName, + Assets.images.multiball.c.dimmed.keyName, + Assets.images.multiball.d.lit.keyName, + Assets.images.multiball.d.dimmed.keyName, + ]; + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + group('Multiballs', () { + flameBlocTester.testGameWidget( + 'loads correctly', + setUp: (game, tester) async { + final multiballs = Multiballs(); + await game.ensureAdd(multiballs); + + expect(game.contains(multiballs), isTrue); + }, + ); + + group('loads', () { + flameBlocTester.testGameWidget( + 'four Multiball', + setUp: (game, tester) async { + final multiballs = Multiballs(); + await game.ensureAdd(multiballs); + + expect( + multiballs.descendants().whereType().length, + equals(4), + ); + }, + ); + }); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 8c1d4e46..148b46cb 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -56,14 +56,14 @@ void main() { Assets.images.slingshot.lower.keyName, Assets.images.dino.dinoLandTop.keyName, Assets.images.dino.dinoLandBottom.keyName, - Assets.images.multiball.a.active.keyName, - Assets.images.multiball.a.inactive.keyName, - Assets.images.multiball.b.active.keyName, - Assets.images.multiball.b.inactive.keyName, - Assets.images.multiball.c.active.keyName, - Assets.images.multiball.c.inactive.keyName, - Assets.images.multiball.d.active.keyName, - Assets.images.multiball.d.inactive.keyName, + Assets.images.multiball.a.lit.keyName, + Assets.images.multiball.a.dimmed.keyName, + Assets.images.multiball.b.lit.keyName, + Assets.images.multiball.b.dimmed.keyName, + Assets.images.multiball.c.lit.keyName, + Assets.images.multiball.c.dimmed.keyName, + Assets.images.multiball.d.lit.keyName, + Assets.images.multiball.d.dimmed.keyName, ]; final flameTester = FlameTester( () => PinballTestGame(assets: assets), @@ -108,12 +108,12 @@ void main() { }); flameTester.test( - 'has only one Multiball', + 'has only one Multiballs', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.children.whereType().length, equals(1), ); }, diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index dc69149d..9b5dcacb 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -88,4 +88,4 @@ class MockSparkyBumper extends Mock implements SparkyBumper {} class MockMultiball extends Mock implements Multiball {} -class MockMultiballGroup extends Mock implements MultiballGroup {} +class MockMultiballGroup extends Mock implements Multiballs {}