From 4744f552f0ddfc19a965224c95599beb6a8bf6c5 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Thu, 7 Apr 2022 19:54:13 +0200 Subject: [PATCH 1/5] fix: removed ghost PlungerAnchor shape (#162) --- lib/game/components/plunger.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 1911be02..b8c079b5 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -136,12 +136,10 @@ class PlungerAnchor extends JointAnchor { @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); + return world.createBody(bodyDef); } } From fdc3039f72713fe4acfc23f30b589637c23e927a Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Fri, 8 Apr 2022 11:22:33 +0200 Subject: [PATCH 2/5] feat: sparky bumpers game logic (#154) * feat: added event for sparky bumpers hit * test: test sparky bumper bloc event * refactor: added sparky bumpers actives to GameState * refactor: removed active sparky bumper from game bloc logic * refactor: moved sparky bumpers logic to SparkyBumperController * test: tests for SparkyBumperController * chore: removed unused import * refactor: changed sparky fire zone * refactor: changed sparky fire zone * chore: analysis errors * chore: analysis error * Update test/game/bloc/game_bloc_test.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * Update lib/game/components/sparky_fire_zone.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * chore: formatting file Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/components/components.dart | 1 + lib/game/components/sparky_fire_zone.dart | 44 ++++++++++++++++++ lib/gen/assets.gen.dart | 3 ++ .../components/sparky_fire_zone_test.dart | 45 +++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 lib/game/components/sparky_fire_zone.dart create mode 100644 test/game/components/sparky_fire_zone_test.dart diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 61d0f3ca..9dd7eed4 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -5,4 +5,5 @@ export 'controlled_flipper.dart'; export 'flutter_forest.dart'; export 'plunger.dart'; export 'score_points.dart'; +export 'sparky_fire_zone.dart'; export 'wall.dart'; diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart new file mode 100644 index 00000000..9d88f0f5 --- /dev/null +++ b/lib/game/components/sparky_fire_zone.dart @@ -0,0 +1,44 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:pinball/flame/flame.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +// TODO(ruimiguel): create and add SparkyFireZone component here in other PR. + +// TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done +// ignore: public_member_api_docs +class ControlledSparkyBumper extends SparkyBumper + with Controls<_SparkyBumperController> { + // TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done + // ignore: public_member_api_docs + ControlledSparkyBumper() : super.a() { + controller = _SparkyBumperController(this); + } +} + +/// {@template sparky_bumper_controller} +/// Controls a [SparkyBumper]. +/// {@endtemplate} +class _SparkyBumperController extends ComponentController + with HasGameRef { + /// {@macro sparky_bumper_controller} + _SparkyBumperController(ControlledSparkyBumper controlledSparkyBumper) + : super(controlledSparkyBumper); + + /// Flag for activated state of the [SparkyBumper]. + /// + /// Used to toggle [SparkyBumper]s' state between activated and deactivated. + bool isActivated = false; + + /// Registers when a [SparkyBumper] is hit by a [Ball]. + void hit() { + if (isActivated) { + component.deactivate(); + } else { + component.activate(); + } + isActivated = !isActivated; + } +} diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 5c2a87c2..90013646 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -3,6 +3,8 @@ /// FlutterGen /// ***************************************************** +// ignore_for_file: directives_ordering,unnecessary_import + import 'package:flutter/widgets.dart'; class $AssetsImagesGen { @@ -15,6 +17,7 @@ class $AssetsImagesGen { class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); + /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); diff --git a/test/game/components/sparky_fire_zone_test.dart b/test/game/components/sparky_fire_zone_test.dart new file mode 100644 index 00000000..dceaa9cc --- /dev/null +++ b/test/game/components/sparky_fire_zone_test.dart @@ -0,0 +1,45 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(EmptyPinballGameTest.new); + + group('SparkyFireZone', () { + group('bumpers', () { + late ControlledSparkyBumper controlledSparkyBumper; + + flameTester.testGameWidget( + 'activate when deactivated bumper is hit', + setUp: (game, tester) async { + controlledSparkyBumper = ControlledSparkyBumper(); + await game.ensureAdd(controlledSparkyBumper); + + controlledSparkyBumper.controller.hit(); + }, + verify: (game, tester) async { + expect(controlledSparkyBumper.controller.isActivated, isTrue); + }, + ); + + flameTester.testGameWidget( + 'deactivate when activated bumper is hit', + setUp: (game, tester) async { + controlledSparkyBumper = ControlledSparkyBumper(); + await game.ensureAdd(controlledSparkyBumper); + + controlledSparkyBumper.controller.hit(); + controlledSparkyBumper.controller.hit(); + }, + verify: (game, tester) async { + expect(controlledSparkyBumper.controller.isActivated, isFalse); + }, + ); + }); + }); +} From 51b7f478afec06be49929c1ba53e1492a5eee9cd Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Fri, 8 Apr 2022 11:11:32 +0100 Subject: [PATCH 3/5] refactor: SpriteComponent subclassing (#158) * refactor: remove _loadSprites * feat: defined BallSprite * refactor: removed late SpriteComponent * refactor: defined BaseboardSprite * refactor: defined DinoWalls sprites * refactor: defined FlutterSIgnPostSprite * refactor: defined KickerSprite * refactor: defined LaunchRampSprite * refactor: removed trailing comma * refactor: defined _AndroidHeadSpriteAnimation * refactor: removed unused import * refactor: defined FlipperSprite * docs: improved SpaceshipWall docs * refactor: defined SpaceshipRailRampSprite * refactor: suffixed Component * refactor: adjusted FlipperSpriteComponent size * refactor: resized ball * chore: trace all bodies in sandbox (#157) * feat: add traceAllBodies method * chore: update tracing stories * refactor: use for each * refactor: convert to mixin * refactor: simplify mixin * feat(sandbox): made BasicBallGame traceable * feat: adjusted ball size --- .../lib/src/components/ball.dart | 36 +++++---- .../lib/src/components/baseboard.dart | 37 +++++---- .../lib/src/components/boundaries.dart | 42 +++++----- .../lib/src/components/dino_walls.dart | 46 ++++++----- .../lib/src/components/flipper.dart | 41 +++++----- .../lib/src/components/flutter_sign_post.dart | 39 +++++----- .../lib/src/components/kicker.dart | 31 +++++--- .../lib/src/components/launch_ramp.dart | 47 +++++------ .../lib/src/components/spaceship.dart | 50 +++++++----- .../lib/src/components/spaceship_rail.dart | 22 +++--- .../lib/src/components/spaceship_ramp.dart | 78 ++++++++++--------- .../lib/stories/ball/basic_ball_game.dart | 3 +- .../sandbox/lib/stories/ball/stories.dart | 2 +- 13 files changed, 254 insertions(+), 220 deletions(-) diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index 892936f9..6aaf88de 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -22,36 +22,31 @@ class Ball extends BodyComponent layer = Layer.board; } - /// The size of the [Ball] - static final Vector2 size = Vector2.all(4.5); + /// The size of the [Ball]. + static final Vector2 size = Vector2.all(4.13); - /// The base [Color] used to tint this [Ball] + /// The base [Color] used to tint this [Ball]. final Color baseColor; double _boostTimer = 0; static const _boostDuration = 2.0; - late SpriteComponent _spriteComponent; + + final _BallSpriteComponent _spriteComponent = _BallSpriteComponent(); @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite(Assets.images.ball.keyName); - final tint = baseColor.withOpacity(0.5); + renderBody = false; + await add( - _spriteComponent = SpriteComponent( - sprite: sprite, - size: size * 1.15, - anchor: Anchor.center, - )..tint(tint), + _spriteComponent..tint(baseColor.withOpacity(0.5)), ); } @override Body createBody() { final shape = CircleShape()..radius = size.x / 2; - final fixtureDef = FixtureDef(shape)..density = 1; - final bodyDef = BodyDef() ..position = initialPosition ..userData = this @@ -70,7 +65,7 @@ class Ball extends BodyComponent /// Allows the [Ball] to be affected by forces. /// - /// If previously [stop]ed, the previous ball's velocity is not kept. + /// If previously [stop]ped, the previous ball's velocity is not kept. void resume() { body.setType(BodyType.dynamic); } @@ -114,3 +109,16 @@ class Ball extends BodyComponent _spriteComponent.scale = Vector2.all(scaleFactor); } } + +class _BallSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + Assets.images.ball.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + } +} diff --git a/packages/pinball_components/lib/src/components/baseboard.dart b/packages/pinball_components/lib/src/components/baseboard.dart index 0a6bcc91..56b7c978 100644 --- a/packages/pinball_components/lib/src/components/baseboard.dart +++ b/packages/pinball_components/lib/src/components/baseboard.dart @@ -82,23 +82,8 @@ class Baseboard extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - - final sprite = await gameRef.loadSprite( - (_side.isLeft) - ? Assets.images.baseboard.left.keyName - : Assets.images.baseboard.right.keyName, - ); - - await add( - SpriteComponent( - sprite: sprite, - size: Vector2(27.5, 17.9), - anchor: Anchor.center, - position: Vector2(_side.isLeft ? 0.4 : -0.4, 0), - ), - ); - renderBody = false; + await add(_BaseboardSpriteComponent(side: _side)); } @override @@ -115,3 +100,23 @@ class Baseboard extends BodyComponent with InitialPosition { return body; } } + +class _BaseboardSpriteComponent extends SpriteComponent with HasGameRef { + _BaseboardSpriteComponent({required BoardSide side}) : _side = side; + + final BoardSide _side; + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + (_side.isLeft) + ? Assets.images.baseboard.left.keyName + : Assets.images.baseboard.right.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + position = Vector2(0.4 * -_side.direction, 0); + anchor = Anchor.center; + } +} diff --git a/packages/pinball_components/lib/src/components/boundaries.dart b/packages/pinball_components/lib/src/components/boundaries.dart index 79b3f909..a542cd2a 100644 --- a/packages/pinball_components/lib/src/components/boundaries.dart +++ b/packages/pinball_components/lib/src/components/boundaries.dart @@ -63,23 +63,22 @@ class _BottomBoundary extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); renderBody = false; + await add(_BottomBoundarySpriteComponent()); } +} - Future _loadSprite() async { +class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( Assets.images.boundary.bottom.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.center, - position: Vector2(-5.4, 57.4), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(-5.4, 57.4); } } @@ -135,22 +134,21 @@ class _OuterBoundary extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); renderBody = false; + await add(_OuterBoundarySpriteComponent()); } +} - Future _loadSprite() async { +class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( Assets.images.boundary.outer.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.center, - position: Vector2(-0.2, -1.4), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(-0.2, -1.4); } } diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index daf83850..3c5d7f37 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -81,13 +81,10 @@ class _DinoTopWall extends BodyComponent with InitialPosition { @override Body createBody() { - renderBody = false; - final bodyDef = BodyDef() ..userData = this ..position = initialPosition ..type = BodyType.static; - final body = world.createBody(bodyDef); _createFixtureDefs().forEach( (fixture) => body.createFixture( @@ -103,21 +100,22 @@ class _DinoTopWall extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); + renderBody = false; + + await add(_DinoTopWallSpriteComponent()); } +} - Future _loadSprite() async { +class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( Assets.images.dino.dinoLandTop.keyName, ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: Vector2(10.6, 27.7), - anchor: Anchor.center, - position: Vector2(27, -28.2), - ); - - await add(spriteComponent); + this.sprite = sprite; + size = sprite.originalSize / 10; + position = Vector2(27, -28.2); } } @@ -182,8 +180,6 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { @override Body createBody() { - renderBody = false; - final bodyDef = BodyDef() ..userData = this ..position = initialPosition @@ -204,19 +200,21 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); + renderBody = false; + + await add(_DinoBottomWallSpriteComponent()); } +} - Future _loadSprite() async { +class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( Assets.images.dino.dinoLandBottom.keyName, ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: Vector2(15.6, 54.8), - anchor: Anchor.center, - )..position = Vector2(31.7, 18); - - await add(spriteComponent); + this.sprite = sprite; + size = sprite.originalSize / 10; + position = Vector2(31.7, 18); } } diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index 64a82269..7e48366b 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -42,22 +42,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { body.linearVelocity = Vector2(0, _speed); } - /// Loads the sprite that renders with the [Flipper]. - Future _loadSprite() async { - final sprite = await gameRef.loadSprite( - (side.isLeft) - ? Assets.images.flipper.left.keyName - : Assets.images.flipper.right.keyName, - ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: size, - anchor: Anchor.center, - ); - - await add(spriteComponent); - } - /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. Future _anchorToJoint() async { final anchor = _FlipperAnchor(flipper: this); @@ -129,10 +113,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { await super.onLoad(); renderBody = false; - await Future.wait([ - _loadSprite(), - _anchorToJoint(), - ]); + await _anchorToJoint(); + await add(_FlipperSpriteComponent(side: side)); } @override @@ -148,6 +130,25 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { } } +class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { + _FlipperSpriteComponent({required BoardSide side}) : _side = side; + + final BoardSide _side; + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + (_side.isLeft) + ? Assets.images.flipper.left.keyName + : Assets.images.flipper.right.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + } +} + /// {@template flipper_anchor} /// [JointAnchor] positioned at the end of a [Flipper]. /// diff --git a/packages/pinball_components/lib/src/components/flutter_sign_post.dart b/packages/pinball_components/lib/src/components/flutter_sign_post.dart index deaceb76..070fa316 100644 --- a/packages/pinball_components/lib/src/components/flutter_sign_post.dart +++ b/packages/pinball_components/lib/src/components/flutter_sign_post.dart @@ -1,33 +1,17 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template flutter_sign_post} -/// A sign, found in the FlutterForest. +/// A sign, found in the Flutter Forest. /// {@endtemplate} -// TODO(alestiago): Revisit doc comment if FlutterForest is moved to package. class FlutterSignPost extends BodyComponent with InitialPosition { - Future _loadSprite() async { - final sprite = await gameRef.loadSprite( - Assets.images.flutterSignPost.keyName, - ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.bottomCenter, - position: Vector2(0.65, 0.45), - ); - await add(spriteComponent); - } - @override Future onLoad() async { await super.onLoad(); - paint = Paint() - ..color = Colors.blue.withOpacity(0.5) - ..style = PaintingStyle.fill; - await _loadSprite(); + renderBody = false; + + await add(_FlutterSignPostSpriteComponent()); } @override @@ -39,3 +23,18 @@ class FlutterSignPost extends BodyComponent with InitialPosition { return world.createBody(bodyDef)..createFixture(fixtureDef); } } + +class _FlutterSignPostSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + + final sprite = await gameRef.loadSprite( + Assets.images.flutterSignPost.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.bottomCenter; + position = Vector2(0.65, 0.45); + } +} diff --git a/packages/pinball_components/lib/src/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker.dart index 442f4200..de009595 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker.dart @@ -18,6 +18,9 @@ class Kicker extends BodyComponent with InitialPosition { required BoardSide side, }) : _side = side; + /// The size of the [Kicker] body. + static final Vector2 size = Vector2(4.4, 15); + /// Whether the [Kicker] is on the left or right side of the board. /// /// A [Kicker] with [BoardSide.left] propels the [Ball] to the right, @@ -25,9 +28,6 @@ class Kicker extends BodyComponent with InitialPosition { /// left. final BoardSide _side; - /// The size of the [Kicker] body. - static final Vector2 size = Vector2(4.4, 15); - List _createFixtureDefs() { final fixturesDefs = []; final direction = _side.direction; @@ -122,21 +122,28 @@ class Kicker extends BodyComponent with InitialPosition { Future onLoad() async { await super.onLoad(); renderBody = false; + await add(_KickerSpriteComponent(side: _side)); + } +} + +class _KickerSpriteComponent extends SpriteComponent with HasGameRef { + _KickerSpriteComponent({required BoardSide side}) : _side = side; + + final BoardSide _side; + + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( (_side.isLeft) ? Assets.images.kicker.left.keyName : Assets.images.kicker.right.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: Vector2(8.7, 19), - anchor: Anchor.center, - position: Vector2(0.7 * -_side.direction, -2.2), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(0.7 * -_side.direction, -2.2); } } diff --git a/packages/pinball_components/lib/src/components/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index 3268cc46..2eea7a91 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -115,23 +115,24 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); renderBody = false; + + await add(_LaunchRampBaseSpriteComponent()); } +} + +class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); - Future _loadSprite() async { final sprite = await gameRef.loadSprite( Assets.images.launchRamp.ramp.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.center, - position: Vector2(25.65, 0), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(25.65, 0); } } @@ -192,23 +193,25 @@ class _LaunchRampForegroundRailing extends BodyComponent @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); renderBody = false; + + await add(_LaunchRampForegroundRailingSpriteComponent()); } +} + +class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); - Future _loadSprite() async { final sprite = await gameRef.loadSprite( Assets.images.launchRamp.foregroundRailing.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.center, - position: Vector2(22.8, 0), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(22.8, 0); } } diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index f1b58db0..10144eef 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -102,27 +102,9 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered { @override Future onLoad() async { await super.onLoad(); - renderBody = false; - final sprite = await gameRef.images.load( - Assets.images.spaceship.bridge.keyName, - ); - - await add( - SpriteAnimationComponent.fromFrameData( - sprite, - SpriteAnimationData.sequenced( - amount: 72, - amountPerRow: 24, - stepTime: 0.05, - textureSize: Vector2(82, 100), - ), - size: Vector2(8.2, 10), - position: Vector2(0, -2), - anchor: Anchor.center, - ), - ); + await add(_AndroidHeadSpriteAnimation()); } @override @@ -141,6 +123,29 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered { } } +class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + + final image = await gameRef.images.load( + Assets.images.spaceship.bridge.keyName, + ); + size = Vector2(8.2, 10); + position = Vector2(0, -2); + anchor = Anchor.center; + + final data = SpriteAnimationData.sequenced( + amount: 72, + amountPerRow: 24, + stepTime: 0.05, + textureSize: size * 10, + ); + animation = SpriteAnimation.fromFrameData(image, data); + } +} + /// {@template spaceship_entrance} /// A sensor [BodyComponent] used to detect when the ball enters the /// the spaceship area in order to modify its filter data so the ball @@ -228,8 +233,11 @@ class _SpaceshipWallShape extends ChainShape { /// {@template spaceship_wall} /// A [BodyComponent] that provides the collision for the wall -/// surrounding the spaceship, with a small opening to allow the -/// [Ball] to get inside the spaceship saucer. +/// surrounding the spaceship. +/// +/// It has a small opening to allow the [Ball] to get inside the spaceship +/// saucer. +/// /// It also contains the [SpriteComponent] for the lower wall /// {@endtemplate} class SpaceshipWall extends BodyComponent with InitialPosition, Layered { diff --git a/packages/pinball_components/lib/src/components/spaceship_rail.dart b/packages/pinball_components/lib/src/components/spaceship_rail.dart index ace12e61..2cc8bccc 100644 --- a/packages/pinball_components/lib/src/components/spaceship_rail.dart +++ b/packages/pinball_components/lib/src/components/spaceship_rail.dart @@ -139,21 +139,23 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); + await add(_SpaceshipRailRampSpriteComponent()); } +} + +class _SpaceshipRailRampSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); - Future _loadSprite() async { final sprite = await gameRef.loadSprite( Assets.images.spaceship.rail.main.keyName, ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: Vector2(17.5, 55.7), - anchor: Anchor.center, - position: Vector2(-29.4, -5.7), - ); - - await add(spriteComponent); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(-29.4, -5.7); } } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp.dart index 6fb6cda0..38c5c7df 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp.dart @@ -96,8 +96,6 @@ class _SpaceshipRampBackground extends BodyComponent @override Body createBody() { - renderBody = false; - final bodyDef = BodyDef() ..userData = this ..position = initialPosition; @@ -111,35 +109,40 @@ class _SpaceshipRampBackground extends BodyComponent @override Future onLoad() async { await super.onLoad(); - await _loadSprites(); - } - - Future _loadSprites() async { - final spriteRamp = await gameRef.loadSprite( - Assets.images.spaceship.ramp.main.keyName, - ); + renderBody = false; - final spriteRampComponent = SpriteComponent( - sprite: spriteRamp, - size: Vector2(38.1, 33.8), - anchor: Anchor.center, - position: Vector2(-12.2, -53.5), - ); + await add(_SpaceshipRampBackgroundRailingSpriteComponent()); + await add(_SpaceshipRampBackgroundRampSpriteComponent()); + } +} - final spriteRailingBg = await gameRef.loadSprite( +class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( Assets.images.spaceship.ramp.railingBackground.keyName, ); - final spriteRailingBgComponent = SpriteComponent( - sprite: spriteRailingBg, - size: Vector2(38.3, 35.1), - anchor: Anchor.center, - position: spriteRampComponent.position + Vector2(0, -1), - ); + this.sprite = sprite; + size = Vector2(38.3, 35.1); + anchor = Anchor.center; + position = Vector2(-12.2, -54.5); + } +} - await addAll([ - spriteRailingBgComponent, - spriteRampComponent, - ]); +class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + Assets.images.spaceship.ramp.main.keyName, + ); + this.sprite = sprite; + size = Vector2(38.3, 35.1); + anchor = Anchor.center; + position = Vector2(-12.2, -543.5); } } @@ -196,21 +199,22 @@ class _SpaceshipRampForegroundRailing extends BodyComponent @override Future onLoad() async { await super.onLoad(); - await _loadSprites(); + await add(_SpaceshipRampForegroundRalingSpriteComponent()); } +} - Future _loadSprites() async { - final spriteRailingFg = await gameRef.loadSprite( +class _SpaceshipRampForegroundRalingSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( Assets.images.spaceship.ramp.railingForeground.keyName, ); - final spriteRailingFgComponent = SpriteComponent( - sprite: spriteRailingFg, - size: Vector2(26.1, 28.3), - anchor: Anchor.center, - position: Vector2(-12.2, -52.5), - ); - - await add(spriteRailingFgComponent); + this.sprite = sprite; + size = Vector2(26.1, 28.3); + anchor = Anchor.center; + position = Vector2(-12.2, -52.5); } } diff --git a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart index 46cfb154..ee9fa88c 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; -class BasicBallGame extends BasicGame with TapDetector { +class BasicBallGame extends BasicGame with TapDetector, Traceable { BasicBallGame({required this.color}); static const info = ''' @@ -19,5 +19,6 @@ class BasicBallGame extends BasicGame with TapDetector { add( Ball(baseColor: color)..initialPosition = info.eventPosition.game, ); + traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart index 2e945c47..64892d22 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart @@ -12,7 +12,7 @@ void addBallStories(Dashbook dashbook) { (context) => GameWidget( game: BasicBallGame( color: context.colorProperty('color', Colors.blue), - ), + )..trace = context.boolProperty('Trace', true), ), codeLink: buildSourceLink('ball/basic.dart'), info: BasicBallGame.info, From d7216bbe6d6b41001f80f658329fe57729110de0 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Fri, 8 Apr 2022 12:59:03 +0200 Subject: [PATCH 4/5] fix: moved `DinoWalls` slightly to the right (#164) * fix: placed right some moved sprites * fix: added renderBody removed --- .../pinball_components/lib/src/components/dino_walls.dart | 4 ++-- .../pinball_components/lib/src/components/spaceship_ramp.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index 3c5d7f37..dc5b7a26 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -115,7 +115,7 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(27, -28.2); + position = Vector2(22, -41.8); } } @@ -215,6 +215,6 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(31.7, 18); + position = Vector2(23.8, -9.5); } } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp.dart index 38c5c7df..773b0441 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp.dart @@ -140,9 +140,9 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent Assets.images.spaceship.ramp.main.keyName, ); this.sprite = sprite; - size = Vector2(38.3, 35.1); + size = sprite.originalSize / 10; anchor = Anchor.center; - position = Vector2(-12.2, -543.5); + position = Vector2(-12.2, -53.5); } } From 5fe5fc007deedc35069c95a8ece513486cc6c6fd Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 8 Apr 2022 10:08:27 -0300 Subject: [PATCH 5/5] feat: adding assets manager (#163) --- .../cubit/assets_manager_cubit.dart | 27 +++ .../cubit/assets_manager_state.dart | 41 +++++ lib/game/game.dart | 1 + lib/game/game_assets.dart | 8 +- lib/game/view/pinball_game_page.dart | 130 ++++++++------ .../cubit/assets_manager_cubit_test.dart | 35 ++++ .../cubit/assets_manager_state_test.dart | 145 ++++++++++++++++ test/game/view/pinball_game_page_test.dart | 159 +++++++++++------- test/helpers/mocks.dart | 2 + test/helpers/pump_app.dart | 24 +++ 10 files changed, 449 insertions(+), 123 deletions(-) create mode 100644 lib/game/assets_manager/cubit/assets_manager_cubit.dart create mode 100644 lib/game/assets_manager/cubit/assets_manager_state.dart create mode 100644 test/game/assets_manager/cubit/assets_manager_cubit_test.dart create mode 100644 test/game/assets_manager/cubit/assets_manager_state_test.dart diff --git a/lib/game/assets_manager/cubit/assets_manager_cubit.dart b/lib/game/assets_manager/cubit/assets_manager_cubit.dart new file mode 100644 index 00000000..b97483d4 --- /dev/null +++ b/lib/game/assets_manager/cubit/assets_manager_cubit.dart @@ -0,0 +1,27 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'assets_manager_state.dart'; + +/// {@template assets_manager_cubit} +/// Cubit responsable for pre loading any game assets +/// {@endtemplate} +class AssetsManagerCubit extends Cubit { + /// {@macro assets_manager_cubit} + AssetsManagerCubit(List loadables) + : super( + AssetsManagerState.initial( + loadables: loadables, + ), + ); + + /// Loads the assets + Future load() async { + final all = state.loadables.map((loadable) async { + await loadable; + emit(state.copyWith(loaded: [...state.loaded, loadable])); + }).toList(); + + await Future.wait(all); + } +} diff --git a/lib/game/assets_manager/cubit/assets_manager_state.dart b/lib/game/assets_manager/cubit/assets_manager_state.dart new file mode 100644 index 00000000..8ef1e874 --- /dev/null +++ b/lib/game/assets_manager/cubit/assets_manager_state.dart @@ -0,0 +1,41 @@ +part of 'assets_manager_cubit.dart'; + +/// {@template assets_manager_state} +/// State used to load the game assets +/// {@endtemplate} +class AssetsManagerState extends Equatable { + /// {@macro assets_manager_state} + const AssetsManagerState({ + required this.loadables, + required this.loaded, + }); + + /// {@macro assets_manager_state} + const AssetsManagerState.initial({ + required List loadables, + }) : this(loadables: loadables, loaded: const []); + + /// List of futures to load + final List loadables; + + /// List of loaded futures + final List loaded; + + /// Returns a value between 0 and 1 to indicate the loading progress + double get progress => loaded.length / loadables.length; + + /// Returns a copy of this instance with the given parameters + /// updated + AssetsManagerState copyWith({ + List? loadables, + List? loaded, + }) { + return AssetsManagerState( + loadables: loadables ?? this.loadables, + loaded: loaded ?? this.loaded, + ); + } + + @override + List get props => [loaded, loadables]; +} diff --git a/lib/game/game.dart b/lib/game/game.dart index ad02533d..7de964eb 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -1,3 +1,4 @@ +export 'assets_manager/cubit/assets_manager_cubit.dart'; export 'bloc/game_bloc.dart'; export 'components/components.dart'; export 'game_assets.dart'; diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 050b2cd3..678e25b6 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -4,9 +4,9 @@ import 'package:pinball_components/pinball_components.dart' as components; /// Add methods to help loading and caching game assets. extension PinballGameAssetsX on PinballGame { - /// Pre load the initial assets of the game. - Future preLoadAssets() async { - await Future.wait([ + /// Returns a list of assets to be loaded + List preLoadAssets() { + return [ images.load(components.Assets.images.ball.keyName), images.load(components.Assets.images.flutterSignPost.keyName), images.load(components.Assets.images.flipper.left.keyName), @@ -47,6 +47,6 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.chromeDino.mouth.keyName), images.load(components.Assets.images.chromeDino.head.keyName), images.load(Assets.images.components.background.path), - ]); + ]; } } diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index e50eb2d7..b1f031c7 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -9,16 +9,41 @@ import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_theme/pinball_theme.dart'; class PinballGamePage extends StatelessWidget { - const PinballGamePage({Key? key, required this.theme}) : super(key: key); + const PinballGamePage({ + Key? key, + required this.theme, + required this.game, + }) : super(key: key); final PinballTheme theme; + final PinballGame game; - static Route route({required PinballTheme theme}) { + static Route route({ + required PinballTheme theme, + bool isDebugMode = kDebugMode, + }) { return MaterialPageRoute( - builder: (_) { - return BlocProvider( - create: (_) => GameBloc(), - child: PinballGamePage(theme: theme), + builder: (context) { + final audio = context.read(); + + final game = isDebugMode + ? DebugPinballGame(theme: theme, audio: audio) + : PinballGame(theme: theme, audio: audio); + + final pinballAudio = context.read(); + final loadables = [ + ...game.preLoadAssets(), + pinballAudio.load(), + ]; + + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => GameBloc()), + BlocProvider( + create: (_) => AssetsManagerCubit(loadables)..load(), + ), + ], + child: PinballGamePage(theme: theme, game: game), ); }, ); @@ -26,51 +51,19 @@ class PinballGamePage extends StatelessWidget { @override Widget build(BuildContext context) { - return PinballGameView(theme: theme); + return PinballGameView(theme: theme, game: game); } } -class PinballGameView extends StatefulWidget { +class PinballGameView extends StatelessWidget { const PinballGameView({ Key? key, required this.theme, - bool isDebugMode = kDebugMode, - }) : _isDebugMode = isDebugMode, - super(key: key); + required this.game, + }) : super(key: key); final PinballTheme theme; - final bool _isDebugMode; - - @override - State createState() => _PinballGameViewState(); -} - -class _PinballGameViewState extends State { - late PinballGame _game; - - @override - void initState() { - super.initState(); - - final audio = context.read(); - - _game = widget._isDebugMode - ? DebugPinballGame(theme: widget.theme, audio: audio) - : PinballGame(theme: widget.theme, audio: audio); - - // TODO(erickzanardo): Revisit this when we start to have more assets - // this could expose a Stream (maybe even a cubit?) so we could show the - // the loading progress with some fancy widgets. - _fetchAssets(); - } - - Future _fetchAssets() async { - final pinballAudio = context.read(); - await Future.wait([ - _game.preLoadAssets(), - pinballAudio.load(), - ]); - } + final PinballGame game; @override Widget build(BuildContext context) { @@ -84,24 +77,51 @@ class _PinballGameViewState extends State { builder: (_) { return GameOverDialog( score: state.score, - theme: widget.theme.characterTheme, + theme: theme.characterTheme, ); }, ); } }, - child: Stack( - children: [ - Positioned.fill( - child: GameWidget(game: _game), - ), - const Positioned( - top: 8, - left: 8, - child: GameHud(), + child: _GameView(game: game), + ); + } +} + +class _GameView extends StatelessWidget { + const _GameView({ + Key? key, + required PinballGame game, + }) : _game = game, + super(key: key); + + final PinballGame _game; + + @override + Widget build(BuildContext context) { + final loadingProgress = context.watch().state.progress; + + if (loadingProgress != 1) { + return Scaffold( + body: Center( + child: Text( + loadingProgress.toString(), ), - ], - ), + ), + ); + } + + return Stack( + children: [ + Positioned.fill( + child: GameWidget(game: _game), + ), + const Positioned( + top: 8, + left: 8, + child: GameHud(), + ), + ], ); } } diff --git a/test/game/assets_manager/cubit/assets_manager_cubit_test.dart b/test/game/assets_manager/cubit/assets_manager_cubit_test.dart new file mode 100644 index 00000000..d0afee34 --- /dev/null +++ b/test/game/assets_manager/cubit/assets_manager_cubit_test.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + group('AssetsManagerCubit', () { + final completer1 = Completer(); + final completer2 = Completer(); + + final future1 = completer1.future; + final future2 = completer2.future; + + blocTest( + 'emits the loaded on the order that they load', + build: () => AssetsManagerCubit([future1, future2]), + act: (cubit) { + cubit.load(); + completer2.complete(); + completer1.complete(); + }, + expect: () => [ + AssetsManagerState( + loadables: [future1, future2], + loaded: [future2], + ), + AssetsManagerState( + loadables: [future1, future2], + loaded: [future2, future1], + ), + ], + ); + }); +} diff --git a/test/game/assets_manager/cubit/assets_manager_state_test.dart b/test/game/assets_manager/cubit/assets_manager_state_test.dart new file mode 100644 index 00000000..12a42485 --- /dev/null +++ b/test/game/assets_manager/cubit/assets_manager_state_test.dart @@ -0,0 +1,145 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + group('AssetsManagerState', () { + test('can be instantiated', () { + expect( + AssetsManagerState(loadables: const [], loaded: const []), + isNotNull, + ); + }); + + test('has the correct initial state', () { + final future = Future.value(); + expect( + AssetsManagerState.initial(loadables: [future]), + equals( + AssetsManagerState( + loadables: [future], + loaded: const [], + ), + ), + ); + }); + + group('progress', () { + final future1 = Future.value(); + final future2 = Future.value(); + + test('returns 0 when no future is loaded', () { + expect( + AssetsManagerState( + loadables: [future1, future2], + loaded: const [], + ).progress, + equals(0), + ); + }); + + test('returns the correct value when some of the futures are loaded', () { + expect( + AssetsManagerState( + loadables: [future1, future2], + loaded: [future1], + ).progress, + equals(0.5), + ); + }); + + test('returns the 1 when all futures are loaded', () { + expect( + AssetsManagerState( + loadables: [future1, future2], + loaded: [future1, future2], + ).progress, + equals(1), + ); + }); + }); + + group('copyWith', () { + final future = Future.value(); + + test('returns a copy with the updated loadables', () { + expect( + AssetsManagerState( + loadables: const [], + loaded: const [], + ).copyWith(loadables: [future]), + equals( + AssetsManagerState( + loadables: [future], + loaded: const [], + ), + ), + ); + }); + + test('returns a copy with the updated loaded', () { + expect( + AssetsManagerState( + loadables: const [], + loaded: const [], + ).copyWith(loaded: [future]), + equals( + AssetsManagerState( + loadables: const [], + loaded: [future], + ), + ), + ); + }); + }); + + test('supports value comparison', () { + final future1 = Future.value(); + final future2 = Future.value(); + + expect( + AssetsManagerState( + loadables: const [], + loaded: const [], + ), + equals( + AssetsManagerState( + loadables: const [], + loaded: const [], + ), + ), + ); + + expect( + AssetsManagerState( + loadables: [future1], + loaded: const [], + ), + isNot( + equals( + AssetsManagerState( + loadables: [future2], + loaded: const [], + ), + ), + ), + ); + + expect( + AssetsManagerState( + loadables: const [], + loaded: [future1], + ), + isNot( + equals( + AssetsManagerState( + loadables: const [], + loaded: [future2], + ), + ), + ), + ); + }); + }); +} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index f16b8ef1..683b53e8 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -11,6 +11,7 @@ import '../../helpers/helpers.dart'; void main() { const theme = PinballTheme(characterTheme: DashTheme()); + final game = PinballGameTest(); group('PinballGamePage', () { testWidgets('renders PinballGameView', (tester) async { @@ -22,37 +23,107 @@ void main() { ); await tester.pumpApp( - PinballGamePage(theme: theme), + PinballGamePage(theme: theme, game: game), gameBloc: gameBloc, ); expect(find.byType(PinballGameView), findsOneWidget); }); - testWidgets('route returns a valid navigation route', (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context) - .push(PinballGamePage.route(theme: theme)); - }, - child: const Text('Tap me'), - ); - }, + testWidgets( + 'renders the loading indicator while the assets load', + (tester) async { + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); + + final assetsManagerCubit = MockAssetsManagerCubit(); + final initialAssetsState = AssetsManagerState( + loadables: [Future.value()], + loaded: const [], + ); + whenListen( + assetsManagerCubit, + Stream.value(initialAssetsState), + initialState: initialAssetsState, + ); + + await tester.pumpApp( + PinballGamePage(theme: theme, game: game), + gameBloc: gameBloc, + assetsManagerCubit: assetsManagerCubit, + ); + expect(find.text('0.0'), findsOneWidget); + + final loadedAssetsState = AssetsManagerState( + loadables: [Future.value()], + loaded: [Future.value()], + ); + whenListen( + assetsManagerCubit, + Stream.value(loadedAssetsState), + initialState: loadedAssetsState, + ); + + await tester.pump(); + expect(find.byType(PinballGameView), findsOneWidget); + }, + ); + + group('route', () { + Future pumpRoute({ + required WidgetTester tester, + required bool isDebugMode, + }) async { + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context).push( + PinballGamePage.route( + theme: theme, + isDebugMode: isDebugMode, + ), + ); + }, + child: const Text('Tap me'), + ); + }, + ), ), - ), - ); + ); - await tester.tap(find.text('Tap me')); + await tester.tap(find.text('Tap me')); - // We can't use pumpAndSettle here because the page renders a Flame game - // which is an infinity animation, so it will timeout - await tester.pump(); // Runs the button action - await tester.pump(); // Runs the navigation + // We can't use pumpAndSettle here because the page renders a Flame game + // which is an infinity animation, so it will timeout + await tester.pump(); // Runs the button action + await tester.pump(); // Runs the navigation + } - expect(find.byType(PinballGamePage), findsOneWidget); + testWidgets('route creates the correct non debug game', (tester) async { + await pumpRoute(tester: tester, isDebugMode: false); + expect( + find.byWidgetPredicate( + (w) => w is PinballGameView && w.game is! DebugPinballGame, + ), + findsOneWidget, + ); + }); + + testWidgets('route creates the correct debug game', (tester) async { + await pumpRoute(tester: tester, isDebugMode: true); + expect( + find.byWidgetPredicate( + (w) => w is PinballGameView && w.game is DebugPinballGame, + ), + findsOneWidget, + ); + }); }); }); @@ -66,7 +137,7 @@ void main() { ); await tester.pumpApp( - PinballGameView(theme: theme), + PinballGameView(theme: theme, game: game), gameBloc: gameBloc, ); @@ -99,7 +170,7 @@ void main() { ); await tester.pumpApp( - const PinballGameView(theme: theme), + PinballGameView(theme: theme, game: game), gameBloc: gameBloc, ); await tester.pump(); @@ -107,45 +178,5 @@ void main() { expect(find.byType(GameOverDialog), findsOneWidget); }, ); - - testWidgets('renders the real game when not in debug mode', (tester) async { - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(const GameState.initial()), - initialState: const GameState.initial(), - ); - - await tester.pumpApp( - const PinballGameView(theme: theme, isDebugMode: false), - gameBloc: gameBloc, - ); - expect( - find.byWidgetPredicate( - (w) => w is GameWidget && w.game is! DebugPinballGame, - ), - findsOneWidget, - ); - }); - - testWidgets('renders the debug game when on debug mode', (tester) async { - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(const GameState.initial()), - initialState: const GameState.initial(), - ); - - await tester.pumpApp( - const PinballGameView(theme: theme), - gameBloc: gameBloc, - ); - expect( - find.byWidgetPredicate( - (w) => w is GameWidget && w.game is DebugPinballGame, - ), - findsOneWidget, - ); - }); }); } diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 748b48f3..2da91a25 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -74,3 +74,5 @@ class MockComponentSet extends Mock implements ComponentSet {} class MockDashNestBumper extends Mock implements DashNestBumper {} class MockPinballAudio extends Mock implements PinballAudio {} + +class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 722dc44c..92e2c042 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -5,6 +5,7 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -26,11 +27,31 @@ PinballAudio _buildDefaultPinballAudio() { return audio; } +MockAssetsManagerCubit _buildDefaultAssetsManagerCubit() { + final cubit = MockAssetsManagerCubit(); + + final state = AssetsManagerState( + loadables: [Future.value()], + loaded: [ + Future.value(), + ], + ); + + whenListen( + cubit, + Stream.value(state), + initialState: state, + ); + + return cubit; +} + extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { MockNavigator? navigator, GameBloc? gameBloc, + AssetsManagerCubit? assetsManagerCubit, ThemeCubit? themeCubit, LeaderboardRepository? leaderboardRepository, PinballAudio? pinballAudio, @@ -54,6 +75,9 @@ extension PumpApp on WidgetTester { BlocProvider.value( value: gameBloc ?? MockGameBloc(), ), + BlocProvider.value( + value: assetsManagerCubit ?? _buildDefaultAssetsManagerCubit(), + ), ], child: MaterialApp( localizationsDelegates: const [