From e5c37089522d45dc25f43f2336ac0062bd7c75c9 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Fri, 1 Apr 2022 09:35:03 +0100 Subject: [PATCH] feat: implemented DashBumper assets (#119) * chore: rebase * chore: rebase * chore: rebase * docs: removed extra paragraph * fix: removed checks * refactor: removed ephemeral state * refactor: uncommented code * feat: automatically sized Sprites * docs: included TODO comment * refactor: corrected typo * docs: updated doc comment * feat: adjusted tests * chore: rebase * feat: included mustCallSuper * feat: implemented FlutterForest controllers * feat: improved tests * fix: analyzer * refactor: removed unneccessary mock * feat: include DashNestBumper tests * docs: improved doc comment * refactor: fixed test name grammar * fix: fixed test --- lib/flame/component_controller.dart | 2 + lib/game/components/flutter_forest.dart | 195 ++++++------ lib/game/game_assets.dart | 6 + .../assets/images/dash_bumper/a/active.png | Bin 0 -> 10739 bytes .../assets/images/dash_bumper/a/inactive.png | Bin 0 -> 10452 bytes .../assets/images/dash_bumper/b/active.png | Bin 0 -> 10553 bytes .../assets/images/dash_bumper/b/inactive.png | Bin 0 -> 10233 bytes .../assets/images/dash_bumper/main/active.png | Bin 0 -> 8795 bytes .../images/dash_bumper/main/inactive.png | Bin 0 -> 8644 bytes .../lib/gen/assets.gen.dart | 47 +++ .../lib/src/components/components.dart | 1 + .../lib/src/components/dash_nest_bumper.dart | 142 +++++++++ packages/pinball_components/pubspec.yaml | 3 + .../src/components/dash_nest_bumper_test.dart | 116 +++++++ .../test/src/components/fire_effect_test.dart | 5 +- test/game/components/flutter_forest_test.dart | 285 ++++++++++++++---- 16 files changed, 648 insertions(+), 154 deletions(-) create mode 100644 packages/pinball_components/assets/images/dash_bumper/a/active.png create mode 100644 packages/pinball_components/assets/images/dash_bumper/a/inactive.png create mode 100644 packages/pinball_components/assets/images/dash_bumper/b/active.png create mode 100644 packages/pinball_components/assets/images/dash_bumper/b/inactive.png create mode 100644 packages/pinball_components/assets/images/dash_bumper/main/active.png create mode 100644 packages/pinball_components/assets/images/dash_bumper/main/inactive.png create mode 100644 packages/pinball_components/lib/src/components/dash_nest_bumper.dart create mode 100644 packages/pinball_components/test/src/components/dash_nest_bumper_test.dart 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 0000000000000000000000000000000000000000..feeee11f4e87bd8e9e622fbbc8d9752ef23a3d5a GIT binary patch literal 10739 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..58ab8c56638c956ed04c25d2a7e343ec1871ed4f GIT binary patch literal 10452 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

-*0KAU4sD|H}ud*g&QycmS@M|*??-74aV|u zBV_b9!b%!a_ct^&S(0XPpGGm%dA_&(PDy|L{1eunqt0(yno;~ysFS@u@m8aK>5XC5 zOQQ}BlX?VSt@v|mYHmI)ZXH{ zV@T0&veLgyTv04{tzYTP);C&qGB-}l!JG@2F6V&q1njJ=JZKMNdOEQncEGHF z9*#{lxgGSNGW1z4*MCpy^1D`;Lva*v{7C!$;F9ereaP1h=~c%yR=J%WcRQmyN3H7~ z=@sACmHP^nW5Z33fk2aOs_=ChsSKoU&fK}dZ8S17|CHm~bHceE1iMCvT;ogJPKC?Gu%U zqkxywjv_#mh7%Gjq3N%v6U-_JlyPb5>9M`yASagzVUgBLL!1=yk@(oSov5|U9eCSh z`^-c$oljpD|Ca>vyv?Ql_u&01Y8iECM{`|tK%}6Netrs1Hr3S9vhu{MP7i3|kIV#d z+GHe_=`m~B%L|>6;D!|9#L@FQgo@msu9mM@#LZU?(`O51!IeM=HHgWKb3K2M}&A8`OR)kM#^``p=|MVwI$G9LZo68 z+dE+{>F;r!J?Sa6``+T@H{jGcUoZXg&|~;cg=N(>QGz3kL8;)*=PNuXZMPFp+g3h@KY!V3$SzrShOdLfyb>j z1GVWmGyDgLCDNWGB%a21&2R*g{5c|qJU7qB?j13wGO?#Jorxti>0f~pu}ig%`}v~_ z7(98?53YZbJDGw{wt)L&_H?%$Zp`{knGoG+v42-pRkgF&4~@H7W#NHs+Y>%%joxoP|4#^vP*`}*|KT`L-4l$HGeQ6l-o#QnX&&?#I=i6evW`{NevY7!yMC3^3#`0AiUuJbxlo8r+h6v!Ew5PBN1cO z)P6GokneHuubN;0Ts4f&W7pwGR)h;4wD<*lclqL+=3vas%vgXvhQEFlpZCWjI-%l$ zQkPz69|;eXsTNt%MfSs5-~kI=uz4iNrNNoVS~vt+0)!|6exOb1_Ro)$sL!1y_0F%| Y+i?ikEAS_SXCZMEWK^ZgB+bJ92f^rSfdBvi literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eddc7693c2467d3054396d2c869df7e8b818f21d GIT binary patch literal 10233 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 Qp5?=llU9-{lr#$XAA1b|^b#SuXwk_;?-6B` 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 B^g#ds literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e6f15b382f69bd70a8d4e4aef6dcecfa17c111da GIT binary patch literal 8644 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 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); + }, + ); + }, + ); }); }