From 254c38d2a4b0e7d215c6ec1943903e6cad0a5c71 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 6 Apr 2022 18:35:11 +0200 Subject: [PATCH 1/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 2/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 {} From 2f40dcc971ffae367817bd7ec75912ee2bc8d9d5 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:22:38 -0500 Subject: [PATCH 3/3] feat: add slingshots (#148) * feat: add slingshots * test: slingshot * feat: add slingshot to sandbox * chore: add todo --- lib/game/game_assets.dart | 4 + lib/game/pinball_game.dart | 1 + .../assets/images/slingshot/left_lower.png | Bin 0 -> 6458 bytes .../assets/images/slingshot/left_upper.png | Bin 0 -> 6061 bytes .../assets/images/slingshot/right_lower.png | Bin 0 -> 6542 bytes .../assets/images/slingshot/right_upper.png | Bin 0 -> 6002 bytes .../lib/gen/assets.gen.dart | 21 +++ .../lib/src/components/components.dart | 1 + .../lib/src/components/slingshot.dart | 138 ++++++++++++++++++ packages/pinball_components/pubspec.yaml | 1 + .../pinball_components/sandbox/lib/main.dart | 1 + .../lib/stories/slingshot/slingshot_game.dart | 66 +++++++++ .../lib/stories/slingshot/stories.dart | 17 +++ .../sandbox/lib/stories/stories.dart | 1 + .../test/src/components/golden/slingshots.png | Bin 0 -> 49944 bytes .../test/src/components/slingshot_test.dart | 97 ++++++++++++ 16 files changed, 348 insertions(+) create mode 100644 packages/pinball_components/assets/images/slingshot/left_lower.png create mode 100644 packages/pinball_components/assets/images/slingshot/left_upper.png create mode 100644 packages/pinball_components/assets/images/slingshot/right_lower.png create mode 100644 packages/pinball_components/assets/images/slingshot/right_upper.png create mode 100644 packages/pinball_components/lib/src/components/slingshot.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart create mode 100644 packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart create mode 100644 packages/pinball_components/test/src/components/golden/slingshots.png create mode 100644 packages/pinball_components/test/src/components/slingshot_test.dart diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 47175c32..050b2cd3 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -15,6 +15,10 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.baseboard.right.keyName), images.load(components.Assets.images.kicker.left.keyName), images.load(components.Assets.images.kicker.right.keyName), + images.load(components.Assets.images.slingshot.leftUpper.keyName), + images.load(components.Assets.images.slingshot.leftLower.keyName), + images.load(components.Assets.images.slingshot.rightUpper.keyName), + images.load(components.Assets.images.slingshot.rightLower.keyName), images.load(components.Assets.images.launchRamp.ramp.keyName), images.load( components.Assets.images.launchRamp.foregroundRailing.keyName, diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 7a0e6823..2ccf8fe8 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -46,6 +46,7 @@ class PinballGame extends Forge2DGame await add(plunger); unawaited(add(Board())); + unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(DinoWalls())); unawaited(_addBonusWord()); unawaited(addFromBlueprint(SpaceshipRamp())); diff --git a/packages/pinball_components/assets/images/slingshot/left_lower.png b/packages/pinball_components/assets/images/slingshot/left_lower.png new file mode 100644 index 0000000000000000000000000000000000000000..b44b58fbca8025771379cb8ac421e789a896c9ee GIT binary patch literal 6458 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 OAs7vVn>A{7k^cjqC8WIo literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/slingshot/right_lower.png b/packages/pinball_components/assets/images/slingshot/right_lower.png new file mode 100644 index 0000000000000000000000000000000000000000..71a6a277828fd5d0f99dc5cae819e067e5d3df90 GIT binary patch literal 6542 zcmW+*2RzjO8$Vf>T}digiHi$AossNp&MKQicCr-_StlnHQD#vHQC1vTT}VgL87Z@i zOSUt!{*V9jGSA(8Ki}thKkw)Le#Dy?>9Dh$U_l@d?0UMIX7F7BJ`Ipe@b!$erXPGe z=&NfTfIzSw+5eMCm@aJr0eMl7@Z4wwyxZ=M~S;5 zax6oYHKGuO{}yB~&qmy6<`L}hv$MCiA4xS<*>pBAFes(WSNxp2by3T-ufNptjeS$t zrkU^~-N&TK%JLaSp_pxR`j;rYf5XW9{-49=2~>{Rk%rsX+}x-vNfZVIMkq!&=AI+3 zduAawOYC-cmb?x#6BARCfa-3uq+OkFo=us>-oLO-N$H+k<?;=3@z3=N{>bTnnA68FZ?!dQj-`?+p_lsdDz; z>Wuu}IJ4?ToU=?un#<59D$Uu|71!0>eMm3meo2WY8I$jEqBkh#PrdgJHr>Tc@<}`j z|I!52HyW|G!@0IF);#82`%~4=SE+}_%)z&jb-g2L?Dq=iJQHbngHGoZLqo%dX2qIU%xlnR@WO|*dJi}^;oF2Wa&txd zJ=0Kl4R~6L$RSg1J;j_aO)K9Fq$vH%}JV#9!-jJZ%$bZM$O#(xBrDjCC-Ksj?Db$ zGm6WbN+^8P&$;%nvkV9>F1VZVm1**yKY#9xcr$nSnQ${A#(fpw%KgK`!<&Cg-|RLF zKqRw2Cr=07-6UNLZ=1VyTONZk;fSN}_dGi3f4tR!i(4x)AN8s7foBa%^H?*I4{=4p zoKN`pZ@!IhZjd@%57TO-$GO3oho;;6hFH7K_Egt*^##{cuDx%H0IXqce*4>*Ntcw z>!&UcsUe>#*@)70QehLUdtqZucb^q`WQM#(Tu7qihD>Ep*@?*F5|;D3N44mLoF5eg z+B0K&*R6bR-Qv+n=5K@S9^#05lFkQ-#gcjG)`FFCd&1YM_Jp~R3<44@z6yNP3kzcs zB$?|nhc)i6#zGD|-v!#oiRf8a@?dx?yBi#AwY zSy@ru{GIwNKYg@9hw))Ps-&TzL5z>17S4muN3Wk(&s5F2-E4w_^vs-?4m=SZ9sRvI zLe)-kP+8y=>a#tGTzxdT#)h3+ONa|ucWYsEN2`cDeFu%OPIsyG8mdflL;f z+#O_u<_9$LeA9hGV$?vUbiNFthX(#SG!#ESKObs0D9r|Vm|-&Ij)hv;52_<09eD#S zEeDJ08ydFwS=DNP&V~OiE|NQ}31Te#S<24WTsBEDb)U8(3GYf^%^c^9qq9MIOrvM*(9g$p41MOeuM;i8U)R2is z^j=(~0Y^_AY`xh1!sgp4b>S}^qYBCuyB_&^jMgk>1I{i0X=SL6tZa-qI z6p6zzk%djnj~196to0uG@bM!(nll|?C+)*&>>#-&4=t+2T{;+n0kZO3UB z84Q-(qq?_EW9H-s%nM84h2dg>_Bw8EMMuxWp)pw7)rS;?Iy*^)93z06UdqEx=ZD>{ zkBo}`la-Avo<94k1mE7S9Yb`iO4AU2q(-pQzI2IRA&J=6$1S3#A;{(1?^)B=mT;^` z*^B69ZNkOi`19S(pA716fja={ILC*EqNZpb$EqPNmRPPt?f+zCq5>7!0Ux|f?wQ&O zeA>pwod5p)Lodc~1vg%;uC2M=hCg~=Q~0^Z4=1hSD=ZVfytf)o6_|D6jd6}%4Y%h0 z*@+x3j&SDj^7Qol*Dl|*lxTwD{uXrV{Q2__U5@9Yq6$4Aac|MAsr?@*e!-2SS{3cm zYkOSEj*o`{=Sf#gZY$&93ir7gWgXe#k zczYYJ5$Npv`!n$sBApSEYC8jQ79T~Lj2fJY1x$4KDMailY3t}fA;VMz6crW6X`8-( z`_9_c2OfkPf5Fx4Jdc>DsI-CtvWOgCSSX5&x)-LBT;JUxOeU2UAm1fmR}WsL_H* zUxMAZ0tY;Dzk~9s-_yjr|MOdaEUwT4enIgh@CpcY2<$F0za)F~_4iv3n#mq5GJe(` ze?;u-zyMBHgKUzP2tG+_=9!?f5s@m7Rr*EDYpsjRNN*DQ?$F6m!>pt z40&3!z^rwATyQje>*oFZ`vk%TX}0Trme6fm+f>!Pw)1XZvR_{jPm4S7XZ21a>q}LJ z19TXmZ&U6W+5i5FT!`4)3WjO@b!^Os@D>VIVO4!KO}|aw3@$j~+cLBIVP2ZR}YTsq!n}FA>vrN4h z_N;Cuy%YytT-wppPLXinc@1tYKvJ+0KMao|^1tR2*%FOhA zWMrf&We~w$+)zvZv<>XFL4H6~6o(#>(g9V48@7{fItpXmCX1f%pk=L?B>DZ&f^qQq7 zpoC%!u~_U`e7>(}Nr(haL+F0Kd;i6Bmx>UYE65c#PR`T($uIKq=;r3;{`~!`5fE@5 zx&y{pNeS@E9hnTi3lY29s_g9SJ926IiJvRcjM1&-X%d!Ae*zUPGlig$e=R8tyt&4C z+4lvCrNZj<-G$M{t%z-kz#etdrnfS^q~vdr>dv$=ob2o2n@ns(pm_H?@7-eXOPwzb|917)^=_8w~n^4vOLG8yq}+BGJbAnTxe37jh#RgqnAS61(ezh+>||##*f$?UMES5b!|EX`o$b0hiZw7TizK&@`Nj@rEK}KeIjER}~ngllsC5G#| zo|_{IZx;3|UX0@{dPXi(ldN{?)G3&Uh4-`|6<>#k?be|3mq>X&T~XTRZoIgm=7_!U z(U+%K?b7m5601MGYj;sn&cF1^Kz&2a?VM4@9lMiCz3lmb3bb% z#7N6J?btGlBPUOs7{C!Hys>1I{opyDrL7%cMGNgog`}I_-rg!ffBQm*yyR$Vm5!pR zov-(A5`R>}0zFrPZT<9^AtYA^#l7rSHM9+2AN%ZDb*yq|);mpnpJIw(trG^+PI@>|z@=mo@$8k_x#dhbi#?5VV4 zb>baK7bP=K|&j zfoT|Xvi$NMxhQ(x)fVwO=68HzM_W8k%QRiK&^p5UtVjXP0IaNNSYAKrMQ znR?_?Wm2JsY*x4H!tCtu!F%@ZZT%HTN zg%;$mUlDL6yzLdtiJK<`1onKryu4O=f=;aW3kDGEBzf7Gb-&Mc z1PY71Tx4`A)g@#o+tif1b7ochdc}89EZLoGz^$cTVYN?yA#V-}jVmMpQwA+G@9kCq zBV3hJ13s9B$hmlX{&i_-X+zKO2YssO-7@&O8DJZ?6ueEG{3whiS9iNotkY!&%IzD* z7S=Cz^MB(m&@VVF{OEq$YoyQ>6n6=UO1q7Vi<8vC(}U464R$13%H@k1dt>%?H0ZAZCnEyq-8 zQ4w>V2@(b{nxtWF9&n#1D=#mfb0J_#Q{`EY)XC?LyzPAuQ(%)ioGQM5t5hdfn;*jt zt2~nmo*yPeI?IG?85`m~B3iDk&wg=JUGMe@r;SBSN?hA~0^0pXey{suYREvgQAW=F z9_l3+!orV`Fn+su&AA!*lf~4K#rvr_9l!A`n_f3X1k}b4$iS_0=XOP z&|oG{`V%%_(C3_I$Xv4!>J* z#6wEZYZdwh!aGBBf4*HVzVh!OTm0ogLj6F0YQ*^?KUrrV7ohM!M^+XP%()7VFDfJLtt6KS3GLBI63t;XV z-W;h9B7s%m940pHn!WQ|{o7ya^M0xQIlPt6k2}Nct%Z4I|zzt}GFcRSoY=RSbE-MAw%q^#16_*FQK|+XOnjH_XqP z@(bKJUi(EaHcRfN&4~n8=I7$#p!7;g-Z@pEs%vW-t_DxU(#LR#rs=m*X{`(Ew(>8{ ziq!^#K-;z%u*EnyJ2Rk3PLrS0A{bPqj4}{s@s+~^6TZG_`ix@6x!XC@9xE~zUM$aC z+xzDWAU}7v=Y5qZ5ta%z;6dIco|F%dq=tC6j-5le@-&SNWM_DR>q%espy4uOV9LV2 zK!J~=zP7e@kV<1rpUa;jfCBjg8Y5Y1GGfZksa5)j zQyk3VX)N=D(x32NcayAbcya2#=RLw4?m-jZ`SUgJQh-4e(NK=HW2CTnoFeOo5L@y=1J#lcR2EU~Atj3w6_9we3w-7$xMGxl)vkM#ytZ#T~9H+jA3Dx-% z*sRkhmV#e9r*=BO?&QXjarvm_OLQ1mNR3Bxs?^;9SQac~wEcJ;01l?j39Q)%%F=#A z@h96>TtY??C)9Lwm|+o7ey!OXn?84|LH2@NT;b)*mj|$ztY7{FuWo^ZLYnE5f9ccW z3o8D=tT0yHSs}C5GIl?GnvoGHqW2WYpo5}*{rZcvZ(?F%aGdd54d*yK-)V#81c?Eg zB#Q+da`og7P;Q_=EwZz!5`7iek|@$DDpwW5HWnkWqN%Bzj1RdqfY-u7vxO2_2Qs>X zC3q50+$;}84#L4DEX!jAk)iisVH*8~$}QhY@kg9)TqHvjAAJfkPB{5B!e&EMaDS-yF*+b7-gv0N5=uRFI< z(UB-Ya1{>z{)aJGB4z1$Ss6QjGBfo{&ZRsPt3Jz2!=mzX4tOSXrRS~VxQdi7v$N=O zJAxDTg>`ycSJ#986cz7On4r2;suMDQVDn5+z%1h}AF!>lwkB}ysp0&JibE>TIDt(j ze4*q{US4eVl_V$`??1b+N{>H1|dZE=VlKCzy6FF747ne`d zArY|}d2jbC`+ueKkUmpCIM3uBn)J!F>S=DSwt+!{EK-)#0z}{krcLX#JWV@eR>$1j zS?2h@#?CMlJ{;II&aZNQJ8KMy6e zjr$XTh6n@s$qydz?aui`Y|X^}>pDHuoeh3sVam2{c(W~g;ivlN45`X^|k@Na~kNu=v%fk9;F66#T91JQA zl}}hVOJqVoGqkt2KQtZ3!7Z(c(X8%620)PVk=ckLFh1Y}bpH?w=mmnw6 zrCb9Q+ux6{5v^{VEzATjO)xqD(rq<4AN6Z{lco?b#RiB1K?A$RqMcbc)h#J8qiltW z>Vv_~Gus)4qO<{9pGM%~!P+ZM7z5afh;4_fLMEXrlK>GKdG}81B!RPp71;BpCVsF{ z787za?of6VUbn#PIf?~F>Yw>}97q@Ow9ot(VIjAp?V0RV#e=YTZB_AWu68Uuv>!ys zZcI%cip8J6w_iVSvl$u3VJ&;5v|o#L##NLF(J+z(uJX$lqrjk`yr!m6KI-u}P63te z{x{*Pr3Gfifn#T4bb{c;qDg!g{C}reUJYq;T{Bu2{|TN38vIph=_OdC0B?img;c`) zK#z^29A=>Nu007KAmSeBGI-a9nx5@+tr{A<7O}IcDjyT#TvSj{FbRfUM;5kQ*MixI zZ~6C%ElM?0L}1liR^)=gp2j&2(;`YrN>n#Hjl=y_8`mmM;qzaOLIC;F(L!1~`~vL< z<8V0KzJzBISkLpw9IXel-3l>j<$s(86HTcz`)5%U)+#T(oV>PXm& zzk_g)=3Tkdcs03$V#=)rd+%H$s8I$4h#8QSHOX7ka~+4@!1^tDnNYAN^<|IlP_Tte UBvxtz{=tIK(=yU5*KmmXA3&?lwg3PC literal 0 HcmV?d00001 diff --git a/packages/pinball_components/assets/images/slingshot/right_upper.png b/packages/pinball_components/assets/images/slingshot/right_upper.png new file mode 100644 index 0000000000000000000000000000000000000000..e6b42ded4f2a513501042baaba58a1a7174ac402 GIT binary patch literal 6002 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 F{|64fsRRH3 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 de59219e..518d3237 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -29,6 +29,7 @@ class $AssetsImagesGen { $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); + $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSparkyBumperGen get sparkyBumper => const $AssetsImagesSparkyBumperGen(); @@ -127,6 +128,26 @@ class $AssetsImagesLaunchRampGen { const AssetGenImage('assets/images/launch_ramp/ramp.png'); } +class $AssetsImagesSlingshotGen { + const $AssetsImagesSlingshotGen(); + + /// File path: assets/images/slingshot/left_lower.png + AssetGenImage get leftLower => + const AssetGenImage('assets/images/slingshot/left_lower.png'); + + /// File path: assets/images/slingshot/left_upper.png + AssetGenImage get leftUpper => + const AssetGenImage('assets/images/slingshot/left_upper.png'); + + /// File path: assets/images/slingshot/right_lower.png + AssetGenImage get rightLower => + const AssetGenImage('assets/images/slingshot/right_lower.png'); + + /// File path: assets/images/slingshot/right_upper.png + AssetGenImage get rightUpper => + const AssetGenImage('assets/images/slingshot/right_upper.png'); +} + class $AssetsImagesSpaceshipGen { const $AssetsImagesSpaceshipGen(); diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 6b0c2ef5..14d657d5 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -16,6 +16,7 @@ export 'launch_ramp.dart'; export 'layer.dart'; export 'ramp_opening.dart'; export 'shapes/shapes.dart'; +export 'slingshot.dart'; export 'spaceship.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; diff --git a/packages/pinball_components/lib/src/components/slingshot.dart b/packages/pinball_components/lib/src/components/slingshot.dart new file mode 100644 index 00000000..0ebe13ce --- /dev/null +++ b/packages/pinball_components/lib/src/components/slingshot.dart @@ -0,0 +1,138 @@ +// ignore_for_file: avoid_renaming_method_parameters + +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 slingshots} +/// A [Blueprint] which creates the left and right pairs of [Slingshot]s. +/// {@endtemplate} +class Slingshots extends Forge2DBlueprint { + @override + void build(_) { + // TODO(allisonryan0002): use radians values instead of converting degrees. + final leftUpperSlingshot = Slingshot( + length: 5.66, + angle: -1.5 * (math.pi / 180), + spritePath: Assets.images.slingshot.leftUpper.keyName, + )..initialPosition = Vector2(-29, 1.5); + + final leftLowerSlingshot = Slingshot( + length: 3.54, + angle: -29.1 * (math.pi / 180), + spritePath: Assets.images.slingshot.leftLower.keyName, + )..initialPosition = Vector2(-31, -6.2); + + final rightUpperSlingshot = Slingshot( + length: 5.64, + angle: 1 * (math.pi / 180), + spritePath: Assets.images.slingshot.rightUpper.keyName, + )..initialPosition = Vector2(22.3, 1.58); + + final rightLowerSlingshot = Slingshot( + length: 3.46, + angle: 26.8 * (math.pi / 180), + spritePath: Assets.images.slingshot.rightLower.keyName, + )..initialPosition = Vector2(24.7, -6.2); + + addAll([ + leftUpperSlingshot, + leftLowerSlingshot, + rightUpperSlingshot, + rightLowerSlingshot, + ]); + } +} + +/// {@template slingshot} +/// Elastic bumper that bounces the [Ball] off of its straight sides. +/// {@endtemplate} +class Slingshot extends BodyComponent with InitialPosition { + /// {@macro slingshot} + Slingshot({ + required double length, + required double angle, + required String spritePath, + }) : _length = length, + _angle = angle, + _spritePath = spritePath, + super(priority: 1); + + final double _length; + + final double _angle; + + final String _spritePath; + + List _createFixtureDefs() { + final fixturesDef = []; + const circleRadius = 1.55; + + final topCircleShape = CircleShape()..radius = circleRadius; + topCircleShape.position.setValues(0, _length / 2); + final topCircleFixtureDef = FixtureDef(topCircleShape)..friction = 0; + fixturesDef.add(topCircleFixtureDef); + + final bottomCircleShape = CircleShape()..radius = circleRadius; + bottomCircleShape.position.setValues(0, -_length / 2); + final bottomCircleFixtureDef = FixtureDef(bottomCircleShape)..friction = 0; + fixturesDef.add(bottomCircleFixtureDef); + + final leftEdgeShape = EdgeShape() + ..set( + Vector2(circleRadius, _length / 2), + Vector2(circleRadius, -_length / 2), + ); + final leftEdgeShapeFixtureDef = FixtureDef(leftEdgeShape) + ..friction = 0 + ..restitution = 5; + fixturesDef.add(leftEdgeShapeFixtureDef); + + final rightEdgeShape = EdgeShape() + ..set( + Vector2(-circleRadius, _length / 2), + Vector2(-circleRadius, -_length / 2), + ); + final rightEdgeShapeFixtureDef = FixtureDef(rightEdgeShape) + ..friction = 0 + ..restitution = 5; + fixturesDef.add(rightEdgeShapeFixtureDef); + + return fixturesDef; + } + + @override + Body createBody() { + final bodyDef = BodyDef() + ..userData = this + ..position = initialPosition + ..angle = _angle; + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprite(); + renderBody = false; + } + + Future _loadSprite() async { + final sprite = await gameRef.loadSprite(_spritePath); + + await add( + SpriteComponent( + sprite: sprite, + size: sprite.originalSize / 10, + anchor: Anchor.center, + angle: _angle, + ), + ); + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 312e01f3..b6f71b8b 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -39,6 +39,7 @@ flutter: - assets/images/spaceship/ramp/ - assets/images/chrome_dino/ - assets/images/kicker/ + - assets/images/slingshot/ - assets/images/sparky_bumper/a/ - assets/images/sparky_bumper/b/ - assets/images/sparky_bumper/c/ diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 88b86da6..481ca781 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -21,6 +21,7 @@ void main() { addChromeDinoStories(dashbook); addDashNestBumperStories(dashbook); addKickerStories(dashbook); + addSlingshotStories(dashbook); addSparkyBumperStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart new file mode 100644 index 00000000..c02689ca --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart @@ -0,0 +1,66 @@ +import 'dart:math' as math; + +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 SlingshotGame extends BasicBallGame { + SlingshotGame({ + required this.trace, + }) : super(color: const Color(0xFFFF0000)); + + static const info = ''' + Shows how Slingshots are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + final bool trace; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + + final leftUpperSlingshot = Slingshot( + length: 5.66, + angle: -1.5 * (math.pi / 180), + spritePath: Assets.images.slingshot.leftUpper.keyName, + )..initialPosition = center + Vector2(-29, 1.5); + + final leftLowerSlingshot = Slingshot( + length: 3.54, + angle: -29.1 * (math.pi / 180), + spritePath: Assets.images.slingshot.leftLower.keyName, + )..initialPosition = center + Vector2(-31, -6.2); + + final rightUpperSlingshot = Slingshot( + length: 5.64, + angle: 1 * (math.pi / 180), + spritePath: Assets.images.slingshot.rightUpper.keyName, + )..initialPosition = center + Vector2(22.3, 1.58); + + final rightLowerSlingshot = Slingshot( + length: 3.46, + angle: 26.8 * (math.pi / 180), + spritePath: Assets.images.slingshot.rightLower.keyName, + )..initialPosition = center + Vector2(24.7, -6.2); + + await addAll([ + leftUpperSlingshot, + leftLowerSlingshot, + rightUpperSlingshot, + rightLowerSlingshot, + ]); + + if (trace) { + leftUpperSlingshot.trace(); + leftLowerSlingshot.trace(); + rightUpperSlingshot.trace(); + rightLowerSlingshot.trace(); + } + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart new file mode 100644 index 00000000..6e985d32 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/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/slingshot/slingshot_game.dart'; + +void addSlingshotStories(Dashbook dashbook) { + dashbook.storiesOf('Slingshots').add( + 'Basic', + (context) => GameWidget( + game: SlingshotGame( + trace: context.boolProperty('Trace', true), + ), + ), + codeLink: buildSourceLink('slingshot_game/basic.dart'), + info: SlingshotGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 746d83d6..c5d60a8d 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -5,5 +5,6 @@ export 'dash_nest_bumper/stories.dart'; export 'effects/stories.dart'; export 'flipper/stories.dart'; export 'layer/stories.dart'; +export 'slingshot/stories.dart'; export 'spaceship/stories.dart'; export 'sparky_bumper/stories.dart'; diff --git a/packages/pinball_components/test/src/components/golden/slingshots.png b/packages/pinball_components/test/src/components/golden/slingshots.png new file mode 100644 index 0000000000000000000000000000000000000000..2e4ada7b6a610b9dce69f08eabff4dc68bc8c29c GIT binary patch literal 49944 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, + // matchesGoldenFile('golden/slingshots.png'), + // ); + // }, + ); + + flameTester.test( + 'loads correctly', + (game) async { + final slingshot = Slingshot( + length: length, + angle: angle, + spritePath: spritePath, + ); + await game.ensureAdd(slingshot); + + expect(game.contains(slingshot), isTrue); + }, + ); + + flameTester.test( + 'body is static', + (game) async { + final slingshot = Slingshot( + length: length, + angle: angle, + spritePath: spritePath, + ); + await game.ensureAdd(slingshot); + + expect(slingshot.body.bodyType, equals(BodyType.static)); + }, + ); + + flameTester.test( + 'has restitution', + (game) async { + final slingshot = Slingshot( + length: length, + angle: angle, + spritePath: spritePath, + ); + await game.ensureAdd(slingshot); + + final totalRestitution = slingshot.body.fixtures.fold( + 0, + (total, fixture) => total + fixture.restitution, + ); + expect(totalRestitution, greaterThan(0)); + }, + ); + + flameTester.test( + 'has no friction', + (game) async { + final slingshot = Slingshot( + length: length, + angle: angle, + spritePath: spritePath, + ); + await game.ensureAdd(slingshot); + + final totalFriction = slingshot.body.fixtures.fold( + 0, + (total, fixture) => total + fixture.friction, + ); + expect(totalFriction, equals(0)); + }, + ); + }); +}