diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 5c722946..d08ba04b 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -55,9 +55,6 @@ class GameState extends Equatable { /// Determines when the game is over. bool get isGameOver => balls == 0; - /// Determines when the player has only one ball left. - bool get isLastBall => balls == 1; - /// Shortcut method to check if the given [i] /// is activated. bool isLetterActivated(int i) => activatedBonusLetters.contains(i); diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index 59ec01b9..e71d5ede 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -19,8 +19,8 @@ class Board extends Component { // TODO(alestiago): adjust positioning to real design. final dino = ChromeDino() ..initialPosition = Vector2( - PinballGame.boardBounds.center.dx + 25, - PinballGame.boardBounds.center.dy + 10, + BoardDimensions.bounds.center.dx + 25, + BoardDimensions.bounds.center.dy + 10, ); await addAll([ diff --git a/lib/game/components/chrome_dino.dart b/lib/game/components/chrome_dino.dart index dc280350..af086e0e 100644 --- a/lib/game/components/chrome_dino.dart +++ b/lib/game/components/chrome_dino.dart @@ -31,7 +31,7 @@ class ChromeDino extends BodyComponent with InitialPosition { anchor: anchor, ); final joint = _ChromeDinoJoint(jointDef); - world.createJoint2(joint); + world.createJoint(joint); return joint; } @@ -154,15 +154,3 @@ class _ChromeDinoJoint extends RevoluteJoint { setMotorSpeed(-motorSpeed); } } - -extension on World { - // TODO(alestiago): Remove once Forge2D supports custom joints. - void createJoint2(Joint joint) { - assert(!isLocked, ''); - - joints.add(joint); - - joint.bodyA.joints.add(joint); - joint.bodyB.joints.add(joint); - } -} diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 463c158f..257d4f1d 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,4 +1,5 @@ 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'; @@ -28,19 +29,19 @@ class ControlledBall extends Ball with Controls { ControlledBall.bonus({ required PinballTheme theme, }) : super(baseColor: theme.characterTheme.ballColor) { - controller = BallController(this); + controller = BonusBallController(this); } /// [Ball] used in [DebugPinballGame]. ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { - controller = BallController(this); + controller = BonusBallController(this); } } /// {@template ball_controller} /// Controller attached to a [Ball] that handles its game related logic. /// {@endtemplate} -class BallController extends ComponentController { +abstract class BallController extends ComponentController { /// {@macro ball_controller} BallController(Ball ball) : super(ball); @@ -50,30 +51,52 @@ class BallController extends ComponentController { /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into /// a [BottomWall]. /// {@endtemplate} - @mustCallSuper + 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 { - /// {@macro ball_controller} + 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; + } + + @override + void onNewState(GameState state) { + super.onNewState(state); + component.shouldRemove = true; + if (state.balls > 1) gameRef.spawnBall(); + } + /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if /// any are left. /// /// {@macro ball_controller_lost} @override void lost() { - super.lost(); - - final bloc = gameRef.read()..add(const BallLost()); - - // TODO(alestiago): Consider the use of onNewState instead. - final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver; - if (shouldBallRespwan) gameRef.spawnBall(); + gameRef.read().add(const BallLost()); } } diff --git a/lib/game/components/jetpack_ramp.dart b/lib/game/components/jetpack_ramp.dart index b58ddfa6..4c4c8be9 100644 --- a/lib/game/components/jetpack_ramp.dart +++ b/lib/game/components/jetpack_ramp.dart @@ -13,8 +13,8 @@ class Jetpack extends Forge2DBlueprint { @override void build(_) { final position = Vector2( - PinballGame.boardBounds.left + 40.5, - PinballGame.boardBounds.top - 31.5, + BoardDimensions.bounds.left + 40.5, + BoardDimensions.bounds.top - 31.5, ); addAllContactCallback([ diff --git a/lib/game/components/launcher_ramp.dart b/lib/game/components/launcher_ramp.dart index b3f3cb23..79b7c831 100644 --- a/lib/game/components/launcher_ramp.dart +++ b/lib/game/components/launcher_ramp.dart @@ -13,8 +13,8 @@ class Launcher extends Forge2DBlueprint { @override void build(_) { final position = Vector2( - PinballGame.boardBounds.right - 31.3, - PinballGame.boardBounds.bottom + 33, + BoardDimensions.bounds.right - 31.3, + BoardDimensions.bounds.bottom + 33, ); addAllContactCallback([ @@ -67,8 +67,8 @@ class LauncherRamp extends BodyComponent with InitialPosition, Layered { final rightStraightShape = EdgeShape() ..set( - startPosition..rotate(PinballGame.boardPerspectiveAngle), - endPosition..rotate(PinballGame.boardPerspectiveAngle), + startPosition..rotate(BoardDimensions.perspectiveAngle), + endPosition..rotate(BoardDimensions.perspectiveAngle), ); final rightStraightFixtureDef = FixtureDef(rightStraightShape); fixturesDef.add(rightStraightFixtureDef); diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 5703e525..60e29a4d 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,7 +1,6 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/services.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template plunger} @@ -26,10 +25,10 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { 1.35, 0.5, Vector2.zero(), - PinballGame.boardPerspectiveAngle, + BoardDimensions.perspectiveAngle, ); - final fixtureDef = FixtureDef(shape)..density = 20; + final fixtureDef = FixtureDef(shape)..density = 80; final bodyDef = BodyDef() ..position = initialPosition @@ -50,7 +49,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { /// The velocity's magnitude depends on how far the [Plunger] has been pulled /// from its original [initialPosition]. void _release() { - final velocity = (initialPosition.y - body.position.y) * 4; + final velocity = (initialPosition.y - body.position.y) * 5; body.linearVelocity = Vector2(0, velocity); } @@ -127,12 +126,12 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { plunger.body, anchor.body, anchor.body.position, - Vector2(18.6, PinballGame.boardBounds.height), + Vector2(18.6, BoardDimensions.bounds.height), ); enableLimit = true; lowerTranslation = double.negativeInfinity; enableMotor = true; - motorSpeed = 80; + motorSpeed = 1000; maxMotorForce = motorSpeed; collideConnected = true; } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 96522cbd..f5d03c80 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -3,7 +3,6 @@ import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/components/components.dart'; -import 'package:pinball/game/pinball_game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template wall} @@ -42,13 +41,12 @@ class Wall extends BodyComponent { /// Create top, left, and right [Wall]s for the game board. List createBoundaries(Forge2DGame game) { - final topLeft = - PinballGame.boardBounds.topLeft.toVector2() + Vector2(18.6, 0); - final bottomRight = PinballGame.boardBounds.bottomRight.toVector2(); + final topLeft = BoardDimensions.bounds.topLeft.toVector2() + Vector2(18.6, 0); + final bottomRight = BoardDimensions.bounds.bottomRight.toVector2(); final topRight = - PinballGame.boardBounds.topRight.toVector2() - Vector2(18.6, 0); - final bottomLeft = PinballGame.boardBounds.bottomLeft.toVector2(); + BoardDimensions.bounds.topRight.toVector2() - Vector2(18.6, 0); + final bottomLeft = BoardDimensions.bounds.bottomLeft.toVector2(); return [ Wall(start: topLeft, end: topRight), @@ -67,8 +65,8 @@ class BottomWall extends Wall { /// {@macro bottom_wall} BottomWall() : super( - start: PinballGame.boardBounds.bottomLeft.toVector2(), - end: PinballGame.boardBounds.bottomRight.toVector2(), + start: BoardDimensions.bounds.bottomLeft.toVector2(), + end: BoardDimensions.bounds.bottomRight.toVector2(), ); } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 514c589c..c2bbe8e0 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,6 +1,5 @@ // ignore_for_file: public_member_api_docs import 'dart:async'; -import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; @@ -22,15 +21,6 @@ class PinballGame extends Forge2DGame late final Plunger plunger; - static final boardSize = Vector2(101.6, 143.8); - static final boardBounds = Rect.fromCenter( - center: Offset.zero, - width: boardSize.x, - height: -boardSize.y, - ); - static final boardPerspectiveAngle = - -math.atan(18.6 / PinballGame.boardBounds.height); - @override void onAttach() { super.onAttach(); @@ -80,7 +70,8 @@ class PinballGame extends Forge2DGame Future _addPlunger() async { plunger = Plunger(compressionDistance: 29) - ..initialPosition = boardBounds.center.toVector2() + Vector2(41.5, -49); + ..initialPosition = + BoardDimensions.bounds.center.toVector2() + Vector2(41.5, -49); await add(plunger); } @@ -88,8 +79,8 @@ class PinballGame extends Forge2DGame await add( BonusWord( position: Vector2( - boardBounds.center.dx - 3.07, - boardBounds.center.dy - 2.4, + BoardDimensions.bounds.center.dx - 3.07, + BoardDimensions.bounds.center.dy - 2.4, ), ), ); diff --git a/packages/pinball_components/assets/images/ball.png b/packages/pinball_components/assets/images/ball.png index af80811b..43332c9a 100644 Binary files a/packages/pinball_components/assets/images/ball.png and b/packages/pinball_components/assets/images/ball.png differ diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index 96e0bf9d..b62ceeba 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -23,13 +23,14 @@ class Ball extends BodyComponent } /// The size of the [Ball] - static final Vector2 size = Vector2.all(3); + static final Vector2 size = Vector2.all(4.5); /// The base [Color] used to tint this [Ball] final Color baseColor; double _boostTimer = 0; static const _boostDuration = 2.0; + late SpriteComponent _spriteComponent; @override Future onLoad() async { @@ -37,9 +38,9 @@ class Ball extends BodyComponent final sprite = await gameRef.loadSprite(Assets.images.ball.keyName); final tint = baseColor.withOpacity(0.5); await add( - SpriteComponent( + _spriteComponent = SpriteComponent( sprite: sprite, - size: size, + size: size * 1.15, anchor: Anchor.center, )..tint(tint), ); @@ -88,6 +89,8 @@ class Ball extends BodyComponent unawaited(gameRef.add(effect)); } + + _rescale(); } /// Applies a boost on this [Ball]. @@ -95,4 +98,18 @@ class Ball extends BodyComponent body.applyLinearImpulse(impulse); _boostTimer = _boostDuration; } + + void _rescale() { + final boardHeight = BoardDimensions.size.y; + const maxShrinkAmount = BoardDimensions.perspectiveShrinkFactor; + + final adjustedYPosition = body.position.y + (boardHeight / 2); + + final scaleFactor = ((boardHeight - adjustedYPosition) / + BoardDimensions.shrinkAdjustedHeight) + + maxShrinkAmount; + + body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor; + _spriteComponent.scale = Vector2.all(scaleFactor); + } } diff --git a/packages/pinball_components/lib/src/components/board_dimensions.dart b/packages/pinball_components/lib/src/components/board_dimensions.dart new file mode 100644 index 00000000..b4db8c3c --- /dev/null +++ b/packages/pinball_components/lib/src/components/board_dimensions.dart @@ -0,0 +1,29 @@ +import 'dart:math' as math; + +import 'package:flame/extensions.dart'; + +/// {@template board_dimensions} +/// Contains various board properties and dimensions for global use. +/// {@endtemplate} +// TODO(allisonryan0002): consider alternatives for global dimensions. +class BoardDimensions { + /// Width and height of the board. + static final size = Vector2(101.6, 143.8); + + /// [Rect] for easier access to board boundaries. + static final bounds = Rect.fromCenter( + center: Offset.zero, + width: size.x, + height: -size.y, + ); + + /// 3D perspective angle of the board in radians. + static final perspectiveAngle = -math.atan(18.6 / bounds.height); + + /// Factor the board shrinks by from the closest point to the farthest. + static const perspectiveShrinkFactor = 0.63; + + /// Board height based on the [perspectiveShrinkFactor]. + static final shrinkAdjustedHeight = + (1 / (1 - perspectiveShrinkFactor)) * size.y; +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 72e13246..a475f91e 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,5 +1,6 @@ export 'ball.dart'; export 'baseboard.dart'; +export 'board_dimensions.dart'; export 'board_side.dart'; export 'fire_effect.dart'; export 'flipper.dart'; diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index de5f18c8..49bd6d6f 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -68,7 +68,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { anchor: anchor, ); final joint = _FlipperJoint(jointDef); - world.createJoint2(joint); + world.createJoint(joint); unawaited(mounted.whenComplete(joint.unlock)); } @@ -219,15 +219,3 @@ class _FlipperJoint extends RevoluteJoint { setLimits(-angle, angle); } } - -// TODO(alestiago): Remove once Forge2D supports custom joints. -extension on World { - void createJoint2(Joint joint) { - assert(!isLocked, ''); - - joints.add(joint); - - joint.bodyA.joints.add(joint); - joint.bodyB.joints.add(joint); - } -} diff --git a/packages/pinball_components/test/src/components/ball_test.dart b/packages/pinball_components/test/src/components/ball_test.dart index a9eb05ad..f2a54c68 100644 --- a/packages/pinball_components/test/src/components/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball_test.dart @@ -86,7 +86,7 @@ void main() { final fixture = ball.body.fixtures[0]; expect(fixture.shape.shapeType, equals(ShapeType.circle)); - expect(fixture.shape.radius, equals(1.5)); + expect(fixture.shape.radius, equals(2.25)); }, ); diff --git a/packages/pinball_components/test/src/components/board_dimensions_test.dart b/packages/pinball_components/test/src/components/board_dimensions_test.dart new file mode 100644 index 00000000..afd4a2d8 --- /dev/null +++ b/packages/pinball_components/test/src/components/board_dimensions_test.dart @@ -0,0 +1,27 @@ +import 'package:flame/extensions.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('BoardDimensions', () { + test('has size', () { + expect(BoardDimensions.size, equals(Vector2(101.6, 143.8))); + }); + + test('has bounds', () { + expect(BoardDimensions.bounds, isNotNull); + }); + + test('has perspectiveAngle', () { + expect(BoardDimensions.perspectiveAngle, isNotNull); + }); + + test('has perspectiveShrinkFactor', () { + expect(BoardDimensions.perspectiveShrinkFactor, equals(0.63)); + }); + + test('has shrinkAdjustedHeight', () { + expect(BoardDimensions.shrinkAdjustedHeight, isNotNull); + }); + }); +} diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 9ca913ab..ed80d192 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -103,38 +103,6 @@ void main() { }); }); - group('isLastBall', () { - test( - 'is true ' - 'when there is only one ball left', - () { - const gameState = GameState( - balls: 1, - score: 0, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ); - expect(gameState.isLastBall, isTrue); - }, - ); - - test( - 'is false ' - 'when there are more balls left', - () { - const gameState = GameState( - balls: 2, - score: 0, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ); - expect(gameState.isLastBall, isFalse); - }, - ); - }); - group('isLetterActivated', () { test( 'is true when the letter is activated', diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index f02adceb..f48d60ee 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -196,7 +196,7 @@ void main() { group('bonus letter activation', () { late GameBloc gameBloc; - final tester = flameBlocTester( + final tester = flameBlocTester( // TODO(alestiago): Use TestGame once BonusLetter has controller. game: PinballGameTest.create, gameBloc: () => gameBloc, @@ -217,13 +217,8 @@ void main() { await game.ready(); final bonusLetter = game.descendants().whereType().first; - await game.add(bonusLetter); - await game.ready(); - bonusLetter.activate(); await game.ready(); - - await tester.pump(); }, verify: (game, tester) async { verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1); diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index 9cf1dd7e..dcd075ca 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -15,20 +15,26 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.create); - group('BallController', () { + 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 = BallController(ball); - await ball.add(controller); - await game.ready(); + final controller = BonusBallController(ball); + await ball.ensureAdd(controller); controller.lost(); await game.ready(); @@ -39,13 +45,20 @@ void main() { }); group('LaunchedBallController', () { - group('lost', () { - late GameBloc gameBloc; + test('can be instantiated', () { + expect( + LaunchedBallController(MockBall()), + isA(), + ); + }); + + group('description', () { late Ball ball; + late GameBloc gameBloc; setUp(() { - gameBloc = MockGameBloc(); ball = Ball(baseColor: const Color(0xFF00FFFF)); + gameBloc = MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -59,81 +72,126 @@ void main() { ); tester.testGameWidget( - 'removes ball', - verify: (game, tester) async { - await game.add(ball); + 'lost adds BallLost to GameBloc', + setUp: (game, tester) async { final controller = LaunchedBallController(ball); await ball.add(controller); - await game.ready(); + await game.ensureAdd(ball); controller.lost(); - await game.ready(); - - expect(game.contains(ball), isFalse); }, - ); - - tester.testGameWidget( - 'adds BallLost to GameBloc', verify: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.add(ball); - await game.ready(); - - controller.lost(); - verify(() => gameBloc.add(const BallLost())).called(1); }, ); - tester.testGameWidget( - 'adds a new ball if the game is not over', - verify: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.add(ball); - await game.ready(); + group('listenWhen', () { + tester.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); + }, + ); - final previousBalls = game.descendants().whereType().length; - controller.lost(); - await game.ready(); - final currentBalls = game.descendants().whereType().length; + tester.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); + }, + ); + }); - expect(previousBalls, equals(currentBalls)); - }, - ); + group('onNewState', () { + tester.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); + }, + ); - tester.testGameWidget( - 'no ball is added on game over', - verify: (game, tester) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState( - score: 10, - balls: 1, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ), - ); - final controller = BallController(ball); - await ball.add(controller); - await game.add(ball); - await game.ready(); + tester.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 previousBalls = game.descendants().whereType().toList(); - controller.lost(); - await game.ready(); - final currentBalls = game.descendants().whereType().length; + final state = MockGameState(); + when(() => state.balls).thenReturn(2); - expect( - currentBalls, - equals((previousBalls..remove(ball)).length), - ); - }, - ); + 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.length)); + }, + ); + + tester.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(1); + + 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/helpers/test_game.dart b/test/helpers/test_game.dart index a1219868..3c6ff42f 100644 --- a/test/helpers/test_game.dart +++ b/test/helpers/test_game.dart @@ -1,6 +1,7 @@ +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class TestGame extends Forge2DGame { +class TestGame extends Forge2DGame with FlameBloc { TestGame() { images.prefix = ''; }