From d798fdf9fe6d61bbde33789f485db1fcf5992559 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 6 Apr 2022 15:12:00 +0200 Subject: [PATCH 1/3] feat: plunger asset (#145) * feat: added plunger asset and move * feat: fixed plunger assets and position * fix: set limits to plunger compression correctly * chore: unused import * fix: placed plunger correctly * test: refactor test game * refactor: changed spawnBall initialPosition * chore: plunger golden test --- assets/images/components/plunger.png | Bin 0 -> 11655 bytes lib/game/components/plunger.dart | 46 +++++++++++++++++++++---- lib/game/pinball_game.dart | 14 ++------ lib/gen/assets.gen.dart | 4 +++ test/game/components/plunger_test.dart | 23 ++++++++++++- 5 files changed, 67 insertions(+), 20 deletions(-) create mode 100644 assets/images/components/plunger.png diff --git a/assets/images/components/plunger.png b/assets/images/components/plunger.png new file mode 100644 index 0000000000000000000000000000000000000000..f3cbdf0f004f617615e882c44d3522b3a2d88bd9 GIT binary patch literal 11655 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 G?Ee5&CbES9 literal 0 HcmV?d00001 diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 9b7eec39..b319af80 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,8 +1,8 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball/gen/assets.gen.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@template plunger} /// [Plunger] serves as a spring, that shoots the ball on the right side of the @@ -14,10 +14,9 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { /// {@macro plunger} Plunger({ required this.compressionDistance, - }) : super( - // TODO(allisonryan0002): remove paint after asset is added. - paint: Paint()..color = const Color.fromARGB(255, 241, 8, 8), - ); + // TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities + // are fixed. + }) : super(priority: 0); /// Distance the plunger can lower. final double compressionDistance; @@ -88,13 +87,36 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { plunger: this, anchor: anchor, ); - world.createJoint(PrismaticJoint(jointDef)); + + world.createJoint( + PrismaticJoint(jointDef)..setLimits(-compressionDistance, 0), + ); } @override Future onLoad() async { await super.onLoad(); await _anchorToJoint(); + + renderBody = false; + + await _loadSprite(); + } + + Future _loadSprite() async { + final sprite = await gameRef.loadSprite( + Assets.images.components.plunger.path, + ); + + await add( + SpriteComponent( + sprite: sprite, + size: Vector2(5.5, 40), + anchor: Anchor.center, + position: Vector2(2, 19), + angle: -0.033, + ), + ); } } @@ -111,6 +133,16 @@ class PlungerAnchor extends JointAnchor { plunger.body.position.y - plunger.compressionDistance, ); } + + @override + Body createBody() { + final shape = CircleShape()..radius = 0.5; + final fixtureDef = FixtureDef(shape); + final bodyDef = BodyDef() + ..position = initialPosition + ..type = BodyType.static; + return world.createBody(bodyDef)..createFixture(fixtureDef); + } } /// {@template plunger_anchor_prismatic_joint_def} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 3c99fbca..a6eb0884 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flame/components.dart'; -import 'package:flame/extensions.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; @@ -72,8 +71,7 @@ class PinballGame extends Forge2DGame Future _addPlunger() async { final plunger = Plunger(compressionDistance: 29) - ..initialPosition = - BoardDimensions.bounds.center.toVector2() + Vector2(41.5, -49); + ..initialPosition = Vector2(38, -19); await add(plunger); } @@ -90,17 +88,9 @@ class PinballGame extends Forge2DGame Future spawnBall() async { // TODO(alestiago): Remove once this logic is moved to controller. - var plunger = firstChild(); - if (plunger == null) { - await add(plunger = Plunger(compressionDistance: 1)); - } - final ball = ControlledBall.launch( theme: theme, - )..initialPosition = Vector2( - plunger.body.position.x, - plunger.body.position.y + Ball.size.y, - ); + )..initialPosition = Vector2(38, -19 + Ball.size.y); await add(ball); } } diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 370d8fcf..5c2a87c2 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -17,6 +17,10 @@ class $AssetsImagesComponentsGen { AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); + + /// File path: assets/images/components/plunger.png + AssetGenImage get plunger => + const AssetGenImage('assets/images/components/plunger.png'); } class Assets { diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 2a49ae2d..65789ae0 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -12,11 +12,32 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(Forge2DGame.new); + final flameTester = FlameTester(TestGame.new); group('Plunger', () { const compressionDistance = 0.0; + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.add( + Plunger( + compressionDistance: compressionDistance, + ), + ); + await game.ready(); + game.camera.followVector2(Vector2.zero()); + game.camera.zoom = 4.1; + }, + // TODO(ruimiguel): enable test when workflows are fixed. + // verify: (game, tester) async { + // await expectLater( + // find.byGame(), + // matchesGoldenFile('golden/plunger.png'), + // ); + // }, + ); + flameTester.test( 'loads correctly', (game) async { From 254c38d2a4b0e7d215c6ec1943903e6cad0a5c71 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 6 Apr 2022 18:35:11 +0200 Subject: [PATCH 2/3] feat: add sparky bumpers (#150) * feat: added sparky bumpers * test: tests for sparky bumpers * feat: sandbox for sparky bumpers * chore: unused imports * refactor: removed Bumper and added TODO for future refactor * fix: fixed size and tracing * fix: fix tracing * fix: final ellipse sizes * refactor: different sized bumpers * Update packages/pinball_components/lib/src/components/sparky_bumper.dart Co-authored-by: Allison Ryan Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- .../assets/images/sparky_bumper/a/active.png | Bin 0 -> 3033 bytes .../images/sparky_bumper/a/inactive.png | Bin 0 -> 2896 bytes .../assets/images/sparky_bumper/b/active.png | Bin 0 -> 2983 bytes .../images/sparky_bumper/b/inactive.png | Bin 0 -> 2882 bytes .../assets/images/sparky_bumper/c/active.png | Bin 0 -> 3278 bytes .../images/sparky_bumper/c/inactive.png | Bin 0 -> 3138 bytes .../lib/gen/assets.gen.dart | 46 +++++++ .../lib/src/components/components.dart | 1 + .../lib/src/components/sparky_bumper.dart | 125 ++++++++++++++++++ packages/pinball_components/pubspec.yaml | 3 + .../pinball_components/sandbox/lib/main.dart | 1 + .../sparky_bumper/sparky_bumper_game.dart | 47 +++++++ .../lib/stories/sparky_bumper/stories.dart | 17 +++ .../sandbox/lib/stories/stories.dart | 1 + .../src/components/sparky_bumper_test.dart | 74 +++++++++++ 15 files changed, 315 insertions(+) create mode 100644 packages/pinball_components/assets/images/sparky_bumper/a/active.png create mode 100644 packages/pinball_components/assets/images/sparky_bumper/a/inactive.png create mode 100644 packages/pinball_components/assets/images/sparky_bumper/b/active.png create mode 100644 packages/pinball_components/assets/images/sparky_bumper/b/inactive.png create mode 100644 packages/pinball_components/assets/images/sparky_bumper/c/active.png create mode 100644 packages/pinball_components/assets/images/sparky_bumper/c/inactive.png create mode 100644 packages/pinball_components/lib/src/components/sparky_bumper.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart create mode 100644 packages/pinball_components/test/src/components/sparky_bumper_test.dart diff --git a/packages/pinball_components/assets/images/sparky_bumper/a/active.png b/packages/pinball_components/assets/images/sparky_bumper/a/active.png new file mode 100644 index 0000000000000000000000000000000000000000..6e84d8efcd84fdd00d08fb7935b91176106c2940 GIT binary patch literal 3033 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 anA|#;+|oH9Y5NGS$Pg0oq+Pj<5BopX4_~hU literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/sparky_bumper/b/active.png b/packages/pinball_components/assets/images/sparky_bumper/b/active.png new file mode 100644 index 0000000000000000000000000000000000000000..02371ce188a5c1b795349d7150bffa59e9b9f2f9 GIT binary patch literal 2983 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 cCTQSoO>o>6gY2)kv?9p3hNa0e@_5^^8f$< literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/sparky_bumper/b/inactive.png b/packages/pinball_components/assets/images/sparky_bumper/b/inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..20b4f092735ec9294afd8aec57ff552bc64cbe63 GIT binary patch literal 2882 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/sparky_bumper/c/active.png b/packages/pinball_components/assets/images/sparky_bumper/c/active.png new file mode 100644 index 0000000000000000000000000000000000000000..857483757ab9e7058fd30e275543ac75b34320ea GIT binary patch literal 3278 zcmWlcc{r3`8^_1K%*0qjVn_&OEKwqits%@HTe2lfmJG(eXUUcbC2Pc_2^paX6V;Tl zljUdMvnBh^d*07=opb(quKPOYKF{~Qzb64>q<4l@fE5CPoI&5xHUX;^7}uE@!R+aq z)ejaH?^{;B5D1j(|APkdDwhuefkmRVHO*YdS93xzW)}IB=H1bdLtIYNShu~Fa>uz? zw{DFX*LHcib9gk$`(-&s!L;n#yA`|rzBl5T9hS=i?$kC9^Gnsk83Ucp)!FmMnXN-q5%%1}3s);4%b&cjg54pv!n*PhM<<#+u8P^nY}K3H6GaxyGMw-{Sy_RdK@ zJ6T&a?epi)*9*E9e6Ipu(TblM!^ZTB4Hvin3y7FohLF>k{CK2%#w+YiUKK&kG~dD=X~MtjGer9^wRzN)4c@@9epJ^AAayod~;#;AfCUx9~eILOmjYk_)*#Likf0ew> zY;d%Uwbz^p+AUsnBfBfZ)r6|5DlDOIasId~j6)=5c41+m`Cy4ynSTv8tqo4zWxN@i z4)#u&S^rp9SEqiw)fYiDDKmo#v3#^HuPp%(i9~4-arRIK%dHdNIyFRkK;=#P?zn$N zJV;O6p~kwrAz*EM6>J`=#&weJSY^2~mkfwd>L7CtUHESDm_R^_00rDL~0%=)+V6ZO`)d!R5QP9xU{< zH2Wfb{r&xB1_lPydve+B0MC(HPiQPkWM3ph4Bgw^-Hp-G(rOLxc_&E0u~wcK?n^Cl2n_cS7ataE4etkoaj#@mFawdWn@&fb$y;ZNilVljs?KLCkqt@5g|GF<< znEequxb&N7ahLdh!0+-Z2MXiXX{u)Hd0f`xD)Xr%8)T`jwzk&i;ZuVJYr*dwen`g!O9@e}HnNE-h|4EX`*$JVI(IA>d?>6AU8z?My?Ymc+SX9grMyFLwR<_OzNUfr! z(9IfaZe9z1DXAt|!8oe#>BH=oOB8MqjV4Wd?_L$VI`W1XC%eu*VMkqLlj?NVWVE zr1J{-Y`h$ClKc`n7pOXw^4+nDh@4*67860Eac|k--xZ&Vih6HI&X(bZwu6w|+y=$P z#S29}u3G|p>!07KT;Nz)qd}c)#ZLJ^4sU9kWguT!u=!$f(?EK59c%Mkke4|*n_9%p z1_A+5u#CQ*?YG5qzL}vjBGxUOgMPUf+k%uXspFT29jeFUj#*jKiWsUtP3Jh!@e40G`oLrkiu2M%mr67saA5*XJNY%l_10Lxa;jpcqzd*sY z`^lT<93UE!+^qY@;l|6x5d%ow{-az<%9llsZi2dT(ERj8jX2&w%)7_w^fvkW**YWQ zfr;H3_{Ml*P!5!Bafe~Z*7ZO$RnB06K_-B-EuMzGlP+aOGNQ_`B zQ9oc2Dz7vaR1NfAJNM|zdK@Uq5vDKfmg%*z+@4A!&w5<@RYx{v^x|AoT7(i2gX(m1 zB^($EJKb5&$Z?3%oQw=N782?;S#2DdcS?UWA7<|ZBZ-L?4NOx>DqG4&eA0G%{JgSd zPgW+Hn~yt`CqjLLgM$xb@nHt3e5sU~#>PgMz6&eWd5gy{m!3)r% zmIo{CP7jtVPgQT+IAn4$!Ye2cQtv0Ma%ZW;ebtLbq%(Ib77jp?ZfgYbd~o8ozr>i# zOa8#z?Mj0(DRfnNu#1xa?by@LIk|DsKA4v89K9}+X(#Rqv)FTp(@*YLv5uC!?4+h> zsdlr0*MZ+B?A;7gZez^~i;HI_cUDL0cDJ{;#}+jYN5Y*H4f`?ym5n?9xp%2Lt zfXApxp3&gY(9m9+bmftjp`l?IlCM7*mPaUM>O?6xX!XoNkwL57@nIMV$HGcjVsz+8 z1iDo67Pn^f9dhM>qizATp^7;_`eOH8Va-MMclMVgpYg{foB@O%ty6^8M(Q3-0Q=A% z5F|WzH%JA={HwFG(+gD7(8ZD$w(lw`!edI^PDrhqb^^ljA&#)3FwWpzMG~!-Q*78| zF^%`SB%d6MC>24X*H1@q(k3_~-~w3l*bll)%>$8qT@?h2oQGvdiAwbV literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/sparky_bumper/c/inactive.png b/packages/pinball_components/assets/images/sparky_bumper/c/inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..b5b3584d466d2f870530e496fb687c2e49d2038f GIT binary patch literal 3138 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 literal 0 HcmV?d00001 diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 154930bc..de59219e 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -30,6 +30,8 @@ class $AssetsImagesGen { $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); + $AssetsImagesSparkyBumperGen get sparkyBumper => + const $AssetsImagesSparkyBumperGen(); } class $AssetsImagesBaseboardGen { @@ -142,6 +144,14 @@ class $AssetsImagesSpaceshipGen { const AssetGenImage('assets/images/spaceship/saucer.png'); } +class $AssetsImagesSparkyBumperGen { + const $AssetsImagesSparkyBumperGen(); + + $AssetsImagesSparkyBumperAGen get a => const $AssetsImagesSparkyBumperAGen(); + $AssetsImagesSparkyBumperBGen get b => const $AssetsImagesSparkyBumperBGen(); + $AssetsImagesSparkyBumperCGen get c => const $AssetsImagesSparkyBumperCGen(); +} + class $AssetsImagesDashBumperAGen { const $AssetsImagesDashBumperAGen(); @@ -206,6 +216,42 @@ class $AssetsImagesSpaceshipRampGen { 'assets/images/spaceship/ramp/railing-foreground.png'); } +class $AssetsImagesSparkyBumperAGen { + const $AssetsImagesSparkyBumperAGen(); + + /// File path: assets/images/sparky_bumper/a/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/sparky_bumper/a/active.png'); + + /// File path: assets/images/sparky_bumper/a/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/sparky_bumper/a/inactive.png'); +} + +class $AssetsImagesSparkyBumperBGen { + const $AssetsImagesSparkyBumperBGen(); + + /// File path: assets/images/sparky_bumper/b/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/sparky_bumper/b/active.png'); + + /// File path: assets/images/sparky_bumper/b/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/sparky_bumper/b/inactive.png'); +} + +class $AssetsImagesSparkyBumperCGen { + const $AssetsImagesSparkyBumperCGen(); + + /// File path: assets/images/sparky_bumper/c/active.png + AssetGenImage get active => + const AssetGenImage('assets/images/sparky_bumper/c/active.png'); + + /// File path: assets/images/sparky_bumper/c/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/sparky_bumper/c/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 bf578ea7..6b0c2ef5 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -19,3 +19,4 @@ export 'shapes/shapes.dart'; export 'spaceship.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; +export 'sparky_bumper.dart'; diff --git a/packages/pinball_components/lib/src/components/sparky_bumper.dart b/packages/pinball_components/lib/src/components/sparky_bumper.dart new file mode 100644 index 00000000..e6a5f237 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper.dart @@ -0,0 +1,125 @@ +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 sparky_bumper} +/// Bumper for Sparky area. +/// {@endtemplate} +// TODO(ruimiguel): refactor later to unify with DashBumpers. +class SparkyBumper extends BodyComponent with InitialPosition { + /// {@macro sparky_bumper} + SparkyBumper._({ + required double majorRadius, + required double minorRadius, + required String activeAssetPath, + required String inactiveAssetPath, + required SpriteComponent spriteComponent, + }) : _majorRadius = majorRadius, + _minorRadius = minorRadius, + _activeAssetPath = activeAssetPath, + _inactiveAssetPath = inactiveAssetPath, + _spriteComponent = spriteComponent; + + /// {@macro sparky_bumper} + SparkyBumper.a() + : this._( + majorRadius: 2.9, + minorRadius: 2.1, + activeAssetPath: Assets.images.sparkyBumper.a.active.keyName, + inactiveAssetPath: Assets.images.sparkyBumper.a.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0, -0.25), + ), + ); + + /// {@macro sparky_bumper} + SparkyBumper.b() + : this._( + majorRadius: 2.85, + minorRadius: 2, + activeAssetPath: Assets.images.sparkyBumper.b.active.keyName, + inactiveAssetPath: Assets.images.sparkyBumper.b.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0, -0.35), + ), + ); + + /// {@macro sparky_bumper} + SparkyBumper.c() + : this._( + majorRadius: 3, + minorRadius: 2.2, + activeAssetPath: Assets.images.sparkyBumper.c.active.keyName, + inactiveAssetPath: Assets.images.sparkyBumper.c.inactive.keyName, + spriteComponent: SpriteComponent( + anchor: Anchor.center, + position: Vector2(0, -0.4), + ), + ); + + final double _majorRadius; + final double _minorRadius; + final String _activeAssetPath; + late final Sprite _activeSprite; + final String _inactiveAssetPath; + late final Sprite _inactiveSprite; + final SpriteComponent _spriteComponent; + + @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); + } + + @override + Body createBody() { + renderBody = false; + + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: _majorRadius, + minorRadius: _minorRadius, + )..rotate(math.pi / 1.9); + final fixtureDef = FixtureDef(shape) + ..friction = 0 + ..restitution = 4; + + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + + 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; + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index c7302d0d..312e01f3 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -39,6 +39,9 @@ flutter: - assets/images/spaceship/ramp/ - assets/images/chrome_dino/ - assets/images/kicker/ + - assets/images/sparky_bumper/a/ + - assets/images/sparky_bumper/b/ + - assets/images/sparky_bumper/c/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 1801fa52..88b86da6 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -21,5 +21,6 @@ void main() { addChromeDinoStories(dashbook); addDashNestBumperStories(dashbook); addKickerStories(dashbook); + addSparkyBumperStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart new file mode 100644 index 00000000..a0ad661a --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SparkyBumperGame extends BasicBallGame { + SparkyBumperGame({ + required this.trace, + }) : super(color: const Color(0xFF0000FF)); + + static const info = ''' + Shows how a SparkyBumper is rendered. + + Activate the "trace" parameter to overlay the body. +'''; + + final bool trace; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + final sparkyBumperA = SparkyBumper.a() + ..initialPosition = Vector2(center.x - 20, center.y - 20) + ..priority = 1; + final sparkyBumperB = SparkyBumper.b() + ..initialPosition = Vector2(center.x - 10, center.y + 10) + ..priority = 1; + final sparkyBumperC = SparkyBumper.c() + ..initialPosition = Vector2(center.x + 20, center.y) + ..priority = 1; + await addAll([ + sparkyBumperA, + sparkyBumperB, + sparkyBumperC, + ]); + + if (trace) { + sparkyBumperA.trace(); + sparkyBumperB.trace(); + sparkyBumperC.trace(); + } + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart new file mode 100644 index 00000000..d0933b67 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart @@ -0,0 +1,17 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/sparky_bumper/sparky_bumper_game.dart'; + +void addSparkyBumperStories(Dashbook dashbook) { + dashbook.storiesOf('Sparky Bumpers').add( + 'Basic', + (context) => GameWidget( + game: SparkyBumperGame( + trace: context.boolProperty('Trace', true), + ), + ), + codeLink: buildSourceLink('sparky_bumper/basic.dart'), + info: SparkyBumperGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 108cca05..746d83d6 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -6,3 +6,4 @@ export 'effects/stories.dart'; export 'flipper/stories.dart'; export 'layer/stories.dart'; export 'spaceship/stories.dart'; +export 'sparky_bumper/stories.dart'; diff --git a/packages/pinball_components/test/src/components/sparky_bumper_test.dart b/packages/pinball_components/test/src/components/sparky_bumper_test.dart new file mode 100644 index 00000000..470c254b --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper_test.dart @@ -0,0 +1,74 @@ +// 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('SparkyBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final bumper = SparkyBumper.a(); + await game.ensureAdd(bumper); + + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final bumper = SparkyBumper.b(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"c" loads correctly', (game) async { + final bumper = SparkyBumper.c(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('activate returns normally', (game) async { + final bumper = SparkyBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.activate, returnsNormally); + }); + + flameTester.test('deactivate returns normally', (game) async { + final bumper = SparkyBumper.a(); + await game.ensureAdd(bumper); + + expect(bumper.deactivate, returnsNormally); + }); + + flameTester.test('changes sprite', (game) async { + final bumper = SparkyBumper.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)), + ); + }); + }); +} From 655007b2d28c7219cb495d1cc7e08c18f41dd11d Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Wed, 6 Apr 2022 19:06:14 +0100 Subject: [PATCH 3/3] feat: improved extra ball logic (#151) --- lib/flame/component_controller.dart | 2 +- lib/game/bloc/game_bloc.dart | 14 +- lib/game/bloc/game_state.dart | 5 +- lib/game/components/controlled_ball.dart | 60 ++----- lib/game/components/wall.dart | 10 +- lib/game/pinball_game.dart | 106 ++++++++++--- test/game/bloc/game_bloc_test.dart | 23 +-- .../game/components/controlled_ball_test.dart | 146 +----------------- test/game/components/flutter_forest_test.dart | 16 +- test/game/components/wall_test.dart | 90 +++++++---- test/game/pinball_game_test.dart | 143 ++++++++++++++++- test/helpers/extensions.dart | 2 + test/helpers/forge2d.dart | 13 ++ test/helpers/helpers.dart | 1 + test/helpers/mocks.dart | 2 + 15 files changed, 339 insertions(+), 294 deletions(-) create mode 100644 test/helpers/forge2d.dart diff --git a/lib/flame/component_controller.dart b/lib/flame/component_controller.dart index 1d6e0173..b9568348 100644 --- a/lib/flame/component_controller.dart +++ b/lib/flame/component_controller.dart @@ -33,7 +33,7 @@ abstract class ComponentController extends Component { /// Mixin that attaches a single [ComponentController] to a [Component]. mixin Controls on Component { /// The [ComponentController] attached to this [Component]. - late final T controller; + late T controller; @override @mustCallSuper diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index c02417a7..7c1b4f44 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -19,9 +19,7 @@ class GameBloc extends Bloc { static const bonusWordScore = 10000; void _onBallLost(BallLost event, Emitter emit) { - if (state.balls > 0) { - emit(state.copyWith(balls: state.balls - 1)); - } + emit(state.copyWith(balls: state.balls - 1)); } void _onScored(Scored event, Emitter emit) { @@ -36,7 +34,8 @@ class GameBloc extends Bloc { event.letterIndex, ]; - if (newBonusLetters.length == bonusWord.length) { + final achievedBonus = newBonusLetters.length == bonusWord.length; + if (achievedBonus) { emit( state.copyWith( activatedBonusLetters: [], @@ -55,15 +54,16 @@ class GameBloc extends Bloc { } void _onDashNestActivated(DashNestActivated event, Emitter emit) { - const nestsRequiredForBonus = 3; - final newNests = { ...state.activatedDashNests, event.nestId, }; - if (newNests.length == nestsRequiredForBonus) { + + final achievedBonus = newNests.length == 3; + if (achievedBonus) { emit( state.copyWith( + balls: state.balls + 1, activatedDashNests: {}, bonusHistory: [ ...state.bonusHistory, diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index d08ba04b..bbaa4cd8 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -5,11 +5,10 @@ part of 'game_bloc.dart'; /// Defines bonuses that a player can gain during a PinballGame. enum GameBonus { /// Bonus achieved when the user activate all of the bonus - /// letters on the board, forming the bonus word + /// letters on the board, forming the bonus word. word, - /// Bonus achieved when the user activates all of the Dash - /// nests on the board, adding a new ball to the board. + /// Bonus achieved when the user activates all dash nest bumpers. dashNest, } diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 1981e39c..cef076d8 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,5 +1,4 @@ import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flutter/material.dart'; import 'package:pinball/flame/flame.dart'; @@ -18,7 +17,7 @@ class ControlledBall extends Ball with Controls { ControlledBall.launch({ required PinballTheme theme, }) : super(baseColor: theme.characterTheme.ballColor) { - controller = LaunchedBallController(this); + controller = BallController(this); } /// {@template bonus_ball} @@ -29,74 +28,43 @@ class ControlledBall extends Ball with Controls { ControlledBall.bonus({ required PinballTheme theme, }) : super(baseColor: theme.characterTheme.ballColor) { - controller = BonusBallController(this); + controller = BallController(this); } /// [Ball] used in [DebugPinballGame]. ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { - controller = BonusBallController(this); + controller = DebugBallController(this); } } /// {@template ball_controller} /// Controller attached to a [Ball] that handles its game related logic. /// {@endtemplate} -abstract class BallController extends ComponentController { +class BallController extends ComponentController + with HasGameRef { /// {@macro ball_controller} BallController(Ball ball) : super(ball); /// Removes the [Ball] from a [PinballGame]. /// - /// {@template ball_controller_lost} /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into /// a [BottomWall]. - /// {@endtemplate} - void lost(); -} - -/// {@template bonus_ball_controller} -/// {@macro ball_controller} -/// -/// A [BonusBallController] doesn't change the [GameState.balls] count. -/// {@endtemplate} -class BonusBallController extends BallController { - /// {@macro bonus_ball_controller} - BonusBallController(Ball component) : super(component); - - @override void lost() { component.shouldRemove = true; } -} - -/// {@template launched_ball_controller} -/// {@macro ball_controller} -/// -/// A [LaunchedBallController] changes the [GameState.balls] count. -/// {@endtemplate} -class LaunchedBallController extends BallController - with HasGameRef, BlocComponent { - /// {@macro launched_ball_controller} - LaunchedBallController(Ball ball) : super(ball); @override - bool listenWhen(GameState? previousState, GameState newState) { - return (previousState?.balls ?? 0) > newState.balls; + void onRemove() { + super.onRemove(); + gameRef.read().add(const BallLost()); } +} - @override - void onNewState(GameState state) { - super.onNewState(state); - component.shouldRemove = true; - if (state.balls > 0) gameRef.spawnBall(); - } +/// {@macro ball_controller} +class DebugBallController extends BallController { + /// {@macro ball_controller} + DebugBallController(Ball component) : super(component); - /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if - /// any are left. - /// - /// {@macro ball_controller_lost} @override - void lost() { - gameRef.read().add(const BallLost()); - } + void onRemove() {} } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 030edc50..ba8af5e7 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -71,12 +71,12 @@ class BottomWall extends Wall { } /// {@template bottom_wall_ball_contact_callback} -/// Listens when a [Ball] falls into a [BottomWall]. +/// Listens when a [ControlledBall] falls into a [BottomWall]. /// {@endtemplate} -class BottomWallBallContactCallback extends ContactCallback { +class BottomWallBallContactCallback + extends ContactCallback { @override - void begin(Ball ball, BottomWall wall, Contact contact) { - // TODO(alestiago): replace with .firstChild when available. - ball.children.whereType().first.lost(); + void begin(ControlledBall ball, BottomWall wall, Contact contact) { + ball.controller.lost(); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index a6eb0884..7a0e6823 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -5,6 +5,7 @@ import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/flame/flame.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_audio/pinball_audio.dart'; @@ -12,29 +13,38 @@ import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets; class PinballGame extends Forge2DGame - with FlameBloc, HasKeyboardHandlerComponents { - PinballGame({required this.theme, required this.audio}) { + with + FlameBloc, + HasKeyboardHandlerComponents, + Controls<_GameBallsController> { + PinballGame({ + required this.theme, + required this.audio, + }) { images.prefix = ''; + controller = _GameBallsController(this); } final PinballTheme theme; final PinballAudio audio; - @override - void onAttach() { - super.onAttach(); - spawnBall(); - } - @override Future onLoad() async { _addContactCallbacks(); + // Fix camera on the center of the board. + camera + ..followVector2(Vector2(0, -7.8)) + ..zoom = size.y / 16; await _addGameBoundaries(); unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(LaunchRamp())); - unawaited(_addPlunger()); + + final plunger = Plunger(compressionDistance: 29) + ..initialPosition = Vector2(38, -19); + await add(plunger); + unawaited(add(Board())); unawaited(addFromBlueprint(DinoWalls())); unawaited(_addBonusWord()); @@ -52,10 +62,8 @@ class PinballGame extends Forge2DGame ), ); - // Fix camera on the center of the board. - camera - ..followVector2(Vector2(0, -7.8)) - ..zoom = size.y / 16; + controller.attachTo(plunger); + await super.onLoad(); } void _addContactCallbacks() { @@ -69,12 +77,6 @@ class PinballGame extends Forge2DGame createBoundaries(this).forEach(add); } - Future _addPlunger() async { - final plunger = Plunger(compressionDistance: 29) - ..initialPosition = Vector2(38, -19); - await add(plunger); - } - Future _addBonusWord() async { await add( BonusWord( @@ -85,13 +87,49 @@ class PinballGame extends Forge2DGame ), ); } +} - Future spawnBall() async { - // TODO(alestiago): Remove once this logic is moved to controller. +class _GameBallsController extends ComponentController + with BlocComponent, HasGameRef { + _GameBallsController(PinballGame game) : super(game); + + late final Plunger _plunger; + + @override + bool listenWhen(GameState? previousState, GameState newState) { + final noBallsLeft = component.descendants().whereType().isEmpty; + final canBallRespawn = newState.balls > 0; + + return noBallsLeft && canBallRespawn; + } + + @override + void onNewState(GameState state) { + super.onNewState(state); + _spawnBall(); + } + + @override + Future onLoad() async { + await super.onLoad(); + _spawnBall(); + } + + void _spawnBall() { final ball = ControlledBall.launch( - theme: theme, - )..initialPosition = Vector2(38, -19 + Ball.size.y); - await add(ball); + theme: gameRef.theme, + )..initialPosition = Vector2( + _plunger.body.position.x, + _plunger.body.position.y + Ball.size.y, + ); + component.add(ball); + } + + /// Attaches the controller to the plunger. + // TODO(alestiago): Remove this method and use onLoad instead. + // ignore: use_setters_to_change_properties + void attachTo(Plunger plunger) { + _plunger = plunger; } } @@ -102,7 +140,9 @@ class DebugPinballGame extends PinballGame with TapDetector { }) : super( theme: theme, audio: audio, - ); + ) { + controller = _DebugGameBallsController(this); + } @override Future onLoad() async { @@ -134,3 +174,19 @@ class DebugPinballGame extends PinballGame with TapDetector { ); } } + +class _DebugGameBallsController extends _GameBallsController { + _DebugGameBallsController(PinballGame game) : super(game); + + @override + bool listenWhen(GameState? previousState, GameState newState) { + final noBallsLeft = component + .descendants() + .whereType() + .where((ball) => ball.controller is! DebugBallController) + .isEmpty; + final canBallRespawn = newState.balls > 0; + + return noBallsLeft && canBallRespawn; + } +} diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index f4b79001..8ec53106 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -12,13 +12,10 @@ void main() { group('LostBall', () { blocTest( - "doesn't decrease ball " - 'when no balls left', + 'decreases number of balls', build: GameBloc.new, act: (bloc) { - for (var i = 0; i <= bloc.state.balls; i++) { - bloc.add(const BallLost()); - } + bloc.add(const BallLost()); }, expect: () => [ const GameState( @@ -28,20 +25,6 @@ void main() { activatedDashNests: {}, bonusHistory: [], ), - const GameState( - score: 0, - balls: 1, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ), - const GameState( - score: 0, - balls: 0, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ), ], ); }); @@ -230,7 +213,7 @@ void main() { ), GameState( score: 0, - balls: 3, + balls: 4, activatedBonusLetters: [], activatedDashNests: {}, bonusHistory: [GameBonus.dashNest], diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index 05056484..53847b3c 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -13,42 +13,12 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballGameTest.new); - group('BonusBallController', () { - late Ball ball; - - setUp(() { - ball = Ball(baseColor: const Color(0xFF00FFFF)); - }); - - test('can be instantiated', () { - expect( - BonusBallController(ball), - isA(), - ); - }); - - flameTester.test( - 'lost removes ball', - (game) async { - await game.add(ball); - final controller = BonusBallController(ball); - await ball.ensureAdd(controller); - - controller.lost(); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - }); - - group('LaunchedBallController', () { + group('BallController', () { test('can be instantiated', () { expect( - LaunchedBallController(MockBall()), - isA(), + BallController(MockBall()), + isA(), ); }); @@ -74,7 +44,7 @@ void main() { flameBlocTester.testGameWidget( 'lost adds BallLost to GameBloc', setUp: (game, tester) async { - final controller = LaunchedBallController(ball); + final controller = BallController(ball); await ball.add(controller); await game.ensureAdd(ball); @@ -84,114 +54,6 @@ void main() { verify(() => gameBloc.add(const BallLost())).called(1); }, ); - - group('listenWhen', () { - flameBlocTester.testGameWidget( - 'listens when a ball has been lost', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - - await ball.add(controller); - await game.ensureAdd(ball); - }, - verify: (game, tester) async { - final controller = - game.descendants().whereType().first; - - final previousState = MockGameState(); - final newState = MockGameState(); - when(() => previousState.balls).thenReturn(3); - when(() => newState.balls).thenReturn(2); - - expect(controller.listenWhen(previousState, newState), isTrue); - }, - ); - - flameBlocTester.testGameWidget( - 'does not listen when a ball has not been lost', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - - await ball.add(controller); - await game.ensureAdd(ball); - }, - verify: (game, tester) async { - final controller = - game.descendants().whereType().first; - - final previousState = MockGameState(); - final newState = MockGameState(); - when(() => previousState.balls).thenReturn(3); - when(() => newState.balls).thenReturn(3); - - expect(controller.listenWhen(previousState, newState), isFalse); - }, - ); - }); - - group('onNewState', () { - flameBlocTester.testGameWidget( - 'removes ball', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.ensureAdd(ball); - - final state = MockGameState(); - when(() => state.balls).thenReturn(1); - controller.onNewState(state); - await game.ready(); - }, - verify: (game, tester) async { - expect(game.contains(ball), isFalse); - }, - ); - - flameBlocTester.testGameWidget( - 'spawns a new ball when the ball is not the last one', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.ensureAdd(ball); - - final state = MockGameState(); - when(() => state.balls).thenReturn(1); - - final previousBalls = game.descendants().whereType().toList(); - controller.onNewState(state); - await game.ready(); - - final currentBalls = game.descendants().whereType().toList(); - - expect(currentBalls.contains(ball), isFalse); - expect(currentBalls.length, equals(previousBalls.length)); - }, - ); - - flameBlocTester.testGameWidget( - 'does not spawn a new ball is the last one', - setUp: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.ensureAdd(ball); - - final state = MockGameState(); - when(() => state.balls).thenReturn(0); - - final previousBalls = game.descendants().whereType().toList(); - controller.onNewState(state); - await game.ready(); - - final currentBalls = game.descendants().whereType(); - - expect(currentBalls.contains(ball), isFalse); - expect( - currentBalls.length, - equals((previousBalls..remove(ball)).length), - ); - }, - ); - }); }); }); } diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index 60c55be9..d85fe54f 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -1,7 +1,6 @@ // 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'; @@ -11,18 +10,6 @@ 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(EmptyPinballGameTest.new); @@ -92,7 +79,7 @@ void main() { ); flameBlocTester.testGameWidget( - 'listens when a Bonus.dashNest is added', + 'listens when a Bonus.dashNest and a bonusBall is added', verify: (game, tester) async { final flutterForest = FlutterForest(); @@ -103,6 +90,7 @@ void main() { activatedDashNests: {}, bonusHistory: [GameBonus.dashNest], ); + expect( flutterForest.controller .listenWhen(const GameState.initial(), state), diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 18c7ea5b..f8e7483c 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -3,40 +3,15 @@ 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/game.dart'; import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(Forge2DGame.new); + final flameTester = FlameTester(EmptyPinballGameTest.new); group('Wall', () { - group('BottomWallBallContactCallback', () { - test( - 'removes the ball on begin contact when the wall is a bottom one', - () { - final wall = MockBottomWall(); - final ballController = MockBallController(); - final ball = MockBall(); - final componentSet = MockComponentSet(); - - when(() => componentSet.whereType()) - .thenReturn([ballController]); - when(() => ball.children).thenReturn(componentSet); - - BottomWallBallContactCallback() - // Remove once https://github.com/flame-engine/flame/pull/1415 - // is merged - ..end(MockBall(), MockBottomWall(), MockContact()) - ..begin(ball, wall, MockContact()); - - verify(ballController.lost).called(1); - }, - ); - }); - flameTester.test( 'loads correctly', (game) async { @@ -123,4 +98,67 @@ void main() { ); }); }); + + group( + 'BottomWall', + () { + group('removes ball on contact', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballGameTest.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'when ball is launch', + setUp: (game, tester) async { + final ball = ControlledBall.launch(theme: game.theme); + final wall = BottomWall(); + await game.ensureAddAll([ball, wall]); + game.addContactCallback(BottomWallBallContactCallback()); + + beginContact(game, ball, wall); + await game.ready(); + + expect(game.contains(ball), isFalse); + }, + ); + + flameBlocTester.testGameWidget( + 'when ball is bonus', + setUp: (game, tester) async { + final ball = ControlledBall.bonus(theme: game.theme); + final wall = BottomWall(); + await game.ensureAddAll([ball, wall]); + game.addContactCallback(BottomWallBallContactCallback()); + + beginContact(game, ball, wall); + await game.ready(); + + expect(game.contains(ball), isFalse); + }, + ); + + flameTester.test( + 'when ball is debug', + (game) async { + final ball = ControlledBall.debug(); + final wall = BottomWall(); + await game.ensureAddAll([ball, wall]); + game.addContactCallback(BottomWallBallContactCallback()); + + beginContact(game, ball, wall); + await game.ready(); + + expect(game.contains(ball), isFalse); + }, + ); + }); + }, + ); } diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index f418bad0..d83bb396 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: cascade_invocations import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -10,11 +11,11 @@ import 'package:pinball_components/pinball_components.dart'; import '../helpers/helpers.dart'; void main() { - group('PinballGame', () { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.new); - final debugModeFlameTester = FlameTester(DebugPinballGameTest.new); + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(PinballGameTest.new); + final debugModeFlameTester = FlameTester(DebugPinballGameTest.new); + group('PinballGame', () { // TODO(alestiago): test if [PinballGame] registers // [BallScorePointsCallback] once the following issue is resolved: // https://github.com/flame-engine/flame/issues/1416 @@ -60,8 +61,106 @@ void main() { equals(1), ); }); + + group('controller', () { + // TODO(alestiago): Write test to be controller agnostic. + group('listenWhen', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballGameTest.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'listens when all balls are lost and there are more than 0 balls', + setUp: (game, tester) async { + final newState = MockGameState(); + when(() => newState.balls).thenReturn(2); + game.descendants().whereType().forEach( + (ball) => ball.controller.lost(), + ); + await game.ready(); + + expect( + game.controller.listenWhen(MockGameState(), newState), + isTrue, + ); + }, + ); + + flameTester.test( + "doesn't listen when some balls are left", + (game) async { + final newState = MockGameState(); + when(() => newState.balls).thenReturn(1); + + expect( + game.descendants().whereType().length, + greaterThan(0), + ); + expect( + game.controller.listenWhen(MockGameState(), newState), + isFalse, + ); + }, + ); + + flameBlocTester.test( + "doesn't listen when no balls left", + (game) async { + final newState = MockGameState(); + when(() => newState.balls).thenReturn(0); + + game.descendants().whereType().forEach( + (ball) => ball.controller.lost(), + ); + await game.ready(); + + expect( + game.descendants().whereType().isEmpty, + isTrue, + ); + expect( + game.controller.listenWhen(MockGameState(), newState), + isFalse, + ); + }, + ); + }); + + group( + 'onNewState', + () { + flameTester.test( + 'spawns a ball', + (game) async { + await game.ready(); + final previousBalls = + game.descendants().whereType().toList(); + + game.controller.onNewState(MockGameState()); + await game.ready(); + final currentBalls = + game.descendants().whereType().toList(); + + expect( + currentBalls.length, + equals(previousBalls.length + 1), + ); + }, + ); + }, + ); + }); }); + }); + group('DebugPinballGame', () { debugModeFlameTester.test('adds a ball on tap up', (game) async { await game.ready(); @@ -71,12 +170,46 @@ void main() { final tapUpEvent = MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + final previousBalls = game.descendants().whereType().toList(); + game.onTapUp(tapUpEvent); await game.ready(); expect( game.children.whereType().length, - equals(1), + equals(previousBalls.length + 1), + ); + }); + + group('controller', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final debugModeFlameBlocTester = + FlameBlocTester( + gameBuilder: DebugPinballGameTest.new, + blocBuilder: () => gameBloc, + ); + + debugModeFlameBlocTester.testGameWidget( + 'ignores debug balls', + setUp: (game, tester) async { + final newState = MockGameState(); + when(() => newState.balls).thenReturn(1); + + await game.ready(); + game.children.removeWhere((component) => component is Ball); + await game.ready(); + await game.ensureAdd(ControlledBall.debug()); + + expect( + game.controller.listenWhen(MockGameState(), newState), + isTrue, + ); + }, ); }); }); diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index 4731eec4..8e054fe0 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -1,3 +1,5 @@ +// ignore_for_file: must_call_super + import 'package:pinball/game/game.dart'; import 'package:pinball_theme/pinball_theme.dart'; diff --git a/test/helpers/forge2d.dart b/test/helpers/forge2d.dart new file mode 100644 index 00000000..f000d404 --- /dev/null +++ b/test/helpers/forge2d.dart @@ -0,0 +1,13 @@ +import 'package:flame_forge2d/flame_forge2d.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); +} diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index d9dc2a17..4b6c29f1 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -7,6 +7,7 @@ export 'builders.dart'; export 'extensions.dart'; export 'fakes.dart'; +export 'forge2d.dart'; export 'key_testers.dart'; export 'mocks.dart'; export 'navigator.dart'; diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index c0dec5f5..748b48f3 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -21,6 +21,8 @@ class MockBody extends Mock implements Body {} class MockBall extends Mock implements Ball {} +class MockControlledBall extends Mock implements ControlledBall {} + class MockBallController extends Mock implements BallController {} class MockContact extends Mock implements Contact {}