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 f022862c..e71d5ede 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -12,22 +12,15 @@ class Board extends Component { @override Future onLoad() async { - // TODO(alestiago): adjust positioning once sprites are added. - final bottomGroup = _BottomGroup( - position: Vector2( - PinballGame.boardBounds.center.dx, - PinballGame.boardBounds.bottom + 10, - ), - spacing: 2, - ); + final bottomGroup = _BottomGroup(); final flutterForest = FlutterForest(); // 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([ @@ -46,27 +39,15 @@ class Board extends Component { // TODO(alestiago): Consider renaming once entire Board is defined. class _BottomGroup extends Component { /// {@macro bottom_group} - _BottomGroup({ - required this.position, - required this.spacing, - }); - - /// The amount of space between the line of symmetry. - final double spacing; - - /// The position of this [_BottomGroup]. - final Vector2 position; + _BottomGroup(); @override Future onLoad() async { - final spacing = this.spacing + Flipper.size.x / 2; final rightSide = _BottomGroupSide( side: BoardSide.right, - position: position + Vector2(spacing, 0), ); final leftSide = _BottomGroupSide( side: BoardSide.left, - position: position + Vector2(-spacing, 0), ); await addAll([rightSide, leftSide]); @@ -82,36 +63,29 @@ class _BottomGroupSide extends Component { /// {@macro bottom_group_side} _BottomGroupSide({ required BoardSide side, - required Vector2 position, - }) : _side = side, - _position = position; + }) : _side = side; final BoardSide _side; - final Vector2 _position; - @override Future onLoad() async { final direction = _side.direction; + final centerXAdjustment = _side.isLeft ? 0 : -6.5; final flipper = ControlledFlipper( side: _side, - )..initialPosition = _position; - + )..initialPosition = Vector2((11.0 * direction) + centerXAdjustment, -42.4); final baseboard = Baseboard(side: _side) - ..initialPosition = _position + - Vector2( - (Baseboard.size.x / 1.6 * direction), - Baseboard.size.y - 2, - ); - + ..initialPosition = Vector2( + (25.58 * direction) + centerXAdjustment, + -28.69, + ); final kicker = Kicker( side: _side, - )..initialPosition = _position + - Vector2( - (Flipper.size.x) * direction, - Flipper.size.y + Kicker.size.y, - ); + )..initialPosition = Vector2( + (22.0 * direction) + centerXAdjustment, + -26, + ); await addAll([flipper, baseboard, kicker]); } 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/components.dart b/lib/game/components/components.dart index 1f1f1ce5..e19c607c 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,4 +1,3 @@ -export 'baseboard.dart'; export 'board.dart'; export 'bonus_word.dart'; export 'chrome_dino.dart'; 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 dc41e5f0..de36c080 100644 --- a/lib/game/components/jetpack_ramp.dart +++ b/lib/game/components/jetpack_ramp.dart @@ -21,6 +21,11 @@ class Jetpack extends Forge2DBlueprint { @override void build(_) { + final position = Vector2( + BoardDimensions.bounds.left + 40.5, + BoardDimensions.bounds.top - 31.5, + ); + addAllContactCallback([ RampOpeningBallContactCallback<_JetpackRampOpening>(), ]); @@ -250,10 +255,10 @@ class _JetpackRampOpening extends RampOpening { required double rotation, }) : _rotation = rotation, super( - pathwayLayer: Layer.jetpack, + insideLayer: Layer.jetpack, outsideLayer: outsideLayer, orientation: RampOrientation.down, - pathwayPriority: Jetpack.ballPriorityInsideRamp, + insidePriority: Jetpack.ballPriorityInsideRamp, outsidePriority: outsidePriority, ); diff --git a/lib/game/components/launcher_ramp.dart b/lib/game/components/launcher_ramp.dart index ad94601d..44f5c7bb 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); @@ -122,7 +122,7 @@ class _LauncherRampOpening extends RampOpening { required double rotation, }) : _rotation = rotation, super( - pathwayLayer: Layer.launcher, + insideLayer: Layer.launcher, orientation: RampOrientation.down, pathwayPriority: 3, ); 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/spaceship_exit_rail.dart b/lib/game/components/spaceship_exit_rail.dart index 617f092a..262f1cc3 100644 --- a/lib/game/components/spaceship_exit_rail.dart +++ b/lib/game/components/spaceship_exit_rail.dart @@ -169,9 +169,9 @@ class SpaceshipExitRailEnd extends RampOpening { /// {@macro spaceship_exit_rail_end} SpaceshipExitRailEnd() : super( - pathwayLayer: Layer.spaceshipExitRail, + insideLayer: Layer.spaceshipExitRail, orientation: RampOrientation.down, - pathwayPriority: 3, + insidePriority: 3, ) { layer = Layer.spaceshipExitRail; } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 96522cbd..030edc50 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -2,9 +2,8 @@ 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'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@template wall} /// A continuous generic and [BodyType.static] barrier that divides a game area. @@ -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/game_assets.dart b/lib/game/game_assets.dart index 4aabbe54..8b67c65b 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -11,6 +11,10 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.flutterSignPost.keyName), images.load(components.Assets.images.flipper.left.keyName), images.load(components.Assets.images.flipper.right.keyName), + images.load(components.Assets.images.baseboard.left.keyName), + images.load(components.Assets.images.baseboard.right.keyName), + images.load(components.Assets.images.dino.dinoLandTop.keyName), + images.load(components.Assets.images.dino.dinoLandBottom.keyName), images.load(Assets.images.components.background.path), images.load(Assets.images.components.spaceshipRamp.path), images.load(Assets.images.components.spaceshipRailingBg.path), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 7c5474ce..cfda7241 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(); @@ -76,11 +66,19 @@ class PinballGame extends Forge2DGame Future _addGameBoundaries() async { await add(BottomWall()); createBoundaries(this).forEach(add); + unawaited( + addFromBlueprint( + DinoWalls( + position: Vector2(-2.4, 0), + ), + ), + ); } 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 +86,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/geometry/lib/src/geometry.dart b/packages/geometry/lib/src/geometry.dart index 6975f8cb..edc176e7 100644 --- a/packages/geometry/lib/src/geometry.dart +++ b/packages/geometry/lib/src/geometry.dart @@ -82,7 +82,7 @@ List calculateEllipse({ /// For more information read: https://en.wikipedia.org/wiki/B%C3%A9zier_curve List calculateBezierCurve({ required List controlPoints, - double step = 0.001, + double step = 0.01, }) { assert( 0 <= step && step <= 1, diff --git a/packages/geometry/test/src/geometry_test.dart b/packages/geometry/test/src/geometry_test.dart index 7a49b2b2..e702f043 100644 --- a/packages/geometry/test/src/geometry_test.dart +++ b/packages/geometry/test/src/geometry_test.dart @@ -98,14 +98,14 @@ void main() { ); }); - test('returns by default 1000 points as indicated by step', () { + test('returns by default 100 points as indicated by step', () { final points = calculateBezierCurve( controlPoints: [ Vector2(0, 0), Vector2(10, 10), ], ); - expect(points.length, 1000); + expect(points.length, 100); }); test('returns as many points as indicated by step', () { @@ -114,9 +114,9 @@ void main() { Vector2(0, 0), Vector2(10, 10), ], - step: 0.01, + step: 0.001, ); - expect(points.length, 100); + expect(points.length, 1000); }); }); 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/assets/images/baseboard/left.png b/packages/pinball_components/assets/images/baseboard/left.png new file mode 100644 index 00000000..17253554 Binary files /dev/null and b/packages/pinball_components/assets/images/baseboard/left.png differ diff --git a/packages/pinball_components/assets/images/baseboard/right.png b/packages/pinball_components/assets/images/baseboard/right.png new file mode 100644 index 00000000..081a1782 Binary files /dev/null and b/packages/pinball_components/assets/images/baseboard/right.png differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-bottom.png b/packages/pinball_components/assets/images/dino/dino-land-bottom.png new file mode 100644 index 00000000..1839dda3 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/dino-land-bottom.png differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-top.png b/packages/pinball_components/assets/images/dino/dino-land-top.png new file mode 100644 index 00000000..85c2619a Binary files /dev/null and b/packages/pinball_components/assets/images/dino/dino-land-top.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 54b0ff53..8bd651ed 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -13,6 +13,8 @@ class $AssetsImagesGen { /// File path: assets/images/ball.png AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); + $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); + $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); /// File path: assets/images/flutter_sign_post.png @@ -28,6 +30,30 @@ class $AssetsImagesGen { const AssetGenImage('assets/images/spaceship_saucer.png'); } +class $AssetsImagesBaseboardGen { + const $AssetsImagesBaseboardGen(); + + /// File path: assets/images/baseboard/left.png + AssetGenImage get left => + const AssetGenImage('assets/images/baseboard/left.png'); + + /// File path: assets/images/baseboard/right.png + AssetGenImage get right => + const AssetGenImage('assets/images/baseboard/right.png'); +} + +class $AssetsImagesDinoGen { + const $AssetsImagesDinoGen(); + + /// File path: assets/images/dino/dino-land-bottom.png + AssetGenImage get dinoLandBottom => + const AssetGenImage('assets/images/dino/dino-land-bottom.png'); + + /// File path: assets/images/dino/dino-land-top.png + AssetGenImage get dinoLandTop => + const AssetGenImage('assets/images/dino/dino-land-top.png'); +} + class $AssetsImagesFlipperGen { const $AssetsImagesFlipperGen(); 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/lib/game/components/baseboard.dart b/packages/pinball_components/lib/src/components/baseboard.dart similarity index 59% rename from lib/game/components/baseboard.dart rename to packages/pinball_components/lib/src/components/baseboard.dart index cdad23fc..0a6bcc91 100644 --- a/lib/game/components/baseboard.dart +++ b/packages/pinball_components/lib/src/components/baseboard.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -12,9 +13,6 @@ class Baseboard extends BodyComponent with InitialPosition { required BoardSide side, }) : _side = side; - /// The size of the [Baseboard]. - static final size = Vector2(24.2, 13.5); - /// Whether the [Baseboard] is on the left or right side of the board. final BoardSide _side; @@ -24,50 +22,55 @@ class Baseboard extends BodyComponent with InitialPosition { final arcsAngle = -1.11 * direction; const arcsRotation = math.pi / 2.08; + final pegBumperShape = CircleShape()..radius = 0.7; + pegBumperShape.position.setValues(11.11 * direction, 7.15); + final pegBumperFixtureDef = FixtureDef(pegBumperShape); + fixturesDef.add(pegBumperFixtureDef); + final topCircleShape = CircleShape()..radius = 0.7; - topCircleShape.position.setValues(11.39 * direction, 6.05); + topCircleShape.position.setValues(9.71 * direction, 4.95); final topCircleFixtureDef = FixtureDef(topCircleShape); fixturesDef.add(topCircleFixtureDef); final innerEdgeShape = EdgeShape() ..set( - Vector2(10.86 * direction, 6.45), - Vector2(6.96 * direction, 0.25), + Vector2(9.01 * direction, 5.35), + Vector2(5.29 * direction, -0.95), ); final innerEdgeShapeFixtureDef = FixtureDef(innerEdgeShape); fixturesDef.add(innerEdgeShapeFixtureDef); final outerEdgeShape = EdgeShape() ..set( - Vector2(11.96 * direction, 5.85), - Vector2(5.48 * direction, -4.85), + Vector2(10.41 * direction, 4.75), + Vector2(3.79 * direction, -5.95), ); final outerEdgeShapeFixtureDef = FixtureDef(outerEdgeShape); fixturesDef.add(outerEdgeShapeFixtureDef); final upperArcShape = ArcShape( - center: Vector2(1.76 * direction, 3.25), + center: Vector2(0.09 * direction, 2.15), arcRadius: 6.1, angle: arcsAngle, rotation: arcsRotation, ); - final upperArcFixtureDefs = FixtureDef(upperArcShape); - fixturesDef.add(upperArcFixtureDefs); + final upperArcFixtureDef = FixtureDef(upperArcShape); + fixturesDef.add(upperArcFixtureDef); final lowerArcShape = ArcShape( - center: Vector2(1.85 * direction, -2.15), + center: Vector2(0.09 * direction, -3.35), arcRadius: 4.5, angle: arcsAngle, rotation: arcsRotation, ); - final lowerArcFixtureDefs = FixtureDef(lowerArcShape); - fixturesDef.add(lowerArcFixtureDefs); + final lowerArcFixtureDef = FixtureDef(lowerArcShape); + fixturesDef.add(lowerArcFixtureDef); final bottomRectangle = PolygonShape() ..setAsBox( - 7, + 6.8, 2, - Vector2(-5.14 * direction, -4.75), + Vector2(-6.3 * direction, -5.85), 0, ); final bottomRectangleFixtureDef = FixtureDef(bottomRectangle); @@ -76,11 +79,31 @@ class Baseboard extends BodyComponent with InitialPosition { return fixturesDef; } + @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; + } + @override Body createBody() { - // TODO(allisonryan0002): share sweeping angle with flipper when components - // are grouped. - const angle = math.pi / 5; + const angle = 37.1 * (math.pi / 180); final bodyDef = BodyDef() ..position = initialPosition 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 c29f91a3..4e38c2c4 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,5 +1,8 @@ export 'ball.dart'; +export 'baseboard.dart'; +export 'board_dimensions.dart'; export 'board_side.dart'; +export 'dino_walls.dart'; export 'fire_effect.dart'; export 'flipper.dart'; export 'flutter_sign_post.dart'; diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart new file mode 100644 index 00000000..13f56ff3 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -0,0 +1,225 @@ +// ignore_for_file: comment_references, avoid_renaming_method_parameters + +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; + +/// {@template dinowalls} +/// A [Blueprint] which creates walls for the [ChromeDino]. +/// {@endtemplate} +class DinoWalls extends Forge2DBlueprint { + /// {@macro dinowalls} + DinoWalls({required this.position}); + + /// The [position] where the elements will be created + final Vector2 position; + + @override + void build(_) { + addAll([ + _DinoTopWall()..initialPosition = position, + _DinoBottomWall()..initialPosition = position, + ]); + } +} + +/// {@template dino_top_wall} +/// Wall segment located above [ChromeDino]. +/// {@endtemplate} +class _DinoTopWall extends BodyComponent with InitialPosition { + ///{@macro dino_top_wall} + _DinoTopWall() : super(priority: 2); + + List _createFixtureDefs() { + final fixturesDef = []; + + final topStraightShape = EdgeShape() + ..set( + Vector2(29.5, 35.1), + Vector2(28.4, 35.1), + ); + final topStraightFixtureDef = FixtureDef(topStraightShape); + fixturesDef.add(topStraightFixtureDef); + + final topCurveShape = BezierCurveShape( + controlPoints: [ + topStraightShape.vertex1, + Vector2(17.4, 26.38), + Vector2(25.5, 20.7), + ], + ); + fixturesDef.add(FixtureDef(topCurveShape)); + + final middleCurveShape = BezierCurveShape( + controlPoints: [ + topCurveShape.vertices.last, + Vector2(27.8, 20.1), + Vector2(26.8, 19.5), + ], + ); + fixturesDef.add(FixtureDef(middleCurveShape)); + + final bottomCurveShape = BezierCurveShape( + controlPoints: [ + middleCurveShape.vertices.last, + Vector2(21.15, 16), + Vector2(25.6, 15.2), + ], + ); + fixturesDef.add(FixtureDef(bottomCurveShape)); + + final bottomStraightShape = EdgeShape() + ..set( + bottomCurveShape.vertices.last, + Vector2(31, 14.5), + ); + final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); + fixturesDef.add(bottomStraightFixtureDef); + + return fixturesDef; + } + + @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( + fixture + ..restitution = 0.1 + ..friction = 0, + ), + ); + + return body; + } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprite(); + } + + Future _loadSprite() async { + 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); + } +} + +/// {@template dino_bottom_wall} +/// Wall segment located below [ChromeDino]. +/// {@endtemplate} +class _DinoBottomWall extends BodyComponent with InitialPosition { + ///{@macro dino_top_wall} + _DinoBottomWall() : super(priority: 2); + + List _createFixtureDefs() { + final fixturesDef = []; + + final topStraightControlPoints = [ + Vector2(32.4, 8.3), + Vector2(25, 7.7), + ]; + final topStraightShape = EdgeShape() + ..set( + topStraightControlPoints.first, + topStraightControlPoints.last, + ); + final topStraightFixtureDef = FixtureDef(topStraightShape); + fixturesDef.add(topStraightFixtureDef); + + final topLeftCurveControlPoints = [ + topStraightControlPoints.last, + Vector2(21.8, 7), + Vector2(29.5, -13.8), + ]; + final topLeftCurveShape = BezierCurveShape( + controlPoints: topLeftCurveControlPoints, + ); + fixturesDef.add(FixtureDef(topLeftCurveShape)); + + final bottomLeftStraightControlPoints = [ + topLeftCurveControlPoints.last, + Vector2(31.8, -44.1), + ]; + final bottomLeftStraightShape = EdgeShape() + ..set( + bottomLeftStraightControlPoints.first, + bottomLeftStraightControlPoints.last, + ); + final bottomLeftStraightFixtureDef = FixtureDef(bottomLeftStraightShape); + fixturesDef.add(bottomLeftStraightFixtureDef); + + final bottomStraightControlPoints = [ + bottomLeftStraightControlPoints.last, + Vector2(37.8, -44.1), + ]; + final bottomStraightShape = EdgeShape() + ..set( + bottomStraightControlPoints.first, + bottomStraightControlPoints.last, + ); + final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); + fixturesDef.add(bottomStraightFixtureDef); + + return fixturesDef; + } + + @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( + fixture + ..restitution = 0.1 + ..friction = 0, + ), + ); + + return body; + } + + @override + Future onLoad() async { + await super.onLoad(); + await _loadSprite(); + } + + Future _loadSprite() async { + 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); + } +} 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/lib/src/components/ramp_opening.dart b/packages/pinball_components/lib/src/components/ramp_opening.dart index cf7e611c..cb6066f2 100644 --- a/packages/pinball_components/lib/src/components/ramp_opening.dart +++ b/packages/pinball_components/lib/src/components/ramp_opening.dart @@ -20,38 +20,39 @@ enum RampOrientation { /// [RampOpeningBallContactCallback] detects when a [Ball] passes /// through this opening. /// -/// By default the base [layer] is set to [Layer.board]. +/// By default the base [layer] is set to [Layer.board] and the +/// [outsidePriority] is set to the lowest possible [Layer]. /// {@endtemplate} // TODO(ruialonso): Consider renaming the class. abstract class RampOpening extends BodyComponent with InitialPosition, Layered { /// {@macro ramp_opening} RampOpening({ - required Layer pathwayLayer, - required int pathwayPriority, + required Layer insideLayer, Layer? outsideLayer, + int? insidePriority, int? outsidePriority, required this.orientation, - }) : _pathwayLayer = pathwayLayer, + }) : _insideLayer = insideLayer, _outsideLayer = outsideLayer ?? Layer.board, - _pathwayPriority = pathwayPriority, - _outsidePriority = outsidePriority ?? 1 { - layer = Layer.board; + _insidePriority = insidePriority ?? 0, + _outsidePriority = outsidePriority ?? 0 { + layer = Layer.opening; } - final int _pathwayPriority; - final int _outsidePriority; - final Layer _pathwayLayer; + final Layer _insideLayer; final Layer _outsideLayer; + final int _insidePriority; + final int _outsidePriority; - /// Mask of category bits for collision inside pathway. - Layer get pathwayLayer => _pathwayLayer; + /// Mask of category bits for collision inside ramp. + Layer get insideLayer => _insideLayer; - /// Mask of category bits for collision outside pathway. + /// Mask of category bits for collision outside ramp. Layer get outsideLayer => _outsideLayer; - /// Mask of category bits for collision outside pathway. - int get pathwayPriority => _pathwayPriority; + /// Priority for the [Ball] inside ramp. + int get insidePriority => _insidePriority; - /// Mask of category bits for collision outside pathway. + /// Priority for the [Ball] outside ramp. int get outsidePriority => _outsidePriority; /// The [Shape] of the [RampOpening]. @@ -76,8 +77,7 @@ abstract class RampOpening extends BodyComponent with InitialPosition, Layered { } /// {@template ramp_opening_ball_contact_callback} -/// Detects when a [Ball] enters or exits a pathway ramp through a -/// [RampOpening]. +/// Detects when a [Ball] enters or exits a ramp through a [RampOpening]. /// /// Modifies [Ball]'s [Layer] accordingly depending on whether the [Ball] is /// outside or inside a ramp. @@ -92,11 +92,11 @@ class RampOpeningBallContactCallback Layer layer; if (!_ballsInside.contains(ball)) { - layer = opening.pathwayLayer; + layer = opening.insideLayer; _ballsInside.add(ball); ball - ..layer = layer - ..priority = opening.pathwayPriority; + ..sendTo(opening.insidePriority) + ..layer = layer; } else { _ballsInside.remove(ball); } @@ -118,8 +118,8 @@ class RampOpeningBallContactCallback if (isBallOutsideOpening) { ball - ..layer = opening.outsideLayer - ..priority = opening.outsidePriority; + ..sendTo(opening.outsidePriority) + ..layer = opening.outsideLayer; _ballsInside.remove(ball); } } diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index 86a1616b..78c6a990 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -21,6 +21,9 @@ class Spaceship extends Forge2DBlueprint { /// The [position] where the elements will be created final Vector2 position; + /// Base priority for wall while be on spaceship. + static const ballPriorityWhenOnSpaceship = 4; + @override void build(_) { addAllContactCallback([ @@ -33,8 +36,8 @@ class Spaceship extends Forge2DBlueprint { SpaceshipEntrance()..initialPosition = position, AndroidHead()..initialPosition = position, SpaceshipHole( - onExitLayer: Layer.spaceshipExitRail, - onExitElevation: 2, + outsideLayer: Layer.spaceshipExitRail, + outsidePriority: 2, )..initialPosition = position - Vector2(5.2, 4.8), SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8), SpaceshipWall()..initialPosition = position, @@ -47,8 +50,8 @@ class Spaceship extends Forge2DBlueprint { /// {@endtemplate} class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_saucer} - // TODO(ruimiguel): apply Elevated when PR merged. - SpaceshipSaucer() : super(priority: 3) { + SpaceshipSaucer() + : super(priority: Spaceship.ballPriorityWhenOnSpaceship - 1) { layer = Layer.spaceship; } @@ -92,7 +95,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@endtemplate} class AndroidHead extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_bridge} - AndroidHead() : super(priority: 4) { + AndroidHead() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) { layer = Layer.spaceship; } @@ -147,9 +150,9 @@ class SpaceshipEntrance extends RampOpening { /// {@macro spaceship_entrance} SpaceshipEntrance() : super( - pathwayLayer: Layer.spaceship, + insideLayer: Layer.spaceship, orientation: RampOrientation.up, - pathwayPriority: 4, + insidePriority: Spaceship.ballPriorityWhenOnSpaceship, ) { layer = Layer.spaceship; } @@ -178,21 +181,18 @@ class SpaceshipEntrance extends RampOpening { /// {@endtemplate} class SpaceshipHole extends RampOpening { /// {@macro spaceship_hole} - SpaceshipHole({Layer? onExitLayer, this.onExitElevation = 1}) + SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1}) : super( - pathwayLayer: Layer.spaceship, - outsideLayer: onExitLayer, + insideLayer: Layer.spaceship, + outsideLayer: outsideLayer, orientation: RampOrientation.up, - pathwayPriority: 4, - outsidePriority: onExitElevation, + insidePriority: 4, + outsidePriority: outsidePriority, ) { + renderBody = false; layer = Layer.spaceship; } - /// Priority order for [SpaceshipHole] on exit. - // TODO(ruimiguel): apply Elevated when PR merged. - final int onExitElevation; - @override Shape get shape { return ArcShape( @@ -234,8 +234,7 @@ class _SpaceshipWallShape extends ChainShape { /// {@endtemplate} class SpaceshipWall extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_wall} - // TODO(ruimiguel): apply Elevated when PR merged - SpaceshipWall() : super(priority: 4) { + SpaceshipWall() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) { layer = Layer.spaceship; } @@ -268,8 +267,7 @@ class SpaceshipEntranceBallContactCallback @override void begin(SpaceshipEntrance entrance, Ball ball, _) { ball - ..priority = entrance.pathwayPriority - ..gameRef.reorderChildren() + ..sendTo(entrance.insidePriority) ..layer = Layer.spaceship; } } @@ -277,16 +275,14 @@ class SpaceshipEntranceBallContactCallback /// [ContactCallback] that handles the contact between the [Ball] /// and a [SpaceshipHole]. /// -/// It sets the [Ball] priority and filter data so it will "be back" on the -/// board. +/// It sets the [Ball] priority and filter data so it will outside of the +/// [Spaceship]. class SpaceshipHoleBallContactCallback extends ContactCallback { @override void begin(SpaceshipHole hole, Ball ball, _) { ball - // TODO(ruimiguel): apply Elevated when PR merged. - ..priority = hole.outsidePriority - ..gameRef.reorderChildren() + ..sendTo(hole.outsidePriority) ..layer = hole.outsideLayer; } } diff --git a/packages/pinball_components/lib/src/flame/flame.dart b/packages/pinball_components/lib/src/flame/flame.dart index c46a6fd2..9af8dba6 100644 --- a/packages/pinball_components/lib/src/flame/flame.dart +++ b/packages/pinball_components/lib/src/flame/flame.dart @@ -1 +1,2 @@ export 'blueprint.dart'; +export 'priority.dart'; diff --git a/packages/pinball_components/lib/src/flame/priority.dart b/packages/pinball_components/lib/src/flame/priority.dart new file mode 100644 index 00000000..f4dccabf --- /dev/null +++ b/packages/pinball_components/lib/src/flame/priority.dart @@ -0,0 +1,39 @@ +import 'dart:math' as math; +import 'package:flame/components.dart'; + +/// Helper methods to change the [priority] of a [Component]. +extension ComponentPriorityX on Component { + static const _lowestPriority = 0; + + /// Changes the priority to a specific one. + void sendTo(int destinationPriority) { + if (priority != destinationPriority) { + priority = math.max(destinationPriority, _lowestPriority); + reorderChildren(); + } + } + + /// Changes the priority to the lowest possible. + void sendToBack() { + if (priority != _lowestPriority) { + priority = _lowestPriority; + reorderChildren(); + } + } + + /// Decreases the priority to be lower than another [Component]. + void showBehindOf(Component other) { + if (priority >= other.priority) { + priority = math.max(other.priority - 1, _lowestPriority); + reorderChildren(); + } + } + + /// Increases the priority to be higher than another [Component]. + void showInFrontOf(Component other) { + if (priority <= other.priority) { + priority = other.priority + 1; + reorderChildren(); + } + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index aa1eb224..d1f138d9 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -26,6 +26,8 @@ flutter: generate: true assets: - assets/images/ + - assets/images/baseboard/ + - assets/images/dino/ - assets/images/flipper/ flutter_gen: diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 2df3c16c..62ff7022 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -6,7 +6,6 @@ // https://opensource.org/licenses/MIT. import 'package:dashbook/dashbook.dart'; import 'package:flutter/material.dart'; -import 'package:sandbox/stories/effects/effects.dart'; import 'package:sandbox/stories/spaceship/spaceship.dart'; import 'package:sandbox/stories/stories.dart'; @@ -18,5 +17,6 @@ void main() { addEffectsStories(dashbook); addFlipperStories(dashbook); addSpaceshipStories(dashbook); + addBaseboardStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard.dart new file mode 100644 index 00000000..96d89928 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard.dart @@ -0,0 +1,15 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/baseboard/basic.dart'; + +void addBaseboardStories(Dashbook dashbook) { + dashbook.storiesOf('Baseboard').add( + 'Basic', + (context) => GameWidget( + game: BasicBaseboardGame(), + ), + codeLink: buildSourceLink('baseboard/basic.dart'), + info: BasicBaseboardGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/basic.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/basic.dart new file mode 100644 index 00000000..127c1dec --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/baseboard/basic.dart @@ -0,0 +1,26 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BasicBaseboardGame extends BasicGame { + static const info = ''' + Basic example of how a Baseboard works. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + + final leftBaseboard = Baseboard(side: BoardSide.left) + ..initialPosition = center - Vector2(25, 0); + final rightBaseboard = Baseboard(side: BoardSide.right) + ..initialPosition = center + Vector2(25, 0); + + await addAll([ + leftBaseboard, + rightBaseboard, + ]); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 9f861bde..90b93723 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,3 +1,5 @@ export 'ball/ball.dart'; +export 'baseboard/baseboard.dart'; +export 'effects/effects.dart'; export 'flipper/flipper.dart'; export 'layer/layer.dart'; diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index 8d6f45b3..c36afff2 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -24,3 +25,5 @@ class MockContact extends Mock implements Contact {} class MockContactCallback extends Mock implements ContactCallback {} + +class MockComponent extends Mock implements Component {} 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/test/game/components/baseboard_test.dart b/packages/pinball_components/test/src/components/baseboard_test.dart similarity index 89% rename from test/game/components/baseboard_test.dart rename to packages/pinball_components/test/src/components/baseboard_test.dart index 37c3c978..b1ce58e2 100644 --- a/test/game/components/baseboard_test.dart +++ b/packages/pinball_components/test/src/components/baseboard_test.dart @@ -3,13 +3,16 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import '../../helpers/helpers.dart'; + void main() { group('Baseboard', () { + // TODO(allisonryan0002): Add golden tests. + TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(Forge2DGame.new); + final flameTester = FlameTester(TestGame.new); flameTester.test( 'loads correctly', @@ -62,14 +65,14 @@ void main() { group('fixtures', () { flameTester.test( - 'has six', + 'has seven', (game) async { final baseboard = Baseboard( side: BoardSide.left, ); await game.ensureAdd(baseboard); - expect(baseboard.body.fixtures.length, equals(6)); + expect(baseboard.body.fixtures.length, equals(7)); }, ); }); 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/packages/pinball_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart new file mode 100644 index 00000000..af80444b --- /dev/null +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -0,0 +1,28 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.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() { + group('DinoWalls', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + flameTester.test( + 'loads correctly', + (game) async { + final dinoWalls = DinoWalls(position: Vector2.zero()); + await game.addFromBlueprint(dinoWalls); + await game.ready(); + + for (final wall in dinoWalls.components) { + expect(game.contains(wall), isTrue); + } + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/golden/dino-bottom-wall.png b/packages/pinball_components/test/src/components/golden/dino-bottom-wall.png new file mode 100644 index 00000000..14ae9c0d Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/dino-bottom-wall.png differ diff --git a/packages/pinball_components/test/src/components/golden/dino-top-wall.png b/packages/pinball_components/test/src/components/golden/dino-top-wall.png new file mode 100644 index 00000000..0d434d69 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/dino-top-wall.png differ diff --git a/packages/pinball_components/test/src/components/ramp_opening_test.dart b/packages/pinball_components/test/src/components/ramp_opening_test.dart index c49e9164..cb42203a 100644 --- a/packages/pinball_components/test/src/components/ramp_opening_test.dart +++ b/packages/pinball_components/test/src/components/ramp_opening_test.dart @@ -12,7 +12,7 @@ class TestRampOpening extends RampOpening { required RampOrientation orientation, required Layer pathwayLayer, }) : super( - pathwayLayer: pathwayLayer, + insideLayer: pathwayLayer, orientation: orientation, ); @@ -129,14 +129,12 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); }); flameTester.test( @@ -152,14 +150,12 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); }); flameTester.test( @@ -174,15 +170,13 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); callback.end(ball, area, MockContact()); verify(() => ball.layer = Layer.board); @@ -200,15 +194,13 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); callback.end(ball, area, MockContact()); verify(() => ball.layer = Layer.board); @@ -226,21 +218,19 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); callback.end(ball, area, MockContact()); verifyNever(() => ball.layer = Layer.board); callback.begin(ball, area, MockContact()); - verifyNever(() => ball.layer = area.pathwayLayer); + verifyNever(() => ball.layer = area.insideLayer); callback.end(ball, area, MockContact()); verify(() => ball.layer = Layer.board); diff --git a/packages/pinball_components/test/src/components/spaceship_test.dart b/packages/pinball_components/test/src/components/spaceship_test.dart index f89408f7..4d980c69 100644 --- a/packages/pinball_components/test/src/components/spaceship_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_test.dart @@ -59,7 +59,8 @@ void main() { group('SpaceshipEntranceBallContactCallback', () { test('changes the ball priority on contact', () { - when(() => entrance.onEnterElevation).thenReturn(3); + when(() => ball.priority).thenReturn(2); + when(() => entrance.insidePriority).thenReturn(3); SpaceshipEntranceBallContactCallback().begin( entrance, @@ -67,39 +68,15 @@ void main() { MockContact(), ); - verify(() => ball.priority = entrance.onEnterElevation).called(1); - }); - - test('re order the game children', () { - when(() => entrance.onEnterElevation).thenReturn(3); - - SpaceshipEntranceBallContactCallback().begin( - entrance, - ball, - MockContact(), - ); - - verify(game.reorderChildren).called(1); + verify(() => ball.sendTo(entrance.insidePriority)).called(1); }); }); group('SpaceshipHoleBallContactCallback', () { test('changes the ball priority on contact', () { + when(() => ball.priority).thenReturn(2); when(() => hole.outsideLayer).thenReturn(Layer.board); - when(() => hole.onExitElevation).thenReturn(1); - - SpaceshipHoleBallContactCallback().begin( - hole, - ball, - MockContact(), - ); - - verify(() => ball.priority = hole.onExitElevation).called(1); - }); - - test('re order the game children', () { - when(() => hole.outsideLayer).thenReturn(Layer.board); - when(() => hole.onExitElevation).thenReturn(1); + when(() => hole.outsidePriority).thenReturn(1); SpaceshipHoleBallContactCallback().begin( hole, @@ -107,7 +84,7 @@ void main() { MockContact(), ); - verify(game.reorderChildren).called(1); + verify(() => ball.sendTo(hole.outsidePriority)).called(1); }); }); }); diff --git a/packages/pinball_components/test/src/flame/priority_test.dart b/packages/pinball_components/test/src/flame/priority_test.dart new file mode 100644 index 00000000..231c7744 --- /dev/null +++ b/packages/pinball_components/test/src/flame/priority_test.dart @@ -0,0 +1,221 @@ +// ignore_for_file: cascade_invocations +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_components/src/flame/priority.dart'; + +import '../../helpers/helpers.dart'; + +class TestBodyComponent extends BodyComponent { + @override + Body createBody() { + final fixtureDef = FixtureDef(CircleShape()); + return world.createBody(BodyDef())..createFixture(fixtureDef); + } +} + +void main() { + final flameTester = FlameTester(Forge2DGame.new); + + group('ComponentPriorityX', () { + group('sendTo', () { + flameTester.test( + 'changes the priority correctly to other level', + (game) async { + const newPriority = 5; + final component = TestBodyComponent()..priority = 4; + + component.sendTo(newPriority); + + expect(component.priority, equals(newPriority)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the new priority is different', + (game) async { + const newPriority = 5; + final component = MockComponent(); + when(() => component.priority).thenReturn(4); + + component.sendTo(newPriority); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is the same", + (game) async { + const newPriority = 5; + final component = MockComponent(); + when(() => component.priority).thenReturn(newPriority); + + component.sendTo(newPriority); + + verifyNever(component.reorderChildren); + }, + ); + }); + + group('sendToBack', () { + flameTester.test( + 'changes the priority correctly to board level', + (game) async { + final component = TestBodyComponent()..priority = 4; + + component.sendToBack(); + + expect(component.priority, equals(0)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the priority is greater than lowest level', + (game) async { + final component = MockComponent(); + when(() => component.priority).thenReturn(4); + + component.sendToBack(); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is the lowest level", + (game) async { + final component = MockComponent(); + when(() => component.priority).thenReturn(0); + + component.sendToBack(); + + verifyNever(component.reorderChildren); + }, + ); + }); + + group('showBehindOf', () { + flameTester.test( + 'changes the priority if it is greater than other component', + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority - 1; + + component.showBehindOf(otherComponent); + + expect(component.priority, equals(otherComponent.priority - 1)); + }, + ); + + flameTester.test( + "doesn't change the priority if it is lower than other component", + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority + 1; + + component.showBehindOf(otherComponent); + + expect(component.priority, equals(startPriority)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the priority is greater than other component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority - 1); + + component.showBehindOf(otherComponent); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is lower than other " + 'component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority + 1); + + component.showBehindOf(otherComponent); + + verifyNever(component.reorderChildren); + }, + ); + }); + + group('showInFrontOf', () { + flameTester.test( + 'changes the priority if it is lower than other component', + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority + 1; + + component.showInFrontOf(otherComponent); + + expect(component.priority, equals(otherComponent.priority + 1)); + }, + ); + + flameTester.test( + "doesn't change the priority if it is greater than other component", + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority - 1; + + component.showInFrontOf(otherComponent); + + expect(component.priority, equals(startPriority)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the priority is lower than other component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority + 1); + + component.showInFrontOf(otherComponent); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is greater than other " + 'component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority - 1); + + component.showInFrontOf(otherComponent); + + verifyNever(component.reorderChildren); + }, + ); + }); + }); +} 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/mocks.dart b/test/helpers/mocks.dart index 9453c93a..fbe8edfb 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -66,10 +66,6 @@ class MockFilter extends Mock implements Filter {} class MockFixture extends Mock implements Fixture {} -class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {} - -class MockSpaceshipHole extends Mock implements SpaceshipHole {} - class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {} class MockComponentSet extends Mock implements ComponentSet {} 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 = ''; }