diff --git a/.gitignore b/.gitignore index 2d9c4dbe..a7531405 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ app.*.map.json test/.test_runner.dart web/__/firebase/init.js + +# Application exceptions +!/packages/pinball_components/assets/images/flutter_sign_post.png diff --git a/assets/images/components/flipper.png b/assets/images/components/flipper.png deleted file mode 100644 index f63974c4..00000000 Binary files a/assets/images/components/flipper.png and /dev/null differ diff --git a/assets/images/components/spaceship/android-bottom.png b/assets/images/components/spaceship/android-bottom.png deleted file mode 100644 index 90dfdc01..00000000 Binary files a/assets/images/components/spaceship/android-bottom.png and /dev/null differ diff --git a/assets/images/components/spaceship/android-top.png b/assets/images/components/spaceship/android-top.png deleted file mode 100644 index 92c99db7..00000000 Binary files a/assets/images/components/spaceship/android-top.png and /dev/null differ diff --git a/assets/images/components/spaceship/lower.png b/assets/images/components/spaceship/lower.png deleted file mode 100644 index 1f0d9b10..00000000 Binary files a/assets/images/components/spaceship/lower.png and /dev/null differ diff --git a/assets/images/components/spaceship/saucer.png b/assets/images/components/spaceship/saucer.png deleted file mode 100644 index 93af98b5..00000000 Binary files a/assets/images/components/spaceship/saucer.png and /dev/null differ diff --git a/assets/images/components/spaceship/upper.png b/assets/images/components/spaceship/upper.png deleted file mode 100644 index 0e03cec8..00000000 Binary files a/assets/images/components/spaceship/upper.png and /dev/null differ diff --git a/lib/flame/component_controller.dart b/lib/flame/component_controller.dart new file mode 100644 index 00000000..2bbf5ca9 --- /dev/null +++ b/lib/flame/component_controller.dart @@ -0,0 +1,37 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; + +/// {@template component_controller} +/// A [ComponentController] is a [Component] in charge of handling the logic +/// associated with another [Component]. +/// +/// [ComponentController]s usually implement [BlocComponent]. +/// {@endtemplate} +abstract class ComponentController extends Component { + /// {@macro component_controller} + ComponentController(this.component); + + /// The [Component] controlled by this [ComponentController]. + final T component; + + @override + Future addToParent(Component parent) async { + assert( + parent == component, + 'ComponentController should be child of $component.', + ); + await super.addToParent(parent); + } +} + +/// Mixin that attaches a single [ComponentController] to a [Component]. +mixin Controls on Component { + /// The [ComponentController] attached to this [Component]. + late final T controller; + + @override + Future onLoad() async { + await super.onLoad(); + await add(controller); + } +} diff --git a/lib/flame/flame.dart b/lib/flame/flame.dart new file mode 100644 index 00000000..9264c0f4 --- /dev/null +++ b/lib/flame/flame.dart @@ -0,0 +1 @@ +export 'component_controller.dart'; 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/ball.dart b/lib/game/components/ball.dart deleted file mode 100644 index aca2b154..00000000 --- a/lib/game/components/ball.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:pinball/flame/blueprint.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template ball_type} -/// Specifies the type of [Ball]. -/// -/// Different [BallType]s are affected by different game mechanics. -/// {@endtemplate} -enum BallType { - /// A [Ball] spawned from the [Plunger]. - /// - /// [normal] balls decrease the [GameState.balls] when they fall through the - /// the [BottomWall]. - normal, - - /// A [Ball] that does not alter [GameState.balls]. - /// - /// For example, a [Ball] spawned by Dash in the [FlutterForest]. - extra, -} - -/// {@template ball_blueprint} -/// [Blueprint] which cretes a ball game object. -/// {@endtemplate} -class BallBlueprint extends Blueprint { - /// {@macro ball_blueprint} - BallBlueprint({ - required this.position, - required this.type, - }); - - /// The initial position of the [Ball]. - final Vector2 position; - - /// {@macro ball_type} - final BallType type; - - @override - void build(PinballGame gameRef) { - final baseColor = gameRef.theme.characterTheme.ballColor; - final ball = Ball(baseColor: baseColor) - ..add( - BallController(type: type), - ); - - add(ball..initialPosition = position + Vector2(0, ball.size.y / 2)); - } -} - -/// {@template ball_controller} -/// Controller attached to a [Ball] that handles its game related logic. -/// {@endtemplate} -class BallController extends Component with HasGameRef { - /// {@macro ball_controller} - BallController({required this.type}); - - /// {@macro ball_type} - final BallType type; - - /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if - /// any are left. - /// - /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into - /// a [BottomWall]. - void lost() { - parent?.shouldRemove = true; - // TODO(alestiago): Consider adding test for this logic once we remove the - // BallX extension. - if (type != BallType.normal) return; - - final bloc = gameRef.read()..add(const BallLost()); - final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver; - if (shouldBallRespwan) { - gameRef.spawnBall(); - } - } -} - -/// Adds helper methods to the [Ball] -extension BallX on Ball { - /// Returns the controller instance of the ball - // TODO(erickzanardo): Remove the need of an extension. - BallController get controller { - return children.whereType().first; - } -} diff --git a/lib/game/components/baseboard.dart b/lib/game/components/baseboard.dart index d4c326e3..cdad23fc 100644 --- a/lib/game/components/baseboard.dart +++ b/lib/game/components/baseboard.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template baseboard} diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index 1f96120e..42183c5f 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -1,20 +1,22 @@ import 'package:flame/components.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; /// {@template board} /// The main flat surface of the [PinballGame]. /// {endtemplate} class Board extends Component { /// {@macro board} - Board(); + // TODO(alestiago): Make Board a Blueprint and sort out priorities. + Board() : super(priority: 5); @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, + BoardDimensions.bounds.center.dx, + BoardDimensions.bounds.bottom + 10, ), spacing: 2, ); @@ -24,8 +26,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([ @@ -92,15 +94,17 @@ class _BottomGroupSide extends Component { Future onLoad() async { final direction = _side.direction; - final flipper = Flipper( + final flipper = ControlledFlipper( side: _side, )..initialPosition = _position; + final baseboard = Baseboard(side: _side) ..initialPosition = _position + Vector2( (Baseboard.size.x / 1.6 * direction), Baseboard.size.y - 2, ); + final kicker = Kicker( side: _side, )..initialPosition = _position + diff --git a/lib/game/components/chrome_dino.dart b/lib/game/components/chrome_dino.dart index 5cf39b3c..af086e0e 100644 --- a/lib/game/components/chrome_dino.dart +++ b/lib/game/components/chrome_dino.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart' hide Timer; import 'package:flutter/material.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template chrome_dino} @@ -32,7 +31,7 @@ class ChromeDino extends BodyComponent with InitialPosition { anchor: anchor, ); final joint = _ChromeDinoJoint(jointDef); - world.createJoint2(joint); + world.createJoint(joint); return joint; } @@ -155,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 7c3347a6..1f1f1ce5 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,17 +1,14 @@ -export 'ball.dart'; export 'baseboard.dart'; export 'board.dart'; -export 'board_side.dart'; export 'bonus_word.dart'; export 'chrome_dino.dart'; -export 'flipper.dart'; +export 'controlled_ball.dart'; +export 'flipper_controller.dart'; export 'flutter_forest.dart'; export 'jetpack_ramp.dart'; -export 'joint_anchor.dart'; export 'kicker.dart'; export 'launcher_ramp.dart'; export 'plunger.dart'; -export 'ramp_opening.dart'; export 'score_points.dart'; -export 'spaceship.dart'; +export 'spaceship_exit_rail.dart'; export 'wall.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart new file mode 100644 index 00000000..257d4f1d --- /dev/null +++ b/lib/game/components/controlled_ball.dart @@ -0,0 +1,102 @@ +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'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// {@template controlled_ball} +/// A [Ball] with a [BallController] attached. +/// {@endtemplate} +class ControlledBall extends Ball with Controls { + /// A [Ball] that launches from the [Plunger]. + /// + /// When a launched [Ball] is lost, it will decrease the [GameState.balls] + /// count, and a new [Ball] is spawned. + ControlledBall.launch({ + required PinballTheme theme, + }) : super(baseColor: theme.characterTheme.ballColor) { + controller = LaunchedBallController(this); + } + + /// {@template bonus_ball} + /// {@macro controlled_ball} + /// + /// When a bonus [Ball] is lost, the [GameState.balls] doesn't change. + /// {@endtemplate} + ControlledBall.bonus({ + required PinballTheme theme, + }) : super(baseColor: theme.characterTheme.ballColor) { + controller = BonusBallController(this); + } + + /// [Ball] used in [DebugPinballGame]. + ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { + controller = BonusBallController(this); + } +} + +/// {@template ball_controller} +/// Controller attached to a [Ball] that handles its game related logic. +/// {@endtemplate} +abstract class BallController extends ComponentController { + /// {@macro ball_controller} + BallController(Ball ball) : super(ball); + + /// Removes the [Ball] from a [PinballGame]. + /// + /// {@template ball_controller_lost} + /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into + /// a [BottomWall]. + /// {@endtemplate} + void lost(); +} + +/// {@template bonus_ball_controller} +/// {@macro ball_controller} +/// +/// A [BonusBallController] doesn't change the [GameState.balls] count. +/// {@endtemplate} +class BonusBallController extends BallController { + /// {@macro bonus_ball_controller} + BonusBallController(Ball component) : super(component); + + @override + void lost() { + component.shouldRemove = true; + } +} + +/// {@template launched_ball_controller} +/// {@macro ball_controller} +/// +/// A [LaunchedBallController] changes the [GameState.balls] count. +/// {@endtemplate} +class LaunchedBallController extends BallController + with HasGameRef, BlocComponent { + /// {@macro launched_ball_controller} + LaunchedBallController(Ball ball) : super(ball); + + @override + bool listenWhen(GameState? previousState, GameState newState) { + return (previousState?.balls ?? 0) > newState.balls; + } + + @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() { + gameRef.read().add(const BallLost()); + } +} diff --git a/lib/game/components/flipper_controller.dart b/lib/game/components/flipper_controller.dart new file mode 100644 index 00000000..9b73b6d3 --- /dev/null +++ b/lib/game/components/flipper_controller.dart @@ -0,0 +1,65 @@ +import 'package:flame/components.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball/flame/flame.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template controlled_flipper} +/// A [Flipper] with a [FlipperController] attached. +/// {@endtemplate} +class ControlledFlipper extends Flipper with Controls { + /// {@macro controlled_flipper} + ControlledFlipper({ + required BoardSide side, + }) : super(side: side) { + controller = FlipperController(this); + } +} + +/// {@template flipper_controller} +/// A [ComponentController] that controls a [Flipper]s movement. +/// {@endtemplate} +class FlipperController extends ComponentController + with KeyboardHandler { + /// {@macro flipper_controller} + FlipperController(Flipper flipper) + : _keys = flipper.side.flipperKeys, + super(flipper); + + /// The [LogicalKeyboardKey]s that will control the [Flipper]. + /// + /// [onKeyEvent] method listens to when one of these keys is pressed. + final List _keys; + + @override + bool onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (!_keys.contains(event.logicalKey)) return true; + + if (event is RawKeyDownEvent) { + component.moveUp(); + } else if (event is RawKeyUpEvent) { + component.moveDown(); + } + + return false; + } +} + +extension on BoardSide { + List get flipperKeys { + switch (this) { + case BoardSide.left: + return [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, + ]; + case BoardSide.right: + return [ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, + ]; + } + } +} diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart index 5b91ee40..6eb3ce7d 100644 --- a/lib/game/components/flutter_forest.dart +++ b/lib/game/components/flutter_forest.dart @@ -6,7 +6,6 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; -import 'package:pinball/flame/blueprint.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -32,11 +31,11 @@ class FlutterForest extends Component @override void onNewState(GameState state) { super.onNewState(state); - gameRef.addFromBlueprint( - BallBlueprint( - position: Vector2(17.2, 52.7), - type: BallType.extra, - ), + + add( + ControlledBall.bonus( + theme: gameRef.theme, + )..initialPosition = Vector2(17.2, 52.7), ); } @@ -44,6 +43,8 @@ class FlutterForest extends Component Future onLoad() async { gameRef.addContactCallback(DashNestBumperBallContactCallback()); + final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); + // TODO(alestiago): adjust positioning once sprites are added. final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest') ..initialPosition = Vector2(8.95, 51.95); @@ -53,6 +54,7 @@ class FlutterForest extends Component ..initialPosition = Vector2(18.55, 59.35); await addAll([ + signPost, smallLeftNest, smallRightNest, bigNest, diff --git a/lib/game/components/jetpack_ramp.dart b/lib/game/components/jetpack_ramp.dart index a24e1438..4c4c8be9 100644 --- a/lib/game/components/jetpack_ramp.dart +++ b/lib/game/components/jetpack_ramp.dart @@ -5,7 +5,6 @@ import 'dart:math' as math; import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; -import 'package:pinball/flame/blueprint.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -14,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([ @@ -115,7 +114,7 @@ class _JetpackRampOpening extends RampOpening { final double _rotation; - static final Vector2 _size = Vector2(JetpackRamp.width / 3, .1); + static final Vector2 _size = Vector2(JetpackRamp.width / 4, .1); @override Shape get shape => PolygonShape() diff --git a/lib/game/components/kicker.dart b/lib/game/components/kicker.dart index dc55a52f..d9eb7932 100644 --- a/lib/game/components/kicker.dart +++ b/lib/game/components/kicker.dart @@ -4,7 +4,6 @@ import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:geometry/geometry.dart' as geometry show centroid; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template kicker} diff --git a/lib/game/components/launcher_ramp.dart b/lib/game/components/launcher_ramp.dart index ca22dcad..ea8a3984 100644 --- a/lib/game/components/launcher_ramp.dart +++ b/lib/game/components/launcher_ramp.dart @@ -6,7 +6,6 @@ import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; -import 'package:pinball/flame/blueprint.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index d9137457..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); } @@ -85,7 +84,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { plunger: this, anchor: anchor, ); - world.createJoint(jointDef); + world.createJoint(PrismaticJoint(jointDef)); } @override @@ -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/score_points.dart b/lib/game/components/score_points.dart index 12649137..39910777 100644 --- a/lib/game/components/score_points.dart +++ b/lib/game/components/score_points.dart @@ -18,16 +18,23 @@ mixin ScorePoints on BodyComponent { } } +/// {@template ball_score_points_callbacks} /// Adds points to the score when a [Ball] collides with a [BodyComponent] that /// implements [ScorePoints]. +/// {@endtemplate} class BallScorePointsCallback extends ContactCallback { + /// {@macro ball_score_points_callbacks} + BallScorePointsCallback(PinballGame game) : _gameRef = game; + + final PinballGame _gameRef; + @override void begin( - Ball ball, + Ball _, ScorePoints scorePoints, - Contact _, + Contact __, ) { - ball.controller.gameRef.read().add( + _gameRef.read().add( Scored(points: scorePoints.points), ); } diff --git a/lib/game/components/spaceship_exit_rail.dart b/lib/game/components/spaceship_exit_rail.dart new file mode 100644 index 00000000..0dc38322 --- /dev/null +++ b/lib/game/components/spaceship_exit_rail.dart @@ -0,0 +1,198 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; + +/// {@template spaceship_exit_rail} +/// A [Blueprint] for the spaceship drop tube. +/// {@endtemplate} +class SpaceshipExitRail extends Forge2DBlueprint { + /// {@macro spaceship_exit_rail} + SpaceshipExitRail({required this.position}); + + /// The [position] where the elements will be created + final Vector2 position; + + @override + void build(_) { + addAllContactCallback([ + SpaceshipExitRailEndBallContactCallback(), + ]); + + final spaceshipExitRailRamp = _SpaceshipExitRailRamp() + ..initialPosition = position; + final exitRail = SpaceshipExitRailEnd() + ..initialPosition = position + _SpaceshipExitRailRamp.exitPoint; + + addAll([ + spaceshipExitRailRamp, + exitRail, + ]); + } +} + +class _SpaceshipExitRailRamp extends BodyComponent + with InitialPosition, Layered { + _SpaceshipExitRailRamp() : super(priority: 2) { + layer = Layer.spaceshipExitRail; + // TODO(ruimiguel): remove color once asset is placed. + paint = Paint() + ..color = const Color.fromARGB(255, 249, 65, 3) + ..style = PaintingStyle.stroke; + } + + static final exitPoint = Vector2(9.2, -48.5); + + List _createFixtureDefs() { + const entranceRotationAngle = 175 * math.pi / 180; + const curveRotationAngle = 275 * math.pi / 180; + const exitRotationAngle = 340 * math.pi / 180; + const width = 5.5; + + final fixturesDefs = []; + + final entranceWall = ArcShape( + center: Vector2(width / 2, 0), + arcRadius: width / 2, + angle: math.pi, + rotation: entranceRotationAngle, + ); + final entranceFixtureDef = FixtureDef(entranceWall); + fixturesDefs.add(entranceFixtureDef); + + final topLeftControlPoints = [ + Vector2(0, 0), + Vector2(10, .5), + Vector2(7, 4), + Vector2(15.5, 8.3), + ]; + final topLeftCurveShape = BezierCurveShape( + controlPoints: topLeftControlPoints, + )..rotate(curveRotationAngle); + final topLeftFixtureDef = FixtureDef(topLeftCurveShape); + fixturesDefs.add(topLeftFixtureDef); + + final topRightControlPoints = [ + Vector2(0, width), + Vector2(10, 6.5), + Vector2(7, 10), + Vector2(15.5, 13.2), + ]; + final topRightCurveShape = BezierCurveShape( + controlPoints: topRightControlPoints, + )..rotate(curveRotationAngle); + final topRightFixtureDef = FixtureDef(topRightCurveShape); + fixturesDefs.add(topRightFixtureDef); + + final mediumLeftControlPoints = [ + topLeftControlPoints.last, + Vector2(21, 12.9), + Vector2(30, 7.1), + Vector2(32, 4.8), + ]; + final mediumLeftCurveShape = BezierCurveShape( + controlPoints: mediumLeftControlPoints, + )..rotate(curveRotationAngle); + final mediumLeftFixtureDef = FixtureDef(mediumLeftCurveShape); + fixturesDefs.add(mediumLeftFixtureDef); + + final mediumRightControlPoints = [ + topRightControlPoints.last, + Vector2(21, 17.2), + Vector2(30, 12.1), + Vector2(32, 10.2), + ]; + final mediumRightCurveShape = BezierCurveShape( + controlPoints: mediumRightControlPoints, + )..rotate(curveRotationAngle); + final mediumRightFixtureDef = FixtureDef(mediumRightCurveShape); + fixturesDefs.add(mediumRightFixtureDef); + + final bottomLeftControlPoints = [ + mediumLeftControlPoints.last, + Vector2(40, -1), + Vector2(48, 1.9), + Vector2(50.5, 2.5), + ]; + final bottomLeftCurveShape = BezierCurveShape( + controlPoints: bottomLeftControlPoints, + )..rotate(curveRotationAngle); + final bottomLeftFixtureDef = FixtureDef(bottomLeftCurveShape); + fixturesDefs.add(bottomLeftFixtureDef); + + final bottomRightControlPoints = [ + mediumRightControlPoints.last, + Vector2(40, 4), + Vector2(46, 6.5), + Vector2(48.8, 7.6), + ]; + final bottomRightCurveShape = BezierCurveShape( + controlPoints: bottomRightControlPoints, + )..rotate(curveRotationAngle); + final bottomRightFixtureDef = FixtureDef(bottomRightCurveShape); + fixturesDefs.add(bottomRightFixtureDef); + + final exitWall = ArcShape( + center: exitPoint, + arcRadius: width / 2, + angle: math.pi, + rotation: exitRotationAngle, + ); + final exitFixtureDef = FixtureDef(exitWall); + fixturesDefs.add(exitFixtureDef); + + return fixturesDefs; + } + + @override + Body createBody() { + final bodyDef = BodyDef() + ..userData = this + ..position = initialPosition; + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} + +/// {@template spaceship_exit_rail_end} +/// A sensor [BodyComponent] responsible for sending the [Ball] +/// back to the board. +/// {@endtemplate} +class SpaceshipExitRailEnd extends RampOpening { + /// {@macro spaceship_exit_rail_end} + SpaceshipExitRailEnd() + : super( + pathwayLayer: Layer.spaceshipExitRail, + orientation: RampOrientation.down, + ) { + layer = Layer.spaceshipExitRail; + } + + @override + Shape get shape { + return CircleShape()..radius = 1; + } +} + +/// [ContactCallback] that handles the contact between the [Ball] +/// and a [SpaceshipExitRailEnd]. +/// +/// It resets the [Ball] priority and filter data so it will "be back" on the +/// board. +class SpaceshipExitRailEndBallContactCallback + extends ContactCallback { + @override + void begin(SpaceshipExitRailEnd exitRail, Ball ball, _) { + ball + ..priority = 1 + ..gameRef.reorderChildren() + ..layer = exitRail.outsideLayer; + } +} diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index 0fb57a41..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(), ); } @@ -78,6 +76,7 @@ class BottomWall extends Wall { class BottomWallBallContactCallback extends ContactCallback { @override void begin(Ball ball, BottomWall wall, Contact contact) { - ball.controller.lost(); + // TODO(alestiago): replace with .firstChild when available. + ball.children.whereType().first.lost(); } } diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 700073fe..93d3effb 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -8,7 +8,9 @@ extension PinballGameAssetsX on PinballGame { Future preLoadAssets() async { await Future.wait([ images.load(components.Assets.images.ball.keyName), - images.load(Assets.images.components.flipper.path), + 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(Assets.images.components.background.path), images.load(Assets.images.components.launchRamp.launchRamp.path), images.load(Assets.images.components.launchRamp.launchRailFG.path), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index e0f40e24..e1911189 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,15 +1,14 @@ // 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'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/flame/blueprint.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets; class PinballGame extends Forge2DGame @@ -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(); @@ -46,7 +36,20 @@ class PinballGame extends Forge2DGame unawaited(_addPlunger()); unawaited(_addBonusWord()); unawaited(_addPaths()); - unawaited(addFromBlueprint(Spaceship())); + unawaited( + addFromBlueprint( + Spaceship( + position: Vector2(-26.5, 28.5), + ), + ), + ); + unawaited( + addFromBlueprint( + SpaceshipExitRail( + position: Vector2(-34.3, 23.8), + ), + ), + ); // Fix camera on the center of the board. camera @@ -55,7 +58,7 @@ class PinballGame extends Forge2DGame } void _addContactCallbacks() { - addContactCallback(BallScorePointsCallback()); + addContactCallback(BallScorePointsCallback(this)); addContactCallback(BottomWallBallContactCallback()); addContactCallback(BonusLetterBallContactCallback()); } @@ -67,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); } @@ -75,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, ), ), ); @@ -88,12 +92,13 @@ class PinballGame extends Forge2DGame } void spawnBall() { - addFromBlueprint( - BallBlueprint( - position: plunger.body.position, - type: BallType.normal, - ), - ); + final ball = ControlledBall.launch( + theme: theme, + )..initialPosition = Vector2( + plunger.body.position.x, + plunger.body.position.y + Ball.size.y, + ); + add(ball); } } @@ -125,11 +130,8 @@ class DebugPinballGame extends PinballGame with TapDetector { @override void onTapUp(TapUpInfo info) { - addFromBlueprint( - BallBlueprint( - position: info.eventPosition.game, - type: BallType.extra, - ), + add( + ControlledBall.debug()..initialPosition = info.eventPosition.game, ); } } diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 579d830b..0fa6a1ad 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -70,7 +70,10 @@ class _PinballGameViewState extends State { showDialog( context: context, builder: (_) { - return GameOverDialog(theme: widget.theme.characterTheme); + return GameOverDialog( + score: state.score, + theme: widget.theme.characterTheme, + ); }, ); } diff --git a/lib/game/view/widgets/game_over_dialog.dart b/lib/game/view/widgets/game_over_dialog.dart index 29164a62..e3c5a1e1 100644 --- a/lib/game/view/widgets/game_over_dialog.dart +++ b/lib/game/view/widgets/game_over_dialog.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; @@ -9,34 +11,162 @@ import 'package:pinball_theme/pinball_theme.dart'; /// {@endtemplate} class GameOverDialog extends StatelessWidget { /// {@macro game_over_dialog} - const GameOverDialog({Key? key, required this.theme}) : super(key: key); + const GameOverDialog({Key? key, required this.score, required this.theme}) + : super(key: key); - /// Current [CharacterTheme] to customize dialog + /// Score achieved by the current user. + final int score; + + /// Theme of the current user. + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LeaderboardBloc( + context.read(), + ), + child: GameOverDialogView(score: score, theme: theme), + ); + } +} + +/// {@template game_over_dialog_view} +/// View for showing final score when the game is finished. +/// {@endtemplate} +@visibleForTesting +class GameOverDialogView extends StatefulWidget { + /// {@macro game_over_dialog_view} + const GameOverDialogView({ + Key? key, + required this.score, + required this.theme, + }) : super(key: key); + + /// Score achieved by the current user. + final int score; + + /// Theme of the current user. final CharacterTheme theme; + @override + State createState() => _GameOverDialogViewState(); +} + +class _GameOverDialogViewState extends State { + final playerInitialsInputController = TextEditingController(); + + @override + void dispose() { + playerInitialsInputController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; + // TODO(ruimiguel): refactor this view once UI design finished. return Dialog( child: SizedBox( width: 200, - height: 200, + height: 250, child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(l10n.gameOver), - TextButton( - onPressed: () => Navigator.of(context).push( - LeaderboardPage.route(theme: theme), - ), - child: Text(l10n.leaderboard), + child: Padding( + padding: const EdgeInsets.all(10), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.gameOver, + style: Theme.of(context).textTheme.headline4, + ), + const SizedBox( + height: 20, + ), + Text( + '${l10n.yourScore} ${widget.score}', + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox( + height: 15, + ), + TextField( + key: const Key('player_initials_text_field'), + controller: playerInitialsInputController, + textCapitalization: TextCapitalization.characters, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: l10n.enterInitials, + ), + maxLength: 3, + ), + const SizedBox( + height: 10, + ), + _GameOverDialogActions( + score: widget.score, + theme: widget.theme, + playerInitialsInputController: + playerInitialsInputController, + ), + ], ), - ], + ), ), ), ), ); } } + +class _GameOverDialogActions extends StatelessWidget { + const _GameOverDialogActions({ + Key? key, + required this.score, + required this.theme, + required this.playerInitialsInputController, + }) : super(key: key); + + final int score; + final CharacterTheme theme; + final TextEditingController playerInitialsInputController; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return BlocBuilder( + builder: (context, state) { + switch (state.status) { + case LeaderboardStatus.loading: + return TextButton( + onPressed: () { + context.read().add( + LeaderboardEntryAdded( + entry: LeaderboardEntryData( + playerInitials: + playerInitialsInputController.text.toUpperCase(), + score: score, + character: theme.toType, + ), + ), + ); + }, + child: Text(l10n.addUser), + ); + case LeaderboardStatus.success: + return TextButton( + onPressed: () => Navigator.of(context).push( + LeaderboardPage.route(theme: theme), + ), + child: Text(l10n.leaderboard), + ); + case LeaderboardStatus.error: + return Text(l10n.error); + } + }, + ); + } +} diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 2c532870..1b023015 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -3,8 +3,6 @@ /// FlutterGen /// ***************************************************** -// ignore_for_file: directives_ordering,unnecessary_import - import 'package:flutter/widgets.dart'; class $AssetsImagesGen { @@ -17,7 +15,6 @@ class $AssetsImagesGen { class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); - /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 235c8f2e..aa56e015 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -51,5 +51,21 @@ "retry": "Retry", "@retry": { "description": "Text displayed on the retry button leaders board page" + }, + "addUser": "Add User", + "@addUser": { + "description": "Text displayed on the add user button at ending dialog" + }, + "error": "Error", + "@error": { + "description": "Text displayed on the ending dialog when there is any error on sending user" + }, + "yourScore": "Your score is", + "@yourScore": { + "description": "Text displayed on the ending dialog when game finishes to show the final score" + }, + "enterInitials": "Enter your initials", + "@enterInitials": { + "description": "Text displayed on the ending dialog when game finishes to ask the user for his initials" } } \ No newline at end of file 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/flipper/left.png b/packages/pinball_components/assets/images/flipper/left.png new file mode 100644 index 00000000..42798f28 Binary files /dev/null and b/packages/pinball_components/assets/images/flipper/left.png differ diff --git a/packages/pinball_components/assets/images/flipper/right.png b/packages/pinball_components/assets/images/flipper/right.png new file mode 100644 index 00000000..86fbc81d Binary files /dev/null and b/packages/pinball_components/assets/images/flipper/right.png differ diff --git a/packages/pinball_components/assets/images/flutter_sign_post.png b/packages/pinball_components/assets/images/flutter_sign_post.png new file mode 100644 index 00000000..28a3facb Binary files /dev/null and b/packages/pinball_components/assets/images/flutter_sign_post.png differ diff --git a/packages/pinball_components/assets/images/spaceship_bridge.png b/packages/pinball_components/assets/images/spaceship_bridge.png new file mode 100644 index 00000000..6ebb143e Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship_bridge.png differ diff --git a/packages/pinball_components/assets/images/spaceship_saucer.png b/packages/pinball_components/assets/images/spaceship_saucer.png new file mode 100644 index 00000000..4cd65522 Binary files /dev/null and b/packages/pinball_components/assets/images/spaceship_saucer.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index c4ed6ca0..54b0ff53 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -3,12 +3,41 @@ /// FlutterGen /// ***************************************************** +// ignore_for_file: directives_ordering,unnecessary_import + import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); + /// File path: assets/images/ball.png AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); + + $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); + + /// File path: assets/images/flutter_sign_post.png + AssetGenImage get flutterSignPost => + const AssetGenImage('assets/images/flutter_sign_post.png'); + + /// File path: assets/images/spaceship_bridge.png + AssetGenImage get spaceshipBridge => + const AssetGenImage('assets/images/spaceship_bridge.png'); + + /// File path: assets/images/spaceship_saucer.png + AssetGenImage get spaceshipSaucer => + const AssetGenImage('assets/images/spaceship_saucer.png'); +} + +class $AssetsImagesFlipperGen { + const $AssetsImagesFlipperGen(); + + /// File path: assets/images/flipper/left.png + AssetGenImage get left => + const AssetGenImage('assets/images/flipper/left.png'); + + /// File path: assets/images/flipper/right.png + AssetGenImage get right => + const AssetGenImage('assets/images/flipper/right.png'); } class Assets { diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index 2ceb56d7..b62ceeba 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -6,7 +6,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template ball} -/// A solid, [BodyType.dynamic] sphere that rolls and bounces around +/// A solid, [BodyType.dynamic] sphere that rolls and bounces around. /// {@endtemplate} class Ball extends BodyComponent with Layered, InitialPosition { @@ -23,13 +23,14 @@ class Ball extends BodyComponent } /// The size of the [Ball] - 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,11 +89,27 @@ class Ball extends BodyComponent unawaited(gameRef.add(effect)); } + + _rescale(); } - /// Applies a boost on this [Ball] + /// Applies a boost on this [Ball]. void boost(Vector2 impulse) { 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/lib/game/components/board_side.dart b/packages/pinball_components/lib/src/components/board_side.dart similarity index 77% rename from lib/game/components/board_side.dart rename to packages/pinball_components/lib/src/components/board_side.dart index 2ef8d651..ac530567 100644 --- a/lib/game/components/board_side.dart +++ b/packages/pinball_components/lib/src/components/board_side.dart @@ -1,4 +1,8 @@ -import 'package:pinball/game/game.dart'; +// ignore_for_file: comment_references +// TODO(alestiago): Revisit ignore lint rule once Kicker is moved to this +// package. + +import 'package:pinball_components/pinball_components.dart'; /// Indicates a side of the board. /// diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index a55f9566..84f956b2 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,5 +1,12 @@ export 'ball.dart'; +export 'board_dimensions.dart'; +export 'board_side.dart'; export 'fire_effect.dart'; +export 'flipper.dart'; +export 'flutter_sign_post.dart'; export 'initial_position.dart'; +export 'joint_anchor.dart'; export 'layer.dart'; +export 'ramp_opening.dart'; export 'shapes/shapes.dart'; +export 'spaceship.dart'; diff --git a/lib/game/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart similarity index 70% rename from lib/game/components/flipper.dart rename to packages/pinball_components/lib/src/components/flipper.dart index 6e64c781..49bd6d6f 100644 --- a/lib/game/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -3,20 +3,7 @@ import 'dart:math' as math; 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/gen/assets.gen.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; - -const _leftFlipperKeys = [ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.keyA, -]; - -const _rightFlipperKeys = [ - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.keyD, -]; +import 'package:pinball_components/pinball_components.dart'; /// {@template flipper} /// A bat, typically found in pairs at the bottom of the board. @@ -27,10 +14,10 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// {@macro flipper} Flipper({ required this.side, - }) : _keys = side.isLeft ? _leftFlipperKeys : _rightFlipperKeys; + }); /// The size of the [Flipper]. - static final size = Vector2(12, 2.8); + static final size = Vector2(13.5, 4.3); /// The speed required to move the [Flipper] to its highest position. /// @@ -43,27 +30,24 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion. final BoardSide side; - /// The [LogicalKeyboardKey]s that will control the [Flipper]. - /// - /// [onKeyEvent] method listens to when one of these keys is pressed. - final List _keys; - /// Applies downward linear velocity to the [Flipper], moving it to its /// resting position. - void _moveDown() { + void moveDown() { body.linearVelocity = Vector2(0, -_speed); } /// Applies upward linear velocity to the [Flipper], moving it to its highest /// position. - void _moveUp() { + void moveUp() { body.linearVelocity = Vector2(0, _speed); } /// Loads the sprite that renders with the [Flipper]. Future _loadSprite() async { final sprite = await gameRef.loadSprite( - Assets.images.components.flipper.path, + (side.isLeft) + ? Assets.images.flipper.left.keyName + : Assets.images.flipper.right.keyName, ); final spriteComponent = SpriteComponent( sprite: sprite, @@ -71,10 +55,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { anchor: Anchor.center, ); - if (side.isRight) { - spriteComponent.flipHorizontally(); - } - await add(spriteComponent); } @@ -87,30 +67,36 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { flipper: this, anchor: anchor, ); - final joint = _FlipperJoint(jointDef)..create(world); - - // FIXME(erickzanardo): when mounted the initial position is not fully - // reached. - unawaited( - mounted.whenComplete(joint.unlock), - ); + final joint = _FlipperJoint(jointDef); + world.createJoint(joint); + unawaited(mounted.whenComplete(joint.unlock)); } List _createFixtureDefs() { final fixturesDef = []; final direction = side.direction; - final bigCircleShape = CircleShape()..radius = 1.75; + final assetShadow = Flipper.size.x * 0.012 * -direction; + final size = Vector2( + Flipper.size.x - (assetShadow * 2), + Flipper.size.y, + ); + + final bigCircleShape = CircleShape()..radius = size.y / 2 - 0.2; bigCircleShape.position.setValues( - ((size.x / 2) * direction) + (bigCircleShape.radius * -direction), + ((size.x / 2) * direction) + + (bigCircleShape.radius * -direction) + + assetShadow, 0, ); final bigCircleFixtureDef = FixtureDef(bigCircleShape); fixturesDef.add(bigCircleFixtureDef); - final smallCircleShape = CircleShape()..radius = 0.9; + final smallCircleShape = CircleShape()..radius = size.y * 0.23; smallCircleShape.position.setValues( - ((size.x / 2) * -direction) + (smallCircleShape.radius * direction), + ((size.x / 2) * -direction) + + (smallCircleShape.radius * direction) - + assetShadow, 0, ); final smallCircleFixtureDef = FixtureDef(smallCircleShape); @@ -143,7 +129,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { await super.onLoad(); renderBody = false; - await Future.wait([ + await Future.wait([ _loadSprite(), _anchorToJoint(), ]); @@ -160,22 +146,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { return body; } - - @override - bool onKeyEvent( - RawKeyEvent event, - Set keysPressed, - ) { - if (!_keys.contains(event.logicalKey)) return true; - - if (event is RawKeyDownEvent) { - _moveUp(); - } else if (event is RawKeyUpEvent) { - _moveDown(); - } - - return false; - } } /// {@template flipper_anchor} @@ -204,45 +174,48 @@ class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef { required Flipper flipper, required _FlipperAnchor anchor, }) : side = flipper.side { + enableLimit = true; initialize( flipper.body, anchor.body, anchor.body.position, ); - - enableLimit = true; - final angle = (_sweepingAngle * -side.direction) / 2; - lowerAngle = upperAngle = angle; } - /// The total angle of the arc motion. - static const _sweepingAngle = math.pi / 3.5; - final BoardSide side; } +/// {@template flipper_joint} +/// [RevoluteJoint] that controls the arc motion of a [Flipper]. +/// {@endtemplate} class _FlipperJoint extends RevoluteJoint { + /// {@macro flipper_joint} _FlipperJoint(_FlipperAnchorRevoluteJointDef def) : side = def.side, - super(def); + super(def) { + lock(); + } + + /// The total angle of the arc motion. + static const _sweepingAngle = math.pi / 3.5; final BoardSide side; - // TODO(alestiago): Remove once Forge2D supports custom joints. - void create(World world) { - world.joints.add(this); - bodyA.joints.add(this); - bodyB.joints.add(this); + /// Locks the [Flipper] to its resting position. + /// + /// The joint is locked when initialized in order to force the [Flipper] + /// at its resting position. + void lock() { + const angle = _sweepingAngle / 2; + setLimits( + -angle * side.direction, + -angle * side.direction, + ); } /// Unlocks the [Flipper] from its resting position. - /// - /// The [Flipper] is locked when initialized in order to force it to be at - /// its resting position. void unlock() { - setLimits( - lowerLimit * side.direction, - -upperLimit * side.direction, - ); + const angle = _sweepingAngle / 2; + setLimits(-angle, angle); } } diff --git a/packages/pinball_components/lib/src/components/flutter_sign_post.dart b/packages/pinball_components/lib/src/components/flutter_sign_post.dart new file mode 100644 index 00000000..deaceb76 --- /dev/null +++ b/packages/pinball_components/lib/src/components/flutter_sign_post.dart @@ -0,0 +1,41 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template flutter_sign_post} +/// A sign, found in the FlutterForest. +/// {@endtemplate} +// TODO(alestiago): Revisit doc comment if FlutterForest is moved to package. +class FlutterSignPost extends BodyComponent with InitialPosition { + Future _loadSprite() async { + final sprite = await gameRef.loadSprite( + Assets.images.flutterSignPost.keyName, + ); + final spriteComponent = SpriteComponent( + sprite: sprite, + size: sprite.originalSize / 10, + anchor: Anchor.bottomCenter, + position: Vector2(0.65, 0.45), + ); + await add(spriteComponent); + } + + @override + Future onLoad() async { + await super.onLoad(); + paint = Paint() + ..color = Colors.blue.withOpacity(0.5) + ..style = PaintingStyle.fill; + await _loadSprite(); + } + + @override + Body createBody() { + final shape = CircleShape()..radius = 0.25; + final fixtureDef = FixtureDef(shape); + final bodyDef = BodyDef()..position = initialPosition; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/lib/game/components/joint_anchor.dart b/packages/pinball_components/lib/src/components/joint_anchor.dart similarity index 100% rename from lib/game/components/joint_anchor.dart rename to packages/pinball_components/lib/src/components/joint_anchor.dart diff --git a/packages/pinball_components/lib/src/components/layer.dart b/packages/pinball_components/lib/src/components/layer.dart index a3f11f46..e0e64ddc 100644 --- a/packages/pinball_components/lib/src/components/layer.dart +++ b/packages/pinball_components/lib/src/components/layer.dart @@ -61,6 +61,9 @@ enum Layer { /// Collide only with Spaceship group elements. spaceship, + + /// Collide only with Spaceship exit rail group elements. + spaceshipExitRail, } /// {@template layer_mask_bits} @@ -89,6 +92,8 @@ extension LayerMaskBits on Layer { return 0x0005; case Layer.spaceship: return 0x000A; + case Layer.spaceshipExitRail: + return 0x0004; } } } diff --git a/lib/game/components/ramp_opening.dart b/packages/pinball_components/lib/src/components/ramp_opening.dart similarity index 94% rename from lib/game/components/ramp_opening.dart rename to packages/pinball_components/lib/src/components/ramp_opening.dart index ee1ecdea..8f33e813 100644 --- a/lib/game/components/ramp_opening.dart +++ b/packages/pinball_components/lib/src/components/ramp_opening.dart @@ -1,17 +1,16 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template ramp_orientation} -/// Determines if a ramp is facing [up] or [down] on the [Board]. +/// Determines if a ramp is facing [up] or [down] on the Board. /// {@endtemplate} enum RampOrientation { - /// Facing up on the [Board]. + /// Facing up on the Board. up, - /// Facing down on the [Board]. + /// Facing down on the Board. down, } diff --git a/lib/game/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart similarity index 55% rename from lib/game/components/spaceship.dart rename to packages/pinball_components/lib/src/components/spaceship.dart index 847e4ac8..7e9d097e 100644 --- a/lib/game/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -5,23 +5,24 @@ import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/flame/blueprint.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball/gen/assets.gen.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +/// {@template spaceship} /// A [Blueprint] which creates the spaceship feature. +/// {@endtemplate} class Spaceship extends Forge2DBlueprint { - /// Total size of the spaceship - static const radius = 10.0; + /// {@macro spaceship} + Spaceship({required this.position}); + + /// Total size of the spaceship. + static final size = Vector2(25, 19); + + /// The [position] where the elements will be created + final Vector2 position; @override void build(_) { - final position = Vector2( - PinballGame.boardBounds.left + radius + 15, - PinballGame.boardBounds.center.dy + 30, - ); - addAllContactCallback([ SpaceshipHoleBallContactCallback(), SpaceshipEntranceBallContactCallback(), @@ -30,10 +31,12 @@ class Spaceship extends Forge2DBlueprint { addAll([ SpaceshipSaucer()..initialPosition = position, SpaceshipEntrance()..initialPosition = position, - SpaceshipBridge()..initialPosition = position, - SpaceshipBridgeTop()..initialPosition = position + Vector2(0, 5.5), - SpaceshipHole()..initialPosition = position - Vector2(5, 4), - SpaceshipHole()..initialPosition = position - Vector2(-5, 4), + AndroidHead()..initialPosition = position, + SpaceshipHole( + onExitLayer: Layer.spaceshipExitRail, + onExitElevation: 2, + )..initialPosition = position - Vector2(5.2, 4.8), + SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8), SpaceshipWall()..initialPosition = position, ]); } @@ -44,32 +47,23 @@ class Spaceship extends Forge2DBlueprint { /// {@endtemplate} class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_saucer} - SpaceshipSaucer() : super(priority: 2) { + // TODO(ruimiguel): apply Elevated when PR merged. + SpaceshipSaucer() : super(priority: 3) { layer = Layer.spaceship; } @override Future onLoad() async { await super.onLoad(); - final sprites = await Future.wait([ - gameRef.loadSprite(Assets.images.components.spaceship.saucer.path), - gameRef.loadSprite(Assets.images.components.spaceship.upper.path), - ]); - - await add( - SpriteComponent( - sprite: sprites.first, - size: Vector2.all(Spaceship.radius * 2), - anchor: Anchor.center, - ), + final sprite = await gameRef.loadSprite( + Assets.images.spaceshipSaucer.keyName, ); await add( SpriteComponent( - sprite: sprites.last, - size: Vector2((Spaceship.radius * 2) + 0.5, Spaceship.radius), + sprite: sprite, + size: Spaceship.size, anchor: Anchor.center, - position: Vector2(0, -((Spaceship.radius * 2) / 3.5)), ), ); @@ -78,7 +72,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { @override Body createBody() { - final circleShape = CircleShape()..radius = Spaceship.radius; + final circleShape = CircleShape()..radius = 3; final bodyDef = BodyDef() ..userData = this @@ -92,48 +86,13 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { } } -/// {@spaceship_bridge_top} -/// The bridge of the spaceship (the android head) is divided in two -// [BodyComponent]s, this is the top part of it which contains a single sprite -/// {@endtemplate} -class SpaceshipBridgeTop extends BodyComponent with InitialPosition { - /// {@macro spaceship_bridge_top} - SpaceshipBridgeTop() : super(priority: 6); - - @override - Future onLoad() async { - await super.onLoad(); - - final sprite = await gameRef.loadSprite( - Assets.images.components.spaceship.androidTop.path, - ); - await add( - SpriteComponent( - sprite: sprite, - anchor: Anchor.center, - size: Vector2((Spaceship.radius * 2) / 2.5 - 1, Spaceship.radius / 2.5), - ), - ); - } - - @override - Body createBody() { - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..type = BodyType.static; - - return world.createBody(bodyDef); - } -} - /// {@template spaceship_bridge} -/// The main part of the [SpaceshipBridge], this [BodyComponent] -/// provides both the collision and the rotation animation for the bridge. +/// A [BodyComponent] that provides both the collision and the rotation +/// animation for the bridge. /// {@endtemplate} -class SpaceshipBridge extends BodyComponent with InitialPosition, Layered { +class AndroidHead extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_bridge} - SpaceshipBridge() : super(priority: 3) { + AndroidHead() : super(priority: 4) { layer = Layer.spaceship; } @@ -144,17 +103,20 @@ class SpaceshipBridge extends BodyComponent with InitialPosition, Layered { renderBody = false; final sprite = await gameRef.images.load( - Assets.images.components.spaceship.androidBottom.path, + Assets.images.spaceshipBridge.keyName, ); + await add( SpriteAnimationComponent.fromFrameData( sprite, SpriteAnimationData.sequenced( - amount: 14, - stepTime: 0.2, - textureSize: Vector2(160, 114), + amount: 72, + amountPerRow: 24, + stepTime: 0.05, + textureSize: Vector2(82, 100), ), - size: Vector2.all((Spaceship.radius * 2) / 2.5), + size: Vector2(8.2, 10), + position: Vector2(0, -2), anchor: Anchor.center, ), ); @@ -162,7 +124,7 @@ class SpaceshipBridge extends BodyComponent with InitialPosition, Layered { @override Body createBody() { - final circleShape = CircleShape()..radius = Spaceship.radius / 2.5; + final circleShape = CircleShape()..radius = 2; final bodyDef = BodyDef() ..userData = this @@ -191,9 +153,14 @@ class SpaceshipEntrance extends RampOpening { layer = Layer.spaceship; } + /// Priority order for [SpaceshipHole] on enter. + // TODO(ruimiguel): apply Elevated when PR merged. + final int onEnterElevation = 4; + @override Shape get shape { - const radius = Spaceship.radius * 2; + renderBody = false; + final radius = Spaceship.size.y / 2; return PolygonShape() ..setAsEdge( Vector2( @@ -210,28 +177,53 @@ class SpaceshipEntrance extends RampOpening { /// {@template spaceship_hole} /// A sensor [BodyComponent] responsible for sending the [Ball] -/// back to the board. +/// out from the [Spaceship]. /// {@endtemplate} -class SpaceshipHole extends BodyComponent with InitialPosition, Layered { +class SpaceshipHole extends RampOpening { /// {@macro spaceship_hole} - SpaceshipHole() { + SpaceshipHole({Layer? onExitLayer, this.onExitElevation = 1}) + : super( + pathwayLayer: Layer.spaceship, + outsideLayer: onExitLayer, + orientation: RampOrientation.up, + ) { layer = Layer.spaceship; } - @override - Body createBody() { - renderBody = false; - final circleShape = CircleShape()..radius = Spaceship.radius / 40; + /// Priority order for [SpaceshipHole] on exit. + // TODO(ruimiguel): apply Elevated when PR merged. + final int onExitElevation; - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..type = BodyType.static; + @override + Shape get shape { + return ArcShape( + center: Vector2(0, 4.2), + arcRadius: 6, + angle: 1, + rotation: 60 * pi / 180, + ); + } +} - return world.createBody(bodyDef) - ..createFixture( - FixtureDef(circleShape)..isSensor = true, - ); +/// {@template spaceship_wall_shape} +/// The [ChainShape] that defines the shape of the [SpaceshipWall]. +/// {@endtemplate} +class _SpaceshipWallShape extends ChainShape { + /// {@macro spaceship_wall_shape} + _SpaceshipWallShape() { + final minorRadius = (Spaceship.size.y - 2) / 2; + final majorRadius = (Spaceship.size.x - 2) / 2; + + createChain( + [ + // TODO(alestiago): Try converting this logic to radian. + for (var angle = 20; angle <= 340; angle++) + Vector2( + minorRadius * cos(angle * pi / 180), + majorRadius * sin(angle * pi / 180), + ), + ], + ); } } @@ -243,48 +235,21 @@ class SpaceshipHole extends BodyComponent with InitialPosition, Layered { /// {@endtemplate} class SpaceshipWall extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_wall} + // TODO(ruimiguel): apply Elevated when PR merged SpaceshipWall() : super(priority: 4) { layer = Layer.spaceship; } - @override - Future onLoad() async { - await super.onLoad(); - - final sprite = await gameRef.loadSprite( - Assets.images.components.spaceship.lower.path, - ); - - await add( - SpriteComponent( - sprite: sprite, - size: Vector2(Spaceship.radius * 2, Spaceship.radius + 1), - anchor: Anchor.center, - position: Vector2(-Spaceship.radius / 2, 0), - angle: 90 * pi / 180, - ), - ); - } - @override Body createBody() { renderBody = false; - final wallShape = ChainShape() - ..createChain( - [ - for (var angle = 20; angle <= 340; angle++) - Vector2( - Spaceship.radius * cos(angle * pi / 180), - Spaceship.radius * sin(angle * pi / 180), - ), - ], - ); + final wallShape = _SpaceshipWallShape(); final bodyDef = BodyDef() ..userData = this ..position = initialPosition - ..angle = 90 * pi / 180 + ..angle = 90 * pi / 172 ..type = BodyType.static; return world.createBody(bodyDef) @@ -304,7 +269,8 @@ class SpaceshipEntranceBallContactCallback @override void begin(SpaceshipEntrance entrance, Ball ball, _) { ball - ..priority = 3 + // TODO(ruimiguel): apply Elevated when PR merged. + ..priority = entrance.onEnterElevation ..gameRef.reorderChildren() ..layer = Layer.spaceship; } @@ -313,15 +279,16 @@ class SpaceshipEntranceBallContactCallback /// [ContactCallback] that handles the contact between the [Ball] /// and a [SpaceshipHole]. /// -/// It resets the [Ball] priority and filter data so it will "be back" on the +/// It sets the [Ball] priority and filter data so it will "be back" on the /// board. class SpaceshipHoleBallContactCallback extends ContactCallback { @override void begin(SpaceshipHole hole, Ball ball, _) { ball - ..priority = 1 + // TODO(ruimiguel): apply Elevated when PR merged. + ..priority = hole.onExitElevation ..gameRef.reorderChildren() - ..layer = Layer.board; + ..layer = hole.outsideLayer; } } diff --git a/lib/flame/blueprint.dart b/packages/pinball_components/lib/src/flame/blueprint.dart similarity index 100% rename from lib/flame/blueprint.dart rename to packages/pinball_components/lib/src/flame/blueprint.dart diff --git a/packages/pinball_components/lib/src/flame/flame.dart b/packages/pinball_components/lib/src/flame/flame.dart new file mode 100644 index 00000000..c46a6fd2 --- /dev/null +++ b/packages/pinball_components/lib/src/flame/flame.dart @@ -0,0 +1 @@ +export 'blueprint.dart'; diff --git a/packages/pinball_components/lib/src/pinball_components.dart b/packages/pinball_components/lib/src/pinball_components.dart index 003c1c49..bd8f99de 100644 --- a/packages/pinball_components/lib/src/pinball_components.dart +++ b/packages/pinball_components/lib/src/pinball_components.dart @@ -1 +1,2 @@ export 'components/components.dart'; +export 'flame/flame.dart'; diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 1c8dfbe3..aa1eb224 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -7,8 +7,8 @@ environment: sdk: ">=2.16.0 <3.0.0" dependencies: - flame: ^1.1.0-releasecandidate.6 - flame_forge2d: ^0.9.0-releasecandidate.6 + flame: ^1.1.0 + flame_forge2d: ^0.10.0 flutter: sdk: flutter geometry: @@ -16,7 +16,7 @@ dependencies: dev_dependencies: - flame_test: ^1.1.0 + flame_test: ^1.3.0 flutter_test: sdk: flutter mocktail: ^0.2.0 @@ -26,6 +26,7 @@ flutter: generate: true assets: - assets/images/ + - assets/images/flipper/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/common/games.dart b/packages/pinball_components/sandbox/lib/common/games.dart index bce1ff90..4aae07cb 100644 --- a/packages/pinball_components/sandbox/lib/common/games.dart +++ b/packages/pinball_components/sandbox/lib/common/games.dart @@ -5,7 +5,7 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; -class BasicGame extends Forge2DGame { +abstract class BasicGame extends Forge2DGame { BasicGame() { images.prefix = ''; } diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 44b594d7..2df3c16c 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -7,6 +7,7 @@ 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'; void main() { @@ -15,5 +16,7 @@ void main() { addBallStories(dashbook); addLayerStories(dashbook); addEffectsStories(dashbook); + addFlipperStories(dashbook); + addSpaceshipStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/ball/basic.dart b/packages/pinball_components/sandbox/lib/stories/ball/basic.dart index f133ee3f..73890519 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/basic.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/basic.dart @@ -7,8 +7,9 @@ class BasicBallGame extends BasicGame with TapDetector { BasicBallGame({required this.color}); static const info = ''' - Basic example of how a Ball works, tap anywhere on the - screen to spawn a ball into the game. + Basic example of how a Ball works. + + Tap anywhere on the screen to spawn a ball into the game. '''; final Color color; diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/basic.dart b/packages/pinball_components/sandbox/lib/stories/flipper/basic.dart new file mode 100644 index 00000000..0e5587ea --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/flipper/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 BasicFlipperGame extends BasicGame { + static const info = ''' + Basic example of how a Flipper works. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + final center = screenToWorld(camera.viewport.canvasSize! / 2); + + final leftFlipper = Flipper(side: BoardSide.left) + ..initialPosition = center - Vector2(Flipper.size.x, 0); + final rightFlipper = Flipper(side: BoardSide.right) + ..initialPosition = center + Vector2(Flipper.size.x, 0); + + await addAll([ + leftFlipper, + rightFlipper, + ]); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/flipper.dart b/packages/pinball_components/sandbox/lib/stories/flipper/flipper.dart new file mode 100644 index 00000000..7c8465da --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/flipper/flipper.dart @@ -0,0 +1,25 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/flipper/basic.dart'; +import 'package:sandbox/stories/flipper/tracing.dart'; + +void addFlipperStories(Dashbook dashbook) { + dashbook.storiesOf('Flipper') + ..add( + 'Basic', + (context) => GameWidget( + game: BasicFlipperGame(), + ), + codeLink: buildSourceLink('flipper/basic.dart'), + info: BasicFlipperGame.info, + ) + ..add( + 'Tracing', + (context) => GameWidget( + game: FlipperTracingGame(), + ), + codeLink: buildSourceLink('flipper/tracing.dart'), + info: FlipperTracingGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/tracing.dart b/packages/pinball_components/sandbox/lib/stories/flipper/tracing.dart new file mode 100644 index 00000000..d6c5d3df --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/flipper/tracing.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class FlipperTracingGame extends BasicGame { + static const info = ''' + Basic example of how the Flipper body overlays the sprite. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + final center = screenToWorld(camera.viewport.canvasSize! / 2); + + final leftFlipper = Flipper(side: BoardSide.left) + ..initialPosition = center - Vector2(Flipper.size.x, 0); + final rightFlipper = Flipper(side: BoardSide.right) + ..initialPosition = center + Vector2(Flipper.size.x, 0); + + await addAll([ + leftFlipper, + rightFlipper, + ]); + leftFlipper.trace(); + rightFlipper.trace(); + } +} + +extension on BodyComponent { + void trace({Color color = Colors.red}) { + paint = Paint()..color = color; + renderBody = true; + body.joints.whereType().forEach( + (joint) => joint.setLimits(0, 0), + ); + body.setType(BodyType.static); + + unawaited( + mounted.whenComplete(() { + final sprite = children.whereType().first; + sprite.paint.color = sprite.paint.color.withOpacity(0.5); + }), + ); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/layer/basic.dart b/packages/pinball_components/sandbox/lib/stories/layer/basic.dart index 89ef337f..ccbd67d9 100644 --- a/packages/pinball_components/sandbox/lib/stories/layer/basic.dart +++ b/packages/pinball_components/sandbox/lib/stories/layer/basic.dart @@ -8,8 +8,9 @@ class BasicLayerGame extends BasicGame with TapDetector { BasicLayerGame({required this.color}); static const info = ''' - Basic example of how layers work with a Ball hitting other components, - tap anywhere on the screen to spawn a ball into the game. + Basic example of how layers work when a Ball hits other components. + + Tap anywhere on the screen to spawn a ball into the game. '''; final Color color; diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship/basic.dart b/packages/pinball_components/sandbox/lib/stories/spaceship/basic.dart new file mode 100644 index 00000000..6f33f444 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/spaceship/basic.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BasicSpaceship extends BasicGame with TapDetector { + static String info = 'Renders a spaceship and allows balls to be ' + 'spawned upon click to test their interactions'; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + + unawaited( + addFromBlueprint(Spaceship(position: Vector2.zero())), + ); + } + + @override + void onTapUp(TapUpInfo info) { + add( + Ball(baseColor: Colors.blue) + ..initialPosition = info.eventPosition.game + ..layer = Layer.jetpack, + ); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/spaceship/spaceship.dart b/packages/pinball_components/sandbox/lib/stories/spaceship/spaceship.dart new file mode 100644 index 00000000..635439ee --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/spaceship/spaceship.dart @@ -0,0 +1,13 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/spaceship/basic.dart'; + +void addSpaceshipStories(Dashbook dashbook) { + dashbook.storiesOf('Spaceship').add( + 'Basic', + (context) => GameWidget(game: BasicSpaceship()), + codeLink: buildSourceLink('spaceship/basic.dart'), + info: BasicSpaceship.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 1135fbaf..9f861bde 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,2 +1,3 @@ export 'ball/ball.dart'; +export 'flipper/flipper.dart'; export 'layer/layer.dart'; diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index bb132da7..b0de4903 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -91,14 +91,14 @@ packages: name: flame url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-releasecandidate.6" + version: "1.1.0" flame_forge2d: dependency: "direct main" description: name: flame_forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.9.0-releasecandidate.6" + version: "0.10.0" flutter: dependency: "direct main" description: flutter @@ -134,7 +134,7 @@ packages: name: forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.9.0" + version: "0.10.0" freezed_annotation: dependency: transitive description: diff --git a/packages/pinball_components/sandbox/pubspec.yaml b/packages/pinball_components/sandbox/pubspec.yaml index 0c8267a8..94c0479b 100644 --- a/packages/pinball_components/sandbox/pubspec.yaml +++ b/packages/pinball_components/sandbox/pubspec.yaml @@ -8,8 +8,8 @@ environment: dependencies: dashbook: ^0.1.7 - flame: ^1.1.0-releasecandidate.6 - flame_forge2d: ^0.9.0-releasecandidate.6 + flame: ^1.1.0 + flame_forge2d: ^0.10.0 flutter: sdk: flutter pinball_components: diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index 67df9918..8d6f45b3 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -1,5 +1,26 @@ import 'dart:ui'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; class MockCanvas extends Mock implements Canvas {} + +class MockFilter extends Mock implements Filter {} + +class MockFixture extends Mock implements Fixture {} + +class MockBody extends Mock implements Body {} + +class MockBall extends Mock implements Ball {} + +class MockGame extends Mock implements Forge2DGame {} + +class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {} + +class MockSpaceshipHole extends Mock implements SpaceshipHole {} + +class MockContact extends Mock implements Contact {} + +class MockContactCallback extends Mock + implements ContactCallback {} 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/components/board_side_test.dart b/packages/pinball_components/test/src/components/board_side_test.dart similarity index 92% rename from test/game/components/board_side_test.dart rename to packages/pinball_components/test/src/components/board_side_test.dart index ba201065..7c17828d 100644 --- a/test/game/components/board_side_test.dart +++ b/packages/pinball_components/test/src/components/board_side_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; void main() { group( diff --git a/packages/pinball_components/test/src/components/flipper_test.dart b/packages/pinball_components/test/src/components/flipper_test.dart new file mode 100644 index 00000000..efd4d2b0 --- /dev/null +++ b/packages/pinball_components/test/src/components/flipper_test.dart @@ -0,0 +1,133 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('Flipper', () { + // TODO(alestiago): Add golden tests. + // TODO(alestiago): Consider testing always both left and right Flipper. + + flameTester.test( + 'loads correctly', + (game) async { + final leftFlipper = Flipper(side: BoardSide.left); + final rightFlipper = Flipper(side: BoardSide.right); + await game.ready(); + await game.ensureAddAll([leftFlipper, rightFlipper]); + + expect(game.contains(leftFlipper), isTrue); + expect(game.contains(rightFlipper), isTrue); + }, + ); + + group('constructor', () { + test('sets BoardSide', () { + final leftFlipper = Flipper(side: BoardSide.left); + expect(leftFlipper.side, equals(leftFlipper.side)); + + final rightFlipper = Flipper(side: BoardSide.right); + expect(rightFlipper.side, equals(rightFlipper.side)); + }); + }); + + group('body', () { + flameTester.test( + 'is dynamic', + (game) async { + final flipper = Flipper(side: BoardSide.left); + await game.ensureAdd(flipper); + expect(flipper.body.bodyType, equals(BodyType.dynamic)); + }, + ); + + flameTester.test( + 'ignores gravity', + (game) async { + final flipper = Flipper(side: BoardSide.left); + await game.ensureAdd(flipper); + + expect(flipper.body.gravityScale, isZero); + }, + ); + + flameTester.test( + 'has greater mass than Ball', + (game) async { + final flipper = Flipper(side: BoardSide.left); + final ball = Ball(baseColor: Colors.white); + + await game.ready(); + await game.ensureAddAll([flipper, ball]); + + expect( + flipper.body.getMassData().mass, + greaterThan(ball.body.getMassData().mass), + ); + }, + ); + }); + + group('fixtures', () { + flameTester.test( + 'has three', + (game) async { + final flipper = Flipper(side: BoardSide.left); + await game.ensureAdd(flipper); + + expect(flipper.body.fixtures.length, equals(3)); + }, + ); + + flameTester.test( + 'has density', + (game) async { + final flipper = Flipper(side: BoardSide.left); + await game.ensureAdd(flipper); + + final fixtures = flipper.body.fixtures; + final density = fixtures.fold( + 0, + (sum, fixture) => sum + fixture.density, + ); + + expect(density, greaterThan(0)); + }, + ); + }); + + flameTester.test( + 'moveDown applies downward velocity', + (game) async { + final flipper = Flipper(side: BoardSide.left); + await game.ensureAdd(flipper); + + expect(flipper.body.linearVelocity, equals(Vector2.zero())); + flipper.moveDown(); + + expect(flipper.body.linearVelocity.y, lessThan(0)); + }, + ); + + flameTester.test( + 'moveUp applies upward velocity', + (game) async { + final flipper = Flipper(side: BoardSide.left); + await game.ensureAdd(flipper); + + expect(flipper.body.linearVelocity, equals(Vector2.zero())); + flipper.moveUp(); + + expect(flipper.body.linearVelocity.y, greaterThan(0)); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/flutter_sign_post_test.dart b/packages/pinball_components/test/src/components/flutter_sign_post_test.dart new file mode 100644 index 00000000..98815af7 --- /dev/null +++ b/packages/pinball_components/test/src/components/flutter_sign_post_test.dart @@ -0,0 +1,25 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('FlutterSignPost', () { + flameTester.test( + 'loads correctly', + (game) async { + final flutterSignPost = FlutterSignPost(); + await game.ready(); + await game.ensureAdd(flutterSignPost); + + expect(game.contains(flutterSignPost), isTrue); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/golden/spaceship.png b/packages/pinball_components/test/src/components/golden/spaceship.png new file mode 100644 index 00000000..da665718 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/spaceship.png differ diff --git a/test/game/components/joint_anchor_test.dart b/packages/pinball_components/test/src/components/joint_anchor_test.dart similarity index 94% rename from test/game/components/joint_anchor_test.dart rename to packages/pinball_components/test/src/components/joint_anchor_test.dart index 652bd445..f7c341dd 100644 --- a/test/game/components/joint_anchor_test.dart +++ b/packages/pinball_components/test/src/components/joint_anchor_test.dart @@ -3,7 +3,7 @@ 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'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/test/game/components/ramp_opening_test.dart b/packages/pinball_components/test/src/components/ramp_opening_test.dart similarity index 97% rename from test/game/components/ramp_opening_test.dart rename to packages/pinball_components/test/src/components/ramp_opening_test.dart index 11cf8ddc..c49e9164 100644 --- a/test/game/components/ramp_opening_test.dart +++ b/packages/pinball_components/test/src/components/ramp_opening_test.dart @@ -2,8 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/game/game.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; @@ -34,11 +33,11 @@ class TestRampOpeningBallContactCallback void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.create); + final flameTester = FlameTester(TestGame.new); group('RampOpening', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.create); + final flameTester = FlameTester(TestGame.new); flameTester.test( 'loads correctly', diff --git a/test/game/components/spaceship_test.dart b/packages/pinball_components/test/src/components/spaceship_test.dart similarity index 59% rename from test/game/components/spaceship_test.dart rename to packages/pinball_components/test/src/components/spaceship_test.dart index 52b12609..f89408f7 100644 --- a/test/game/components/spaceship_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_test.dart @@ -1,7 +1,9 @@ +// 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/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; @@ -11,10 +13,10 @@ void main() { late Filter filterData; late Fixture fixture; late Body body; - late PinballGame game; late Ball ball; late SpaceshipEntrance entrance; late SpaceshipHole hole; + late Forge2DGame game; setUp(() { filterData = MockFilter(); @@ -25,7 +27,7 @@ void main() { body = MockBody(); when(() => body.fixtures).thenReturn([fixture]); - game = MockPinballGame(); + game = MockGame(); ball = MockBall(); when(() => ball.gameRef).thenReturn(game); @@ -35,18 +37,42 @@ void main() { hole = MockSpaceshipHole(); }); + group('Spaceship', () { + final tester = FlameTester(TestGame.new); + + tester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.addFromBlueprint(Spaceship(position: Vector2(30, -30))); + await game.ready(); + await tester.pump(); + }, + verify: (game, tester) async { + // FIXME(erickzanardo): Failing pipeline. + // await expectLater( + // find.byGame(), + // matchesGoldenFile('golden/spaceship.png'), + // ); + }, + ); + }); + group('SpaceshipEntranceBallContactCallback', () { test('changes the ball priority on contact', () { + when(() => entrance.onEnterElevation).thenReturn(3); + SpaceshipEntranceBallContactCallback().begin( entrance, ball, MockContact(), ); - verify(() => ball.priority = 3).called(1); + verify(() => ball.priority = entrance.onEnterElevation).called(1); }); test('re order the game children', () { + when(() => entrance.onEnterElevation).thenReturn(3); + SpaceshipEntranceBallContactCallback().begin( entrance, ball, @@ -59,16 +85,22 @@ void main() { group('SpaceshipHoleBallContactCallback', () { test('changes the ball priority on contact', () { + when(() => hole.outsideLayer).thenReturn(Layer.board); + when(() => hole.onExitElevation).thenReturn(1); + SpaceshipHoleBallContactCallback().begin( hole, ball, MockContact(), ); - verify(() => ball.priority = 1).called(1); + verify(() => ball.priority = hole.onExitElevation).called(1); }); test('re order the game children', () { + when(() => hole.outsideLayer).thenReturn(Layer.board); + when(() => hole.onExitElevation).thenReturn(1); + SpaceshipHoleBallContactCallback().begin( hole, ball, diff --git a/test/flame/blueprint_test.dart b/packages/pinball_components/test/src/flame/blueprint_test.dart similarity index 86% rename from test/flame/blueprint_test.dart rename to packages/pinball_components/test/src/flame/blueprint_test.dart index e5fc2c4f..a9629422 100644 --- a/test/flame/blueprint_test.dart +++ b/packages/pinball_components/test/src/flame/blueprint_test.dart @@ -1,10 +1,9 @@ import 'package:flame/components.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:pinball/flame/blueprint.dart'; -import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; -import '../helpers/helpers.dart'; +import '../../helpers/helpers.dart'; class MyBlueprint extends Blueprint { @override @@ -52,19 +51,19 @@ void main() { }); test('components can be added to it', () { - final blueprint = MyBlueprint()..build(MockPinballGame()); + final blueprint = MyBlueprint()..build(MockGame()); expect(blueprint.components.length, equals(3)); }); test('blueprints can be added to it', () { - final blueprint = MyComposedBlueprint()..build(MockPinballGame()); + final blueprint = MyComposedBlueprint()..build(MockGame()); expect(blueprint.blueprints.length, equals(3)); }); test('adds the components to a game on attach', () { - final mockGame = MockPinballGame(); + final mockGame = MockGame(); when(() => mockGame.addAll(any())).thenAnswer((_) async {}); MyBlueprint().attach(mockGame); @@ -72,7 +71,7 @@ void main() { }); test('adds components from a child Blueprint the to a game on attach', () { - final mockGame = MockPinballGame(); + final mockGame = MockGame(); when(() => mockGame.addAll(any())).thenAnswer((_) async {}); MyComposedBlueprint().attach(mockGame); @@ -82,7 +81,7 @@ void main() { test( 'throws assertion error when adding to an already attached blueprint', () async { - final mockGame = MockPinballGame(); + final mockGame = MockGame(); when(() => mockGame.addAll(any())).thenAnswer((_) async {}); final blueprint = MyBlueprint(); await blueprint.attach(mockGame); @@ -99,13 +98,13 @@ void main() { }); test('callbacks can be added to it', () { - final blueprint = MyForge2dBlueprint()..build(MockPinballGame()); + final blueprint = MyForge2dBlueprint()..build(MockGame()); expect(blueprint.callbacks.length, equals(3)); }); test('adds the callbacks to a game on attach', () async { - final mockGame = MockPinballGame(); + final mockGame = MockGame(); when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {}); await MyForge2dBlueprint().attach(mockGame); @@ -116,7 +115,7 @@ void main() { test( 'throws assertion error when adding to an already attached blueprint', () async { - final mockGame = MockPinballGame(); + final mockGame = MockGame(); when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {}); final blueprint = MyForge2dBlueprint(); diff --git a/pubspec.lock b/pubspec.lock index 067559c4..ada9db4e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -182,28 +182,28 @@ packages: name: flame url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-releasecandidate.6" + version: "1.1.0" flame_bloc: dependency: "direct main" description: name: flame_bloc url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-releasecandidate.6" + version: "1.2.0" flame_forge2d: dependency: "direct main" description: name: flame_forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.9.0-releasecandidate.6" + version: "0.10.0" flame_test: dependency: "direct dev" description: name: flame_test url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -237,7 +237,7 @@ packages: name: forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.9.0" + version: "0.10.0" frontend_server_client: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7a50f962..7f5b4a83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,9 +10,9 @@ dependencies: bloc: ^8.0.2 cloud_firestore: ^3.1.10 equatable: ^2.0.3 - flame: ^1.1.0-releasecandidate.6 - flame_bloc: ^1.2.0-releasecandidate.6 - flame_forge2d: ^0.9.0-releasecandidate.6 + flame: ^1.1.0 + flame_bloc: ^1.2.0 + flame_forge2d: ^0.10.0 flutter: sdk: flutter flutter_bloc: ^8.0.1 @@ -30,7 +30,7 @@ dependencies: dev_dependencies: bloc_test: ^9.0.2 - flame_test: ^1.1.0 + flame_test: ^1.3.0 flutter_test: sdk: flutter mockingjay: ^0.2.0 diff --git a/test/flame/component_controller_test.dart b/test/flame/component_controller_test.dart new file mode 100644 index 00000000..4e5da210 --- /dev/null +++ b/test/flame/component_controller_test.dart @@ -0,0 +1,66 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/game.dart'; +import 'package:flame/src/components/component.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/flame/flame.dart'; + +class TestComponentController extends ComponentController { + TestComponentController(Component component) : super(component); +} + +class ControlledComponent extends Component + with Controls { + ControlledComponent() : super() { + controller = TestComponentController(this); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(FlameGame.new); + + group('ComponentController', () { + flameTester.test( + 'can be instantiated', + (game) async { + expect( + TestComponentController(Component()), + isA(), + ); + }, + ); + flameTester.test( + 'throws AssertionError when not attached to controlled component', + (game) async { + final component = Component(); + final controller = TestComponentController(component); + + final anotherComponet = Component(); + await expectLater( + () async => await anotherComponet.add(controller), + throwsAssertionError, + ); + }, + ); + }); + + group('Controls', () { + flameTester.test( + 'can be instantiated', + (game) async { + expect(ControlledComponent(), isA()); + }, + ); + + flameTester.test('adds controller', (game) async { + final component = ControlledComponent(); + + await game.add(component); + await game.ready(); + + expect(component.contains(component.controller), isTrue); + }); + }); +} 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/ball_test.dart b/test/game/components/ball_test.dart deleted file mode 100644 index a872dc1f..00000000 --- a/test/game/components/ball_test.dart +++ /dev/null @@ -1,84 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; - -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('Ball', () { - group('lost', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); - - final tester = flameBlocTester(gameBloc: () => gameBloc); - - tester.widgetTest( - 'adds BallLost to GameBloc', - (game, tester) async { - await game.ready(); - - game.children.whereType().first.controller.lost(); - await tester.pump(); - - verify(() => gameBloc.add(const BallLost())).called(1); - }, - ); - - tester.widgetTest( - 'resets the ball if the game is not over', - (game, tester) async { - await game.ready(); - - game.children.whereType().first.controller.lost(); - await game.ready(); // Making sure that all additions are done - - expect( - game.children.whereType().length, - equals(1), - ); - }, - ); - - tester.widgetTest( - 'no ball is added on game over', - (game, tester) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState( - score: 10, - balls: 1, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ), - ); - await game.ready(); - - game.children.whereType().first.controller.lost(); - await tester.pump(); - - expect( - game.children.whereType().length, - equals(0), - ); - }, - ); - }); - }); -} diff --git a/test/game/components/baseboard_test.dart b/test/game/components/baseboard_test.dart index f834a41e..37c3c978 100644 --- a/test/game/components/baseboard_test.dart +++ b/test/game/components/baseboard_test.dart @@ -4,6 +4,7 @@ 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'; void main() { group('Baseboard', () { diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index f6304cec..2f51b2b1 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -3,6 +3,7 @@ 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'; @@ -22,9 +23,9 @@ void main() { }, ); - group('children', () { + group('loads', () { flameTester.test( - 'has one left flipper', + 'one left flipper', (game) async { final board = Board(); await game.ready(); @@ -38,7 +39,7 @@ void main() { ); flameTester.test( - 'has one right flipper', + 'one right flipper', (game) async { final board = Board(); await game.ready(); @@ -51,7 +52,7 @@ void main() { ); flameTester.test( - 'has two Baseboards', + 'two Baseboards', (game) async { final board = Board(); await game.ready(); @@ -63,7 +64,7 @@ void main() { ); flameTester.test( - 'has two Kickers', + 'two Kickers', (game) async { final board = Board(); await game.ready(); @@ -75,7 +76,7 @@ void main() { ); flameTester.test( - 'has one FlutterForest', + 'one FlutterForest', (game) async { // TODO(alestiago): change to [NestBumpers] once provided. final board = Board(); @@ -88,7 +89,7 @@ void main() { ); flameTester.test( - 'has one ChromeDino', + 'one ChromeDino', (game) async { final board = Board(); await game.ready(); diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index 293062ae..f48d60ee 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -29,6 +29,7 @@ void main() { group('listenWhen', () { final previousState = MockGameState(); final currentState = MockGameState(); + test( 'returns true when there is a new word bonus awarded', () { @@ -193,10 +194,16 @@ void main() { }); group('bonus letter activation', () { - final gameBloc = MockGameBloc(); - final tester = flameBlocTester(gameBloc: () => gameBloc); + late GameBloc gameBloc; + + final tester = flameBlocTester( + // TODO(alestiago): Use TestGame once BonusLetter has controller. + game: PinballGameTest.create, + gameBloc: () => gameBloc, + ); setUp(() { + gameBloc = MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -204,22 +211,23 @@ void main() { ); }); - tester.widgetTest( + tester.testGameWidget( 'adds BonusLetterActivated to GameBloc when not activated', - (game, tester) async { + setUp: (game, tester) async { await game.ready(); final bonusLetter = game.descendants().whereType().first; + bonusLetter.activate(); await game.ready(); - - await tester.pump(); + }, + verify: (game, tester) async { verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1); }, ); - tester.widgetTest( + tester.testGameWidget( "doesn't add BonusLetterActivated to GameBloc when already activated", - (game, tester) async { + setUp: (game, tester) async { const state = GameState( score: 0, balls: 2, @@ -233,18 +241,21 @@ void main() { initialState: state, ); + final bonusLetter = BonusLetter(letter: '', index: 0); + await game.add(bonusLetter); await game.ready(); - final bonusLetter = game.descendants().whereType().first; + bonusLetter.activate(); await game.ready(); - + }, + verify: (game, tester) async { verifyNever(() => gameBloc.add(const BonusLetterActivated(0))); }, ); - tester.widgetTest( + tester.testGameWidget( 'adds a ColorEffect', - (game, tester) async { + setUp: (game, tester) async { const state = GameState( score: 0, balls: 2, @@ -253,13 +264,19 @@ void main() { bonusHistory: [], ); + final bonusLetter = BonusLetter(letter: '', index: 0); + await game.add(bonusLetter); await game.ready(); - final bonusLetter = game.descendants().whereType().first; + bonusLetter.activate(); bonusLetter.onNewState(state); await tester.pump(); - + }, + verify: (game, tester) async { + // TODO(aleastiago): Look into making `testGameWidget` pass the + // subject. + final bonusLetter = game.descendants().whereType().last; expect( bonusLetter.children.whereType().length, equals(1), @@ -267,9 +284,14 @@ void main() { }, ); - tester.widgetTest( + tester.testGameWidget( 'only listens when there is a change on the letter status', - (game, tester) async { + setUp: (game, tester) async { + await game.ready(); + final bonusLetter = game.descendants().whereType().first; + bonusLetter.activate(); + }, + verify: (game, tester) async { const state = GameState( score: 0, balls: 2, @@ -277,11 +299,7 @@ void main() { activatedDashNests: {}, bonusHistory: [], ); - - await game.ready(); final bonusLetter = game.descendants().whereType().first; - bonusLetter.activate(); - expect( bonusLetter.listenWhen(const GameState.initial(), state), isTrue, diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart new file mode 100644 index 00000000..dcd075ca --- /dev/null +++ b/test/game/components/controlled_ball_test.dart @@ -0,0 +1,197 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; + +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(PinballGameTest.create); + + group('BonusBallController', () { + late Ball ball; + + setUp(() { + ball = Ball(baseColor: const Color(0xFF00FFFF)); + }); + + test('can be instantiated', () { + expect( + BonusBallController(ball), + isA(), + ); + }); + + flameTester.test( + 'lost removes ball', + (game) async { + await game.add(ball); + final controller = BonusBallController(ball); + await ball.ensureAdd(controller); + + controller.lost(); + await game.ready(); + + expect(game.contains(ball), isFalse); + }, + ); + }); + + group('LaunchedBallController', () { + test('can be instantiated', () { + expect( + LaunchedBallController(MockBall()), + isA(), + ); + }); + + group('description', () { + late Ball ball; + late GameBloc gameBloc; + + setUp(() { + ball = Ball(baseColor: const Color(0xFF00FFFF)); + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final tester = flameBlocTester( + game: PinballGameTest.create, + gameBloc: () => gameBloc, + ); + + tester.testGameWidget( + 'lost adds BallLost to GameBloc', + setUp: (game, tester) async { + final controller = LaunchedBallController(ball); + await ball.add(controller); + await game.ensureAdd(ball); + + controller.lost(); + }, + verify: (game, tester) async { + verify(() => gameBloc.add(const BallLost())).called(1); + }, + ); + + 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); + }, + ); + + 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); + }, + ); + }); + + 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( + 'spawns a new ball when the ball is not the last one', + setUp: (game, tester) async { + final controller = LaunchedBallController(ball); + await ball.add(controller); + await game.ensureAdd(ball); + + final state = MockGameState(); + when(() => state.balls).thenReturn(2); + + 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/game/components/flipper_controller_test.dart b/test/game/components/flipper_controller_test.dart new file mode 100644 index 00000000..eabeca5e --- /dev/null +++ b/test/game/components/flipper_controller_test.dart @@ -0,0 +1,169 @@ +import 'dart:collection'; + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/services.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() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(PinballGameTest.create); + + group('FlipperController', () { + group('onKeyEvent', () { + final leftKeys = UnmodifiableListView([ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, + ]); + final rightKeys = UnmodifiableListView([ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, + ]); + + group('and Flipper is left', () { + late Flipper flipper; + late FlipperController controller; + + setUp(() { + flipper = Flipper(side: BoardSide.left); + controller = FlipperController(flipper); + flipper.add(controller); + }); + + testRawKeyDownEvents(leftKeys, (event) { + flameTester.test( + 'moves upwards ' + 'when ${event.logicalKey.keyLabel} is pressed', + (game) async { + await game.ready(); + await game.add(flipper); + controller.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isPositive); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyUpEvents(leftKeys, (event) { + flameTester.test( + 'moves downwards ' + 'when ${event.logicalKey.keyLabel} is released', + (game) async { + await game.ready(); + await game.add(flipper); + controller.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isNegative); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyUpEvents(rightKeys, (event) { + flameTester.test( + 'does nothing ' + 'when ${event.logicalKey.keyLabel} is released', + (game) async { + await game.ready(); + await game.add(flipper); + controller.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyDownEvents(rightKeys, (event) { + flameTester.test( + 'does nothing ' + 'when ${event.logicalKey.keyLabel} is pressed', + (game) async { + await game.ready(); + await game.add(flipper); + controller.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + }); + + group('and Flipper is right', () { + late Flipper flipper; + late FlipperController controller; + + setUp(() { + flipper = Flipper(side: BoardSide.right); + controller = FlipperController(flipper); + flipper.add(controller); + }); + + testRawKeyDownEvents(rightKeys, (event) { + flameTester.test( + 'moves upwards ' + 'when ${event.logicalKey.keyLabel} is pressed', + (game) async { + await game.ready(); + await game.add(flipper); + controller.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isPositive); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyUpEvents(rightKeys, (event) { + flameTester.test( + 'moves downwards ' + 'when ${event.logicalKey.keyLabel} is released', + (game) async { + await game.ready(); + await game.add(flipper); + controller.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isNegative); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyUpEvents(leftKeys, (event) { + flameTester.test( + 'does nothing ' + 'when ${event.logicalKey.keyLabel} is released', + (game) async { + await game.ready(); + await game.add(flipper); + controller.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyDownEvents(leftKeys, (event) { + flameTester.test( + 'does nothing ' + 'when ${event.logicalKey.keyLabel} is pressed', + (game) async { + await game.ready(); + await game.add(flipper); + controller.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + }); + }); + }); +} diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart deleted file mode 100644 index 3e6429df..00000000 --- a/test/game/components/flipper_test.dart +++ /dev/null @@ -1,275 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'dart:collection'; - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.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() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameTest.create); - - group( - 'Flipper', - () { - // TODO(alestiago): Add golden tests. - flameTester.test( - 'loads correctly', - (game) async { - final leftFlipper = Flipper( - side: BoardSide.left, - ); - final rightFlipper = Flipper( - side: BoardSide.right, - ); - await game.ready(); - await game.ensureAddAll([leftFlipper, rightFlipper]); - - expect(game.contains(leftFlipper), isTrue); - expect(game.contains(rightFlipper), isTrue); - }, - ); - - group('constructor', () { - test('sets BoardSide', () { - final leftFlipper = Flipper( - side: BoardSide.left, - ); - - expect(leftFlipper.side, equals(leftFlipper.side)); - - final rightFlipper = Flipper( - side: BoardSide.right, - ); - expect(rightFlipper.side, equals(rightFlipper.side)); - }); - }); - - group('body', () { - flameTester.test( - 'is dynamic', - (game) async { - final flipper = Flipper( - side: BoardSide.left, - ); - await game.ensureAdd(flipper); - - expect(flipper.body.bodyType, equals(BodyType.dynamic)); - }, - ); - - flameTester.test( - 'ignores gravity', - (game) async { - final flipper = Flipper( - side: BoardSide.left, - ); - await game.ensureAdd(flipper); - - expect(flipper.body.gravityScale, isZero); - }, - ); - - flameTester.test( - 'has greater mass than Ball', - (game) async { - final flipper = Flipper( - side: BoardSide.left, - ); - final ball = Ball(baseColor: Colors.white); - - await game.ready(); - await game.ensureAddAll([flipper, ball]); - - expect( - flipper.body.getMassData().mass, - greaterThan(ball.body.getMassData().mass), - ); - }, - ); - }); - - group('fixtures', () { - flameTester.test( - 'has three', - (game) async { - final flipper = Flipper( - side: BoardSide.left, - ); - await game.ensureAdd(flipper); - - expect(flipper.body.fixtures.length, equals(3)); - }, - ); - - flameTester.test( - 'has density', - (game) async { - final flipper = Flipper( - side: BoardSide.left, - ); - await game.ensureAdd(flipper); - - final fixtures = flipper.body.fixtures; - final density = fixtures.fold( - 0, - (sum, fixture) => sum + fixture.density, - ); - - expect(density, greaterThan(0)); - }, - ); - }); - - group('onKeyEvent', () { - final leftKeys = UnmodifiableListView([ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.keyA, - ]); - final rightKeys = UnmodifiableListView([ - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.keyD, - ]); - - group('and Flipper is left', () { - late Flipper flipper; - - setUp(() { - flipper = Flipper( - side: BoardSide.left, - ); - }); - - testRawKeyDownEvents(leftKeys, (event) { - flameTester.test( - 'moves upwards ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - await game.ensureAdd(flipper); - flipper.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isPositive); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(leftKeys, (event) { - flameTester.test( - 'moves downwards ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - await game.ensureAdd(flipper); - flipper.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isNegative); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(rightKeys, (event) { - flameTester.test( - 'does nothing ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - await game.ensureAdd(flipper); - flipper.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyDownEvents(rightKeys, (event) { - flameTester.test( - 'does nothing ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - await game.ensureAdd(flipper); - flipper.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - }); - - group('and Flipper is right', () { - late Flipper flipper; - - setUp(() { - flipper = Flipper( - side: BoardSide.right, - ); - }); - - testRawKeyDownEvents(rightKeys, (event) { - flameTester.test( - 'moves upwards ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - await game.ensureAdd(flipper); - flipper.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isPositive); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(rightKeys, (event) { - flameTester.test( - 'moves downwards ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - await game.ensureAdd(flipper); - flipper.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isNegative); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(leftKeys, (event) { - flameTester.test( - 'does nothing ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - await game.ensureAdd(flipper); - flipper.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyDownEvents(leftKeys, (event) { - flameTester.test( - 'does nothing ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - await game.ensureAdd(flipper); - flipper.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - }); - }); - }, - ); -} diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index 0dd9212d..48586895 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -25,6 +25,20 @@ void main() { }, ); + group('loads', () { + flameTester.test( + 'a FlutterSignPost', + (game) async { + await game.ready(); + + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + }); + flameTester.test( 'onNewState adds a new ball', (game) async { @@ -45,7 +59,10 @@ void main() { group('listenWhen', () { final gameBloc = MockGameBloc(); - final tester = flameBlocTester(gameBloc: () => gameBloc); + final tester = flameBlocTester( + game: TestGame.new, + gameBloc: () => gameBloc, + ); setUp(() { whenListen( @@ -55,12 +72,10 @@ void main() { ); }); - tester.widgetTest( + tester.testGameWidget( 'listens when a Bonus.dashNest is added', - (game, tester) async { - await game.ready(); - final flutterForest = - game.descendants().whereType().first; + verify: (game, tester) async { + final flutterForest = FlutterForest(); const state = GameState( score: 0, @@ -69,7 +84,6 @@ void main() { activatedDashNests: {}, bonusHistory: [GameBonus.dashNest], ); - expect( flutterForest.listenWhen(const GameState.initial(), state), isTrue, @@ -81,7 +95,11 @@ void main() { group('DashNestBumperBallContactCallback', () { final gameBloc = MockGameBloc(); - final tester = flameBlocTester(gameBloc: () => gameBloc); + final tester = flameBlocTester( + // TODO(alestiago): Use TestGame.new once a controller is implemented. + game: PinballGameTest.create, + gameBloc: () => gameBloc, + ); setUp(() { whenListen( @@ -91,19 +109,21 @@ void main() { ); }); - tester.widgetTest( + final dashNestBumper = MockDashNestBumper(); + tester.testGameWidget( 'adds a DashNestActivated event with DashNestBumper.id', - (game, tester) async { - final contactCallback = DashNestBumperBallContactCallback(); + setUp: (game, tester) async { const id = '0'; - final dashNestBumper = MockDashNestBumper(); when(() => dashNestBumper.id).thenReturn(id); when(() => dashNestBumper.gameRef).thenReturn(game); - + }, + verify: (game, tester) async { + final contactCallback = DashNestBumperBallContactCallback(); contactCallback.begin(dashNestBumper, MockBall(), MockContact()); - verify(() => gameBloc.add(DashNestActivated(dashNestBumper.id))) - .called(1); + verify( + () => gameBloc.add(DashNestActivated(dashNestBumper.id)), + ).called(1); }, ); }); diff --git a/test/game/components/kicker_test.dart b/test/game/components/kicker_test.dart index 211ff8ad..333c7fbe 100644 --- a/test/game/components/kicker_test.dart +++ b/test/game/components/kicker_test.dart @@ -4,6 +4,7 @@ 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'; void main() { group('Kicker', () { diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 1cec7e0c..2a49ae2d 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -2,7 +2,6 @@ import 'dart:collection'; -import 'package:bloc_test/bloc_test.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/services.dart'; @@ -189,22 +188,14 @@ void main() { group('PlungerAnchorPrismaticJointDef', () { const compressionDistance = 10.0; - final gameBloc = MockGameBloc(); late Plunger plunger; setUp(() { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); plunger = Plunger( compressionDistance: compressionDistance, ); }); - final flameTester = flameBlocTester(gameBloc: () => gameBloc); - group('initializes with', () { flameTester.test( 'plunger body as bodyA', @@ -233,7 +224,7 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); expect(jointDef.bodyB, equals(anchor.body)); }, @@ -250,7 +241,7 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); expect(jointDef.enableLimit, isTrue); }, @@ -267,7 +258,7 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); expect(jointDef.lowerTranslation, equals(double.negativeInfinity)); }, @@ -284,7 +275,7 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); expect(jointDef.collideConnected, isTrue); }, @@ -292,11 +283,11 @@ void main() { }); testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { - flameTester.widgetTest( + late final anchor = PlungerAnchor(plunger: plunger); + flameTester.testGameWidget( 'plunger cannot go below anchor', - (game, tester) async { + setUp: (game, tester) async { await game.ensureAdd(plunger); - final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); // Giving anchor a shape for the plunger to collide with. @@ -306,19 +297,20 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); await tester.pump(const Duration(seconds: 1)); - + }, + verify: (game, tester) async { expect(plunger.body.position.y > anchor.body.position.y, isTrue); }, ); }); testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { - flameTester.widgetTest( + flameTester.testGameWidget( 'plunger cannot excessively exceed starting position', - (game, tester) async { + setUp: (game, tester) async { await game.ensureAdd(plunger); final anchor = PlungerAnchor(plunger: plunger); await game.ensureAdd(anchor); @@ -327,12 +319,13 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); plunger.body.setTransform(Vector2(0, -1), 0); await tester.pump(const Duration(seconds: 1)); - + }, + verify: (game, tester) async { expect(plunger.body.position.y < 1, isTrue); }, ); diff --git a/test/game/components/score_points_test.dart b/test/game/components/score_points_test.dart index 30ec70db..f97bdada 100644 --- a/test/game/components/score_points_test.dart +++ b/test/game/components/score_points_test.dart @@ -1,4 +1,3 @@ -import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -7,14 +6,6 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; -class MockGameBloc extends Mock implements GameBloc {} - -class MockPinballGame extends Mock implements PinballGame {} - -class FakeContact extends Fake implements Contact {} - -class FakeGameEvent extends Fake implements GameEvent {} - class FakeScorePoints extends BodyComponent with ScorePoints { @override Body createBody() { @@ -30,16 +21,12 @@ void main() { late PinballGame game; late GameBloc bloc; late Ball ball; - late ComponentSet componentSet; - late BallController ballController; late FakeScorePoints fakeScorePoints; setUp(() { game = MockPinballGame(); bloc = MockGameBloc(); ball = MockBall(); - componentSet = MockComponentSet(); - ballController = MockBallController(); fakeScorePoints = FakeScorePoints(); }); @@ -51,13 +38,9 @@ void main() { test( 'emits Scored event with points', () { - when(() => componentSet.whereType()) - .thenReturn([ballController]); - when(() => ball.children).thenReturn(componentSet); - when(() => ballController.gameRef).thenReturn(game); when(game.read).thenReturn(bloc); - BallScorePointsCallback().begin( + BallScorePointsCallback(game).begin( ball, fakeScorePoints, FakeContact(), @@ -71,19 +54,5 @@ void main() { }, ); }); - - group('end', () { - test("doesn't add events to GameBloc", () { - BallScorePointsCallback().end( - ball, - fakeScorePoints, - FakeContact(), - ); - - verifyNever( - () => bloc.add(any()), - ); - }); - }); }); } diff --git a/test/game/components/spaceship_exit_rail_test.dart b/test/game/components/spaceship_exit_rail_test.dart new file mode 100644 index 00000000..99afc808 --- /dev/null +++ b/test/game/components/spaceship_exit_rail_test.dart @@ -0,0 +1,60 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('SpaceshipExitRail', () { + late PinballGame game; + late SpaceshipExitRailEnd exitRailEnd; + late Ball ball; + late Body body; + late Fixture fixture; + late Filter filterData; + + setUp(() { + game = MockPinballGame(); + + exitRailEnd = MockSpaceshipExitRailEnd(); + + ball = MockBall(); + body = MockBody(); + when(() => ball.gameRef).thenReturn(game); + when(() => ball.body).thenReturn(body); + + fixture = MockFixture(); + filterData = MockFilter(); + when(() => body.fixtures).thenReturn([fixture]); + when(() => fixture.filterData).thenReturn(filterData); + }); + + group('SpaceshipExitHoleBallContactCallback', () { + test('changes the ball priority on contact', () { + when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board); + + SpaceshipExitRailEndBallContactCallback().begin( + exitRailEnd, + ball, + MockContact(), + ); + + verify(() => ball.priority = 1).called(1); + }); + + test('reorders the game children', () { + when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board); + + SpaceshipExitRailEndBallContactCallback().begin( + exitRailEnd, + ball, + MockContact(), + ); + + verify(game.reorderChildren).called(1); + }); + }); + }); +} diff --git a/test/game/view/widgets/game_over_dialog_test.dart b/test/game/view/widgets/game_over_dialog_test.dart index 8150bcd5..814a7a45 100644 --- a/test/game/view/widgets/game_over_dialog_test.dart +++ b/test/game/view/widgets/game_over_dialog_test.dart @@ -1,44 +1,195 @@ // ignore_for_file: prefer_const_constructors +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball_theme/pinball_theme.dart'; import '../../../helpers/helpers.dart'; void main() { group('GameOverDialog', () { - testWidgets('renders correctly', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); + testWidgets('renders GameOverDialogView', (tester) async { await tester.pumpApp( - const GameOverDialog( + GameOverDialog( + score: 1000, theme: DashTheme(), ), ); - expect(find.text(l10n.gameOver), findsOneWidget); - expect(find.text(l10n.leaderboard), findsOneWidget); + expect(find.byType(GameOverDialogView), findsOneWidget); }); - testWidgets('tapping on leaderboard button navigates to LeaderBoardPage', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); + group('GameOverDialogView', () { + late LeaderboardBloc leaderboardBloc; - await tester.pumpApp( - const GameOverDialog( - theme: DashTheme(), + final leaderboard = [ + LeaderboardEntry( + rank: '1', + playerInitials: 'ABC', + score: 5000, + character: DashTheme().characterAsset, ), - navigator: navigator, + ]; + final entryData = LeaderboardEntryData( + playerInitials: 'VGV', + score: 10000, + character: CharacterType.dash, ); - await tester.tap(find.widgetWithText(TextButton, l10n.leaderboard)); + setUp(() { + leaderboardBloc = MockLeaderboardBloc(); + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: const LeaderboardState.initial(), + ); + }); + + testWidgets('renders input text view when bloc emits [loading]', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + ); + + expect(find.widgetWithText(TextButton, l10n.addUser), findsOneWidget); + }); + + testWidgets('renders error view when bloc emits [error]', (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial() + .copyWith(status: LeaderboardStatus.error), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + ); + + expect(find.text(l10n.error), findsOneWidget); + }); + + testWidgets('renders success view when bloc emits [success]', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState( + status: LeaderboardStatus.success, + ranking: LeaderboardRanking(ranking: 1, outOf: 2), + leaderboard: leaderboard, + ), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + ); + + expect( + find.widgetWithText(TextButton, l10n.leaderboard), + findsOneWidget, + ); + }); + + testWidgets('adds LeaderboardEntryAdded when tap on add user button', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial(), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + ); + + await tester.enterText( + find.byKey(const Key('player_initials_text_field')), + entryData.playerInitials, + ); + + final button = find.widgetWithText(TextButton, l10n.addUser); + await tester.ensureVisible(button); + await tester.tap(button); + + verify( + () => leaderboardBloc.add(LeaderboardEntryAdded(entry: entryData)), + ).called(1); + }); + + testWidgets('navigates to LeaderboardPage when tap on leaderboard button', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + final navigator = MockNavigator(); + when(() => navigator.push(any())).thenAnswer((_) async {}); + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState( + status: LeaderboardStatus.success, + ranking: LeaderboardRanking(ranking: 1, outOf: 2), + leaderboard: leaderboard, + ), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + navigator: navigator, + ); + + final button = find.widgetWithText(TextButton, l10n.leaderboard); + await tester.ensureVisible(button); + await tester.tap(button); - verify(() => navigator.push(any())).called(1); + verify(() => navigator.push(any())).called(1); + }); }); }); } diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart index d8ffd715..970dd12b 100644 --- a/test/helpers/builders.dart +++ b/test/helpers/builders.dart @@ -1,14 +1,14 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; -import 'helpers.dart'; - -FlameTester flameBlocTester({ +FlameTester flameBlocTester({ + required T Function() game, required GameBloc Function() gameBloc, }) { - return FlameTester( - PinballGameTest.create, + return FlameTester( + game, pumpWidget: (gameWidget, tester) async { await tester.pumpWidget( BlocProvider.value( diff --git a/test/helpers/fakes.dart b/test/helpers/fakes.dart new file mode 100644 index 00000000..706733a1 --- /dev/null +++ b/test/helpers/fakes.dart @@ -0,0 +1,7 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; + +class FakeContact extends Fake implements Contact {} + +class FakeGameEvent extends Fake implements GameEvent {} diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 223ec627..d9dc2a17 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -6,7 +6,9 @@ // license that can be found in the LICENSE file or at export 'builders.dart'; export 'extensions.dart'; +export 'fakes.dart'; export 'key_testers.dart'; export 'mocks.dart'; export 'navigator.dart'; export 'pump_app.dart'; +export 'test_game.dart'; diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index c658c531..9453c93a 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -1,4 +1,3 @@ -import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; @@ -39,8 +38,7 @@ class MockGameState extends Mock implements GameState {} class MockThemeCubit extends Mock implements ThemeCubit {} -class MockLeaderboardBloc extends MockBloc - implements LeaderboardBloc {} +class MockLeaderboardBloc extends Mock implements LeaderboardBloc {} class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} @@ -72,6 +70,8 @@ class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {} class MockSpaceshipHole extends Mock implements SpaceshipHole {} +class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {} + class MockComponentSet extends Mock implements ComponentSet {} class MockDashNestBumper extends Mock implements DashNestBumper {} diff --git a/test/helpers/test_game.dart b/test/helpers/test_game.dart new file mode 100644 index 00000000..3c6ff42f --- /dev/null +++ b/test/helpers/test_game.dart @@ -0,0 +1,8 @@ +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class TestGame extends Forge2DGame with FlameBloc { + TestGame() { + images.prefix = ''; + } +} diff --git a/test/leaderboard/view/leaderboard_page_test.dart b/test/leaderboard/view/leaderboard_page_test.dart index 9460818d..4221d727 100644 --- a/test/leaderboard/view/leaderboard_page_test.dart +++ b/test/leaderboard/view/leaderboard_page_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: prefer_const_constructors +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -42,7 +43,11 @@ void main() { testWidgets('renders correctly', (tester) async { final l10n = await AppLocalizations.delegate.load(Locale('en')); - when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial(), + ); await tester.pumpApp( BlocProvider.value( @@ -59,7 +64,11 @@ void main() { testWidgets('renders loading view when bloc emits [loading]', (tester) async { - when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial(), + ); await tester.pumpApp( BlocProvider.value( @@ -76,8 +85,12 @@ void main() { }); testWidgets('renders error view when bloc emits [error]', (tester) async { - when(() => leaderboardBloc.state).thenReturn( - LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial().copyWith( + status: LeaderboardStatus.error, + ), ); await tester.pumpApp( @@ -97,8 +110,10 @@ void main() { testWidgets('renders success view when bloc emits [success]', (tester) async { final l10n = await AppLocalizations.delegate.load(Locale('en')); - when(() => leaderboardBloc.state).thenReturn( - LeaderboardState( + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState( status: LeaderboardStatus.success, ranking: LeaderboardRanking(ranking: 0, outOf: 0), leaderboard: [