diff --git a/lib/flame/component_controller.dart b/lib/flame/component_controller.dart index 2bbf5ca9..851028f0 100644 --- a/lib/flame/component_controller.dart +++ b/lib/flame/component_controller.dart @@ -1,5 +1,6 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flutter/foundation.dart'; /// {@template component_controller} /// A [ComponentController] is a [Component] in charge of handling the logic @@ -30,6 +31,7 @@ mixin Controls on Component { late final T controller; @override + @mustCallSuper Future onLoad() async { await super.onLoad(); await add(controller); diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart index 6eb3ce7d..2d7bdf33 100644 --- a/lib/game/components/flutter_forest.dart +++ b/lib/game/components/flutter_forest.dart @@ -1,11 +1,10 @@ // ignore_for_file: avoid_renaming_method_parameters -import 'dart:math' as math; - import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; +import 'package:pinball/flame/flame.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -16,10 +15,47 @@ import 'package:pinball_components/pinball_components.dart'; /// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] /// is awarded, and the [BigDashNestBumper] releases a new [Ball]. /// {@endtemplate} -// TODO(alestiago): Make a [Blueprint] once nesting [Blueprint] is implemented. -class FlutterForest extends Component - with HasGameRef, BlocComponent { +// TODO(alestiago): Make a [Blueprint] once [Blueprint] inherits from +// [Component]. +class FlutterForest extends Component with Controls<_FlutterForestController> { /// {@macro flutter_forest} + FlutterForest() { + controller = _FlutterForestController(this); + } + + @override + Future onLoad() async { + await super.onLoad(); + final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); + + final bigNest = _ControlledBigDashNestBumper( + id: 'big_nest_bumper', + )..initialPosition = Vector2(18.55, 59.35); + final smallLeftNest = _ControlledSmallDashNestBumper.a( + id: 'small_nest_bumper_a', + )..initialPosition = Vector2(8.95, 51.95); + final smallRightNest = _ControlledSmallDashNestBumper.b( + id: 'small_nest_bumper_b', + )..initialPosition = Vector2(23.3, 46.75); + + await addAll([ + signPost, + smallLeftNest, + smallRightNest, + bigNest, + ]); + } +} + +class _FlutterForestController extends ComponentController + with BlocComponent, HasGameRef { + _FlutterForestController(FlutterForest flutterForest) : super(flutterForest); + + @override + Future onLoad() async { + await super.onLoad(); + gameRef.addContactCallback(_ControlledDashNestBumperBallContactCallback()); + } @override bool listenWhen(GameState? previousState, GameState newState) { @@ -32,117 +68,90 @@ class FlutterForest extends Component void onNewState(GameState state) { super.onNewState(state); - add( - ControlledBall.bonus( - theme: gameRef.theme, - )..initialPosition = Vector2(17.2, 52.7), + component.add( + ControlledBall.bonus(theme: gameRef.theme) + ..initialPosition = Vector2(17.2, 52.7), ); } +} - @override - Future onLoad() async { - gameRef.addContactCallback(DashNestBumperBallContactCallback()); +class _ControlledBigDashNestBumper extends BigDashNestBumper + with Controls, ScorePoints { + _ControlledBigDashNestBumper({required String id}) : super() { + controller = DashNestBumperController(this, id: id); + } - final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); + @override + int get points => 20; +} - // TODO(alestiago): adjust positioning once sprites are added. - final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest') - ..initialPosition = Vector2(8.95, 51.95); - final smallRightNest = SmallDashNestBumper(id: 'small_right_nest') - ..initialPosition = Vector2(23.3, 46.75); - final bigNest = BigDashNestBumper(id: 'big_nest') - ..initialPosition = Vector2(18.55, 59.35); +class _ControlledSmallDashNestBumper extends SmallDashNestBumper + with Controls, ScorePoints { + _ControlledSmallDashNestBumper.a({required String id}) : super.a() { + controller = DashNestBumperController(this, id: id); + } - await addAll([ - signPost, - smallLeftNest, - smallRightNest, - bigNest, - ]); + _ControlledSmallDashNestBumper.b({required String id}) : super.b() { + controller = DashNestBumperController(this, id: id); } + + @override + int get points => 10; } -/// {@template dash_nest_bumper} -/// Bumper located in the [FlutterForest]. +/// {@template dash_nest_bumper_controller} +/// Controls a [DashNestBumper]. /// {@endtemplate} @visibleForTesting -abstract class DashNestBumper extends BodyComponent - with ScorePoints, InitialPosition { - /// {@macro dash_nest_bumper} - DashNestBumper({required this.id}) { - paint = Paint() - ..color = Colors.blue.withOpacity(0.5) - ..style = PaintingStyle.fill; - } - - /// Unique identifier for this [DashNestBumper]. +class DashNestBumperController extends ComponentController + with BlocComponent, HasGameRef { + /// {@macro dash_nest_bumper_controller} + DashNestBumperController( + DashNestBumper dashNestBumper, { + required this.id, + }) : super(dashNestBumper); + + /// Unique identifier for the controlled [DashNestBumper]. /// /// Used to identify [DashNestBumper]s in [GameState.activatedDashNests]. final String id; -} -/// Listens when a [Ball] bounces bounces against a [DashNestBumper]. -@visibleForTesting -class DashNestBumperBallContactCallback - extends ContactCallback { @override - void begin(DashNestBumper dashNestBumper, Ball ball, Contact _) { - dashNestBumper.gameRef.read().add( - DashNestActivated(dashNestBumper.id), - ); - } -} + bool listenWhen(GameState? previousState, GameState newState) { + final wasActive = previousState?.activatedDashNests.contains(id) ?? false; + final isActive = newState.activatedDashNests.contains(id); -/// {@macro dash_nest_bumper} -@visibleForTesting -class BigDashNestBumper extends DashNestBumper { - /// {@macro dash_nest_bumper} - BigDashNestBumper({required String id}) : super(id: id); + return wasActive != isActive; + } @override - int get points => 20; + void onNewState(GameState state) { + super.onNewState(state); - @override - Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: 4.85, - minorRadius: 3.95, - )..rotate(math.pi / 2); - final fixtureDef = FixtureDef(shape); - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - - return world.createBody(bodyDef)..createFixture(fixtureDef); + if (state.activatedDashNests.contains(id)) { + component.activate(); + } else { + component.deactivate(); + } } -} -/// {@macro dash_nest_bumper} -@visibleForTesting -class SmallDashNestBumper extends DashNestBumper { - /// {@macro dash_nest_bumper} - SmallDashNestBumper({required String id}) : super(id: id); - - @override - int get points => 10; + /// Registers when a [DashNestBumper] is hit by a [Ball]. + /// + /// Triggered by [_ControlledDashNestBumperBallContactCallback]. + void hit() { + gameRef.read().add(DashNestActivated(id)); + } +} +/// Listens when a [Ball] bounces bounces against a [DashNestBumper]. +class _ControlledDashNestBumperBallContactCallback + extends ContactCallback, Ball> { @override - Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: 3, - minorRadius: 2.25, - )..rotate(math.pi / 2); - final fixtureDef = FixtureDef(shape) - ..friction = 0 - ..restitution = 4; - - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - - return world.createBody(bodyDef)..createFixture(fixtureDef); + void begin( + Controls controlledDashNestBumper, + Ball _, + Contact __, + ) { + controlledDashNestBumper.controller.hit(); } } diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index edfe7947..cc8aac9c 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -15,6 +15,12 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.baseboard.right.keyName), images.load(components.Assets.images.dino.dinoLandTop.keyName), images.load(components.Assets.images.dino.dinoLandBottom.keyName), + images.load(components.Assets.images.dashBumper.a.active.keyName), + images.load(components.Assets.images.dashBumper.a.inactive.keyName), + images.load(components.Assets.images.dashBumper.b.active.keyName), + images.load(components.Assets.images.dashBumper.b.inactive.keyName), + images.load(components.Assets.images.dashBumper.main.active.keyName), + images.load(components.Assets.images.dashBumper.main.inactive.keyName), images.load(Assets.images.components.background.path), ]); } diff --git a/packages/pinball_components/assets/images/dash_bumper/a/active.png b/packages/pinball_components/assets/images/dash_bumper/a/active.png new file mode 100644 index 00000000..feeee11f Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/a/active.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/a/inactive.png b/packages/pinball_components/assets/images/dash_bumper/a/inactive.png new file mode 100644 index 00000000..58ab8c56 Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/a/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/b/active.png b/packages/pinball_components/assets/images/dash_bumper/b/active.png new file mode 100644 index 00000000..4bc2897f Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/b/active.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/b/inactive.png b/packages/pinball_components/assets/images/dash_bumper/b/inactive.png new file mode 100644 index 00000000..eddc7693 Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/b/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/main/active.png b/packages/pinball_components/assets/images/dash_bumper/main/active.png new file mode 100644 index 00000000..bef56684 Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/main/active.png differ diff --git a/packages/pinball_components/assets/images/dash_bumper/main/inactive.png b/packages/pinball_components/assets/images/dash_bumper/main/inactive.png new file mode 100644 index 00000000..e6f15b38 Binary files /dev/null and b/packages/pinball_components/assets/images/dash_bumper/main/inactive.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 8bd651ed..cf32e986 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -14,6 +14,8 @@ class $AssetsImagesGen { AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); + $AssetsImagesDashBumperGen get dashBumper => + const $AssetsImagesDashBumperGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); @@ -42,6 +44,15 @@ class $AssetsImagesBaseboardGen { const AssetGenImage('assets/images/baseboard/right.png'); } +class $AssetsImagesDashBumperGen { + const $AssetsImagesDashBumperGen(); + + $AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen(); + $AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen(); + $AssetsImagesDashBumperMainGen get main => + const $AssetsImagesDashBumperMainGen(); +} + class $AssetsImagesDinoGen { const $AssetsImagesDinoGen(); @@ -66,6 +77,42 @@ class $AssetsImagesFlipperGen { const AssetGenImage('assets/images/flipper/right.png'); } +class $AssetsImagesDashBumperAGen { + const $AssetsImagesDashBumperAGen(); + + /// File path: assets/images/dash_bumper/a/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/dash_bumper/a/active.png'); + + /// File path: assets/images/dash_bumper/a/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/dash_bumper/a/inactive.png'); +} + +class $AssetsImagesDashBumperBGen { + const $AssetsImagesDashBumperBGen(); + + /// File path: assets/images/dash_bumper/b/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/dash_bumper/b/active.png'); + + /// File path: assets/images/dash_bumper/b/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/dash_bumper/b/inactive.png'); +} + +class $AssetsImagesDashBumperMainGen { + const $AssetsImagesDashBumperMainGen(); + + /// File path: assets/images/dash_bumper/main/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/dash_bumper/main/active.png'); + + /// File path: assets/images/dash_bumper/main/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/dash_bumper/main/inactive.png'); +} + class Assets { Assets._(); diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 4e38c2c4..bbb2c29c 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -2,6 +2,7 @@ export 'ball.dart'; export 'baseboard.dart'; export 'board_dimensions.dart'; export 'board_side.dart'; +export 'dash_nest_bumper.dart'; export 'dino_walls.dart'; export 'fire_effect.dart'; export 'flipper.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper.dart new file mode 100644 index 00000000..a2b9b982 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper.dart @@ -0,0 +1,142 @@ +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template dash_nest_bumper} +/// Bumper with a nest appearance. +/// {@endtemplate} +abstract class DashNestBumper extends BodyComponent with InitialPosition { + /// {@macro dash_nest_bumper} + DashNestBumper._({ + required String activeAssetPath, + required String inactiveAssetPath, + required SpriteComponent spriteComponent, + }) : _activeAssetPath = activeAssetPath, + _inactiveAssetPath = inactiveAssetPath, + _spriteComponent = spriteComponent; + + final String _activeAssetPath; + late final Sprite _activeSprite; + final String _inactiveAssetPath; + late final Sprite _inactiveSprite; + final SpriteComponent _spriteComponent; + + Future _loadSprites() async { + // TODO(alestiago): I think ideally we would like to do: + // Sprite(path).load so we don't require to store the activeAssetPath and + // the inactive assetPath. + _inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath); + _activeSprite = await gameRef.loadSprite(_activeAssetPath); + } + + /// Activates the [DashNestBumper]. + void activate() { + _spriteComponent + ..sprite = _activeSprite + ..size = _activeSprite.originalSize / 10; + } + + /// Deactivates the [DashNestBumper]. + void deactivate() { + _spriteComponent + ..sprite = _inactiveSprite + ..size = _inactiveSprite.originalSize / 10; + } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprites(); + + // TODO(erickzanardo): Look into using onNewState instead. + // Currently doing: onNewState(gameRef.read()) will throw an + // `Exception: build context is not available yet` + deactivate(); + await add(_spriteComponent); + } +} + +/// {@macro dash_nest_bumper} +class BigDashNestBumper extends DashNestBumper { + /// {@macro dash_nest_bumper} + BigDashNestBumper() + : super._( + activeAssetPath: Assets.images.dashBumper.main.active.keyName, + inactiveAssetPath: Assets.images.dashBumper.main.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + ), + ); + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: 4.85, + minorRadius: 3.95, + )..rotate(math.pi / 2); + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +/// {@macro dash_nest_bumper} +class SmallDashNestBumper extends DashNestBumper { + /// {@macro dash_nest_bumper} + SmallDashNestBumper._({ + required String activeAssetPath, + required String inactiveAssetPath, + required SpriteComponent spriteComponent, + }) : super._( + activeAssetPath: activeAssetPath, + inactiveAssetPath: inactiveAssetPath, + spriteComponent: spriteComponent, + ); + + /// {@macro dash_nest_bumper} + SmallDashNestBumper.a() + : this._( + activeAssetPath: Assets.images.dashBumper.a.active.keyName, + inactiveAssetPath: Assets.images.dashBumper.a.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0.35, -1.2), + ), + ); + + /// {@macro dash_nest_bumper} + SmallDashNestBumper.b() + : this._( + activeAssetPath: Assets.images.dashBumper.b.active.keyName, + inactiveAssetPath: Assets.images.dashBumper.b.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0.35, -1.2), + ), + ); + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: 3, + minorRadius: 2.25, + )..rotate(math.pi / 2); + final fixtureDef = FixtureDef(shape) + ..friction = 0 + ..restitution = 4; + + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index d1f138d9..8fc9c6f8 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -29,6 +29,9 @@ flutter: - assets/images/baseboard/ - assets/images/dino/ - assets/images/flipper/ + - assets/images/dash_bumper/a/ + - assets/images/dash_bumper/b/ + - assets/images/dash_bumper/main/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart new file mode 100644 index 00000000..2c6bb00c --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart @@ -0,0 +1,116 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('BigDashNestBumper', () { + flameTester.test('loads correctly', (game) async { + final bumper = BigDashNestBumper(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('activate returns normally', (game) async { + final bumper = BigDashNestBumper(); + await game.ensureAdd(bumper); + + expect(bumper.activate, returnsNormally); + }); + + flameTester.test('deactivate returns normally', (game) async { + final bumper = BigDashNestBumper(); + await game.ensureAdd(bumper); + + expect(bumper.deactivate, returnsNormally); + }); + + flameTester.test('changes sprite', (game) async { + final bumper = BigDashNestBumper(); + await game.ensureAdd(bumper); + + final spriteComponent = bumper.firstChild()!; + + final deactivatedSprite = spriteComponent.sprite; + bumper.activate(); + expect( + spriteComponent.sprite, + isNot(equals(deactivatedSprite)), + ); + + final activatedSprite = spriteComponent.sprite; + bumper.deactivate(); + expect( + spriteComponent.sprite, + isNot(equals(activatedSprite)), + ); + + expect( + activatedSprite, + isNot(equals(deactivatedSprite)), + ); + }); + }); + + group('SmallDashNestBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final bumper = SmallDashNestBumper.a(); + await game.ensureAdd(bumper); + + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final bumper = SmallDashNestBumper.b(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('activate returns normally', (game) async { + final bumper = SmallDashNestBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.activate, returnsNormally); + }); + + flameTester.test('deactivate returns normally', (game) async { + final bumper = SmallDashNestBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.deactivate, returnsNormally); + }); + + flameTester.test('changes sprite', (game) async { + final bumper = SmallDashNestBumper.a(); + await game.ensureAdd(bumper); + + final spriteComponent = bumper.firstChild()!; + + final deactivatedSprite = spriteComponent.sprite; + bumper.activate(); + expect( + spriteComponent.sprite, + isNot(equals(deactivatedSprite)), + ); + + final activatedSprite = spriteComponent.sprite; + bumper.deactivate(); + expect( + spriteComponent.sprite, + isNot(equals(activatedSprite)), + ); + + expect( + activatedSprite, + isNot(equals(deactivatedSprite)), + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/fire_effect_test.dart b/packages/pinball_components/test/src/components/fire_effect_test.dart index bc6baa4b..7bc62212 100644 --- a/packages/pinball_components/test/src/components/fire_effect_test.dart +++ b/packages/pinball_components/test/src/components/fire_effect_test.dart @@ -48,8 +48,9 @@ void main() { final canvas = MockCanvas(); effect.render(canvas); - verify(() => canvas.drawCircle(any(), any(), any())) - .called(greaterThan(0)); + verify(() => canvas.drawCircle(any(), any(), any())).called( + greaterThan(0), + ); }); }); } diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index 48586895..a0e1b81f 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -1,7 +1,9 @@ // ignore_for_file: cascade_invocations import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -9,6 +11,18 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; +void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) { + assert( + bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty, + 'Bodies require fixtures to contact each other.', + ); + + final fixtureA = bodyA.body.fixtures.first; + final fixtureB = bodyB.body.fixtures.first; + final contact = Contact.init(fixtureA, 0, fixtureB, 0); + game.world.contactManager.contactListener?.beginContact(contact); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.create); @@ -30,13 +44,73 @@ void main() { 'a FlutterSignPost', (game) async { await game.ready(); + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); expect( - game.descendants().whereType().length, + flutterForest.descendants().whereType().length, equals(1), ); }, ); + + flameTester.test( + 'a BigDashNestBumper', + (game) async { + await game.ready(); + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'two SmallDashNestBumper', + (game) async { + await game.ready(); + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(2), + ); + }, + ); + }); + + group('controller', () { + group('listenWhen', () { + final gameBloc = MockGameBloc(); + final tester = flameBlocTester( + game: TestGame.new, + gameBloc: () => gameBloc, + ); + + tester.testGameWidget( + 'listens when a Bonus.dashNest is added', + verify: (game, tester) async { + final flutterForest = FlutterForest(); + + const state = GameState( + score: 0, + balls: 3, + activatedBonusLetters: [], + activatedDashNests: {}, + bonusHistory: [GameBonus.dashNest], + ); + expect( + flutterForest.controller + .listenWhen(const GameState.initial(), state), + isTrue, + ); + }, + ); + }); }); flameTester.test( @@ -47,7 +121,7 @@ void main() { await game.ensureAdd(flutterForest); final previousBalls = game.descendants().whereType().length; - flutterForest.onNewState(MockGameState()); + flutterForest.controller.onNewState(MockGameState()); await game.ready(); expect( @@ -57,14 +131,13 @@ void main() { }, ); - group('listenWhen', () { - final gameBloc = MockGameBloc(); - final tester = flameBlocTester( - game: TestGame.new, - gameBloc: () => gameBloc, - ); + group('bumpers', () { + late Ball ball; + late GameBloc gameBloc; setUp(() { + ball = Ball(baseColor: const Color(0xFF00FFFF)); + gameBloc = MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -72,73 +145,167 @@ void main() { ); }); + final tester = flameBlocTester( + game: PinballGameTest.create, + gameBloc: () => gameBloc, + ); + + tester.testGameWidget( + 'add DashNestActivated event', + setUp: (game, tester) async { + await game.ready(); + final flutterForest = + game.descendants().whereType().first; + await game.ensureAdd(ball); + + final bumpers = + flutterForest.descendants().whereType(); + + for (final bumper in bumpers) { + beginContact(game, bumper, ball); + final controller = bumper.firstChild()!; + verify( + () => gameBloc.add(DashNestActivated(controller.id)), + ).called(1); + } + }, + ); + tester.testGameWidget( - 'listens when a Bonus.dashNest is added', - verify: (game, tester) async { + 'add Scored event', + setUp: (game, tester) async { final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + await game.ensureAdd(ball); - const state = GameState( - score: 0, - balls: 3, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [GameBonus.dashNest], - ); - expect( - flutterForest.listenWhen(const GameState.initial(), state), - isTrue, - ); + final bumpers = + flutterForest.descendants().whereType(); + + for (final bumper in bumpers) { + beginContact(game, bumper, ball); + final points = (bumper as ScorePoints).points; + verify( + () => gameBloc.add(Scored(points: points)), + ).called(1); + } }, ); }); }); - group('DashNestBumperBallContactCallback', () { - final gameBloc = MockGameBloc(); - final tester = flameBlocTester( - // TODO(alestiago): Use TestGame.new once a controller is implemented. - game: PinballGameTest.create, - gameBloc: () => gameBloc, - ); + group('DashNestBumperController', () { + late DashNestBumper dashNestBumper; setUp(() { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); + dashNestBumper = MockDashNestBumper(); }); - final dashNestBumper = MockDashNestBumper(); - tester.testGameWidget( - 'adds a DashNestActivated event with DashNestBumper.id', - setUp: (game, tester) async { - const id = '0'; - when(() => dashNestBumper.id).thenReturn(id); - when(() => dashNestBumper.gameRef).thenReturn(game); - }, - verify: (game, tester) async { - final contactCallback = DashNestBumperBallContactCallback(); - contactCallback.begin(dashNestBumper, MockBall(), MockContact()); + group( + 'listensWhen', + () { + late GameState previousState; + late GameState newState; - verify( - () => gameBloc.add(DashNestActivated(dashNestBumper.id)), - ).called(1); + setUp( + () { + previousState = MockGameState(); + newState = MockGameState(); + }, + ); + + test('listens when the id is added to activatedDashNests', () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => previousState.activatedDashNests).thenReturn({}); + when(() => newState.activatedDashNests).thenReturn({id}); + + expect(controller.listenWhen(previousState, newState), isTrue); + }); + + test('listens when the id is removed from activatedDashNests', () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => previousState.activatedDashNests).thenReturn({id}); + when(() => newState.activatedDashNests).thenReturn({}); + + expect(controller.listenWhen(previousState, newState), isTrue); + }); + + test("doesn't listen when the id is never in activatedDashNests", () { + final controller = DashNestBumperController( + dashNestBumper, + id: '', + ); + + when(() => previousState.activatedDashNests).thenReturn({}); + when(() => newState.activatedDashNests).thenReturn({}); + + expect(controller.listenWhen(previousState, newState), isFalse); + }); + + test("doesn't listen when the id still in activatedDashNests", () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => previousState.activatedDashNests).thenReturn({id}); + when(() => newState.activatedDashNests).thenReturn({id}); + + expect(controller.listenWhen(previousState, newState), isFalse); + }); }, ); - }); - group('BigDashNestBumper', () { - test('has points', () { - final dashNestBumper = BigDashNestBumper(id: ''); - expect(dashNestBumper.points, greaterThan(0)); - }); - }); + group( + 'onNewState', + () { + late GameState state; - group('SmallDashNestBumper', () { - test('has points', () { - final dashNestBumper = SmallDashNestBumper(id: ''); - expect(dashNestBumper.points, greaterThan(0)); - }); + setUp(() { + state = MockGameState(); + }); + + test( + 'activates the bumper when id in activatedDashNests', + () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => state.activatedDashNests).thenReturn({id}); + controller.onNewState(state); + + verify(() => dashNestBumper.activate()).called(1); + }, + ); + + test( + 'deactivates the bumper when id not in activatedDashNests', + () { + final controller = DashNestBumperController( + dashNestBumper, + id: '', + ); + + when(() => state.activatedDashNests).thenReturn({}); + controller.onNewState(state); + + verify(() => dashNestBumper.deactivate()).called(1); + }, + ); + }, + ); }); }