diff --git a/.gitignore b/.gitignore index e47b373d..a7531405 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,7 @@ app.*.map.json .firebase test/.test_runner.dart -web/__/firebase/init.js \ No newline at end of file +web/__/firebase/init.js + +# Application exceptions +!/packages/pinball_components/assets/images/flutter_sign_post.png diff --git a/assets/images/components/background.png b/assets/images/components/background.png new file mode 100644 index 00000000..8b8fdf77 Binary files /dev/null and b/assets/images/components/background.png differ 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/game/components/ball.dart b/lib/game/components/ball.dart index 756a8a97..9f6241fd 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -1,33 +1,63 @@ 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_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 +/// [Blueprint] which cretes a ball game object. /// {@endtemplate} class BallBlueprint extends Blueprint { /// {@macro ball_blueprint} - BallBlueprint({required this.position}); + BallBlueprint({ + required this.position, + required this.type, + }); - /// The initial position of the [Ball] + /// 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()); + final ball = Ball(baseColor: baseColor) + ..add( + BallController(type: type), + ); add(ball..initialPosition = position + Vector2(0, ball.size.y / 2)); } } -/// {@template ball} -/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the -/// [PinballGame]. +/// {@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. /// @@ -35,9 +65,11 @@ class BallController extends Component with HasGameRef { /// 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(); 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 34ef4d33..fb6dcda3 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -1,12 +1,14 @@ 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 { @@ -21,8 +23,16 @@ class Board extends Component { final flutterForest = FlutterForest(); + // TODO(alestiago): adjust positioning to real design. + final dino = ChromeDino() + ..initialPosition = Vector2( + PinballGame.boardBounds.center.dx + 25, + PinballGame.boardBounds.center.dy + 10, + ); + await addAll([ bottomGroup, + dino, flutterForest, ]); } @@ -87,12 +97,15 @@ class _BottomGroupSide extends Component { final flipper = Flipper( side: _side, )..initialPosition = _position; + await flipper.add(FlipperController(flipper)); + 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 new file mode 100644 index 00000000..dc280350 --- /dev/null +++ b/lib/game/components/chrome_dino.dart @@ -0,0 +1,168 @@ +import 'dart:async'; +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_components/pinball_components.dart'; + +/// {@template chrome_dino} +/// Dinosaur that gobbles up a [Ball], swivel his head around, and shoots it +/// back out. +/// {@endtemplate} +class ChromeDino extends BodyComponent with InitialPosition { + /// {@macro chrome_dino} + ChromeDino() { + // TODO(alestiago): Remove once sprites are defined. + paint = Paint()..color = Colors.blue; + } + + /// The size of the dinosaur mouth. + static final size = Vector2(5, 2.5); + + /// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc + /// motion. + Future<_ChromeDinoJoint> _anchorToJoint() async { + final anchor = _ChromeDinoAnchor(chromeDino: this); + await add(anchor); + + final jointDef = _ChromeDinoAnchorRevoluteJointDef( + chromeDino: this, + anchor: anchor, + ); + final joint = _ChromeDinoJoint(jointDef); + world.createJoint2(joint); + + return joint; + } + + @override + Future onLoad() async { + await super.onLoad(); + final joint = await _anchorToJoint(); + await add( + TimerComponent( + period: 1, + onTick: joint.swivel, + repeat: true, + ), + ); + } + + List _createFixtureDefs() { + final fixtureDefs = []; + + // TODO(alestiago): Subject to change when sprites are added. + final box = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2); + final fixtureDef = FixtureDef(box) + ..shape = box + ..density = 999 + ..friction = 0.3 + ..restitution = 0.1 + ..isSensor = true; + fixtureDefs.add(fixtureDef); + + // FIXME(alestiago): Investigate why adding these fixtures is considered as + // an invalid contact type. + // final upperEdge = EdgeShape() + // ..set( + // Vector2(-size.x / 2, -size.y / 2), + // Vector2(size.x / 2, -size.y / 2), + // ); + // final upperEdgeDef = FixtureDef(upperEdge)..density = 0.5; + // fixtureDefs.add(upperEdgeDef); + + // final lowerEdge = EdgeShape() + // ..set( + // Vector2(-size.x / 2, size.y / 2), + // Vector2(size.x / 2, size.y / 2), + // ); + // final lowerEdgeDef = FixtureDef(lowerEdge)..density = 0.5; + // fixtureDefs.add(lowerEdgeDef); + + // final rightEdge = EdgeShape() + // ..set( + // Vector2(size.x / 2, -size.y / 2), + // Vector2(size.x / 2, size.y / 2), + // ); + // final rightEdgeDef = FixtureDef(rightEdge)..density = 0.5; + // fixtureDefs.add(rightEdgeDef); + + return fixtureDefs; + } + + @override + Body createBody() { + final bodyDef = BodyDef() + ..gravityScale = 0 + ..position = initialPosition + ..type = BodyType.dynamic; + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} + +/// {@template flipper_anchor} +/// [JointAnchor] positioned at the end of a [ChromeDino]. +/// {@endtemplate} +class _ChromeDinoAnchor extends JointAnchor { + /// {@macro flipper_anchor} + _ChromeDinoAnchor({ + required ChromeDino chromeDino, + }) { + initialPosition = Vector2( + chromeDino.body.position.x + ChromeDino.size.x / 2, + chromeDino.body.position.y, + ); + } +} + +/// {@template chrome_dino_anchor_revolute_joint_def} +/// Hinges a [ChromeDino] to a [_ChromeDinoAnchor]. +/// {@endtemplate} +class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef { + /// {@macro chrome_dino_anchor_revolute_joint_def} + _ChromeDinoAnchorRevoluteJointDef({ + required ChromeDino chromeDino, + required _ChromeDinoAnchor anchor, + }) { + initialize( + chromeDino.body, + anchor.body, + anchor.body.position, + ); + enableLimit = true; + // TODO(alestiago): Apply design angle value. + const angle = math.pi / 3.5; + lowerAngle = -angle / 2; + upperAngle = angle / 2; + + enableMotor = true; + // TODO(alestiago): Tune this values. + maxMotorTorque = motorSpeed = chromeDino.body.mass * 30; + } +} + +class _ChromeDinoJoint extends RevoluteJoint { + _ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def); + + /// Sweeps the [ChromeDino] up and down repeatedly. + void swivel() { + 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 d2c7c72d..3c1a4302 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,16 +1,14 @@ export 'ball.dart'; export 'baseboard.dart'; export 'board.dart'; -export 'board_side.dart'; export 'bonus_word.dart'; -export 'flipper.dart'; +export 'chrome_dino.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/flipper_controller.dart b/lib/game/components/flipper_controller.dart new file mode 100644 index 00000000..946cfd49 --- /dev/null +++ b/lib/game/components/flipper_controller.dart @@ -0,0 +1,52 @@ +import 'package:flame/components.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template flipper_controller} +/// A [Component] that controls the [Flipper]s movement. +/// {@endtemplate} +class FlipperController extends Component with KeyboardHandler { + /// {@macro flipper_controller} + FlipperController(this.flipper) : _keys = flipper.side.flipperKeys; + + /// The [Flipper] this controller is controlling. + final Flipper 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) { + flipper.moveUp(); + } else if (event is RawKeyUpEvent) { + flipper.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 31ea8c2b..c5ed20bf 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'; @@ -35,6 +34,7 @@ class FlutterForest extends Component gameRef.addFromBlueprint( BallBlueprint( position: Vector2(17.2, 52.7), + type: BallType.extra, ), ); } @@ -43,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); @@ -52,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..b58ddfa6 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'; @@ -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 5313a2d9..b3f3cb23 100644 --- a/lib/game/components/launcher_ramp.dart +++ b/lib/game/components/launcher_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'; diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index d9137457..5703e525 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -85,7 +85,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition { plunger: this, anchor: anchor, ); - world.createJoint(jointDef); + world.createJoint(PrismaticJoint(jointDef)); } @override 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/game_assets.dart b/lib/game/game_assets.dart index fdc1f332..73aae25f 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -8,11 +8,10 @@ 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(Assets.images.components.spaceship.androidTop.path), - images.load(Assets.images.components.spaceship.androidBottom.path), - images.load(Assets.images.components.spaceship.lower.path), - images.load(Assets.images.components.spaceship.upper.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), ]); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 2a8ddd42..9673b2d2 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -2,13 +2,15 @@ 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_theme/pinball_theme.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 with FlameBloc, HasKeyboardHandlerComponents { @@ -44,11 +46,24 @@ 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 - ..followVector2(Vector2.zero()) + ..followVector2(Vector2(0, -7.8)) ..zoom = size.y / 16; } @@ -86,15 +101,48 @@ class PinballGame extends Forge2DGame } void spawnBall() { - addFromBlueprint(BallBlueprint(position: plunger.body.position)); + addFromBlueprint( + BallBlueprint( + position: plunger.body.position, + type: BallType.normal, + ), + ); } } class DebugPinballGame extends PinballGame with TapDetector { DebugPinballGame({required PinballTheme theme}) : super(theme: theme); + @override + Future onLoad() async { + await super.onLoad(); + await _loadBackground(); + } + + // TODO(alestiago): Move to PinballGame once we have the real background + // component. + Future _loadBackground() async { + final sprite = await loadSprite( + Assets.images.components.background.path, + ); + final spriteComponent = SpriteComponent( + sprite: sprite, + size: Vector2(120, 160), + anchor: Anchor.center, + ) + ..position = Vector2(0, -7.8) + ..priority = -1; + + await add(spriteComponent); + } + @override void onTapUp(TapUpInfo info) { - addFromBlueprint(BallBlueprint(position: info.eventPosition.game)); + addFromBlueprint( + BallBlueprint( + position: info.eventPosition.game, + type: BallType.extra, + ), + ); } } 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 6e81fe77..370d8fcf 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -15,25 +15,8 @@ class $AssetsImagesGen { class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); - AssetGenImage get flipper => - const AssetGenImage('assets/images/components/flipper.png'); - $AssetsImagesComponentsSpaceshipGen get spaceship => - const $AssetsImagesComponentsSpaceshipGen(); -} - -class $AssetsImagesComponentsSpaceshipGen { - const $AssetsImagesComponentsSpaceshipGen(); - - AssetGenImage get androidBottom => const AssetGenImage( - 'assets/images/components/spaceship/android-bottom.png'); - AssetGenImage get androidTop => - const AssetGenImage('assets/images/components/spaceship/android-top.png'); - AssetGenImage get lower => - const AssetGenImage('assets/images/components/spaceship/lower.png'); - AssetGenImage get saucer => - const AssetGenImage('assets/images/components/spaceship/saucer.png'); - AssetGenImage get upper => - const AssetGenImage('assets/images/components/spaceship/upper.png'); + AssetGenImage get background => + const AssetGenImage('assets/images/components/background.png'); } class Assets { 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/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..9a2da898 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 { @@ -90,7 +90,7 @@ class Ball extends BodyComponent } } - /// Applies a boost on this [Ball] + /// Applies a boost on this [Ball]. void boost(Vector2 impulse) { body.applyLinearImpulse(impulse); _boostTimer = _boostDuration; 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..c29f91a3 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,5 +1,11 @@ export 'ball.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..de5f18c8 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.createJoint2(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,60 @@ 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); + } +} + +// TODO(alestiago): Remove once Forge2D supports custom joints. +extension on World { + void createJoint2(Joint joint) { + assert(!isLocked, ''); + + joints.add(joint); + + joint.bodyA.joints.add(joint); + joint.bodyB.joints.add(joint); } } diff --git a/packages/pinball_components/lib/src/components/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 53% rename from lib/game/components/spaceship.dart rename to packages/pinball_components/lib/src/components/spaceship.dart index 1aee6d14..03166a1b 100644 --- a/lib/game/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -5,48 +5,42 @@ import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/flame/blueprint.dart'; -import 'package:pinball/flame/priority.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; /// Base priority for wall while be on spaceship. - static const ballPriorityWhenOnSpaceship = 3; + static const ballPriorityWhenOnSpaceship = 4; @override void build(_) { - final position = Vector2( - PinballGame.boardBounds.left + radius + 15, - PinballGame.boardBounds.center.dy + 30, - ); - addAllContactCallback([ SpaceshipHoleBallContactCallback(), SpaceshipEntranceBallContactCallback(), ]); - final rendersBehindBall = [ - SpaceshipEntrance()..initialPosition = position, + addAll([ SpaceshipSaucer()..initialPosition = position, - ]; - - final rendersInFrontOfBall = [ - SpaceshipBridge()..initialPosition = position, - SpaceshipBridgeTop()..initialPosition = position + Vector2(0, 5.5), + SpaceshipEntrance()..initialPosition = position, + 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, - ]; - - addAll([ - ...rendersBehindBall, - ...rendersInFrontOfBall, - SpaceshipHole()..initialPosition = position - Vector2(5, 4), - SpaceshipHole()..initialPosition = position - Vector2(-5, 4), ]); } } @@ -64,25 +58,15 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { @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)), ), ); @@ -91,7 +75,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 @@ -105,50 +89,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: Spaceship.ballPriorityWhenOnSpaceship + 1); - - @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: Spaceship.ballPriorityWhenOnSpaceship + 1) { + AndroidHead() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) { layer = Layer.spaceship; } @@ -159,17 +106,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, ), ); @@ -177,7 +127,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 @@ -203,13 +153,14 @@ class SpaceshipEntrance extends RampOpening { pathwayLayer: Layer.spaceship, orientation: RampOrientation.up, ) { - priority = Spaceship.ballPriorityWhenOnSpaceship - 1; + priority = Spaceship.ballPriorityWhenOnSpaceship; layer = Layer.spaceship; } @override Shape get shape { - const radius = Spaceship.radius * 2; + renderBody = false; + final radius = Spaceship.size.y / 2; return PolygonShape() ..setAsEdge( Vector2( @@ -226,28 +177,52 @@ 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. + 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), + ), + ], + ); } } @@ -263,44 +238,16 @@ class SpaceshipWall extends BodyComponent with InitialPosition, Layered { 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) @@ -320,22 +267,24 @@ class SpaceshipEntranceBallContactCallback @override void begin(SpaceshipEntrance entrance, Ball ball, _) { ball - ..layer = Layer.spaceship - ..showInFrontOf(entrance); + ..showInFrontOf(entrance) + ..layer = Layer.spaceship; } } /// [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 - ..layer = Layer.board - ..sendToBack(); + // TODO(ruimiguel): apply Elevated when PR merged. + ..priority = hole.onExitElevation + ..gameRef.reorderChildren() + ..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..9af8dba6 --- /dev/null +++ b/packages/pinball_components/lib/src/flame/flame.dart @@ -0,0 +1,2 @@ +export 'blueprint.dart'; +export 'priority.dart'; diff --git a/lib/flame/priority.dart b/packages/pinball_components/lib/src/flame/priority.dart similarity index 100% rename from lib/flame/priority.dart rename to packages/pinball_components/lib/src/flame/priority.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/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/packages/pinball_components/test/src/components/spaceship_test.dart b/packages/pinball_components/test/src/components/spaceship_test.dart new file mode 100644 index 00000000..87987ca3 --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_test.dart @@ -0,0 +1,102 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('Spaceship', () { + late Filter filterData; + late Fixture fixture; + late Body body; + late Ball ball; + late SpaceshipEntrance entrance; + late SpaceshipHole hole; + late Forge2DGame game; + + setUp(() { + filterData = MockFilter(); + + fixture = MockFixture(); + when(() => fixture.filterData).thenReturn(filterData); + + body = MockBody(); + when(() => body.fixtures).thenReturn([fixture]); + + game = MockGame(); + + ball = MockBall(); + when(() => ball.gameRef).thenReturn(game); + when(() => ball.body).thenReturn(body); + + entrance = MockSpaceshipEntrance(); + 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 { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/spaceship.png'), + ); + }, + ); + }); + + group('SpaceshipEntranceBallContactCallback', () { + test('changes the ball priority on contact', () { + when(() => ball.priority).thenReturn(2); + when(() => entrance.priority).thenReturn(3); + + SpaceshipEntranceBallContactCallback().begin( + entrance, + ball, + MockContact(), + ); + + verify(() => ball.showInFrontOf(entrance)).called(1); + }); + }); + + 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 = 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, + MockContact(), + ); + + verify(() => ball.sendToBack()).called(1); + }); + }); + }); +} 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 1efe9281..a0cca553 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 @@ -43,7 +43,6 @@ flutter: assets: - assets/images/components/ - - assets/images/components/spaceship/ flutter_gen: line_length: 80 diff --git a/test/flame/priority_test.dart b/test/flame/priority_test.dart index 24862951..c4f61569 100644 --- a/test/flame/priority_test.dart +++ b/test/flame/priority_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:mockingjay/mockingjay.dart'; -import 'package:pinball/flame/priority.dart'; +import 'package:pinball_components/src/flame/priority.dart'; import '../helpers/helpers.dart'; diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index a872dc1f..f12b3569 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -27,11 +27,12 @@ void main() { final tester = flameBlocTester(gameBloc: () => gameBloc); - tester.widgetTest( + tester.testGameWidget( 'adds BallLost to GameBloc', - (game, tester) async { + setUp: (game, tester) async { await game.ready(); - + }, + verify: (game, tester) async { game.children.whereType().first.controller.lost(); await tester.pump(); @@ -39,14 +40,15 @@ void main() { }, ); - tester.widgetTest( + tester.testGameWidget( 'resets the ball if the game is not over', - (game, tester) async { + setUp: (game, tester) async { await game.ready(); game.children.whereType().first.controller.lost(); await game.ready(); // Making sure that all additions are done - + }, + verify: (game, tester) async { expect( game.children.whereType().length, equals(1), @@ -54,9 +56,9 @@ void main() { }, ); - tester.widgetTest( + tester.testGameWidget( 'no ball is added on game over', - (game, tester) async { + setUp: (game, tester) async { whenListen( gameBloc, const Stream.empty(), @@ -72,7 +74,8 @@ void main() { game.children.whereType().first.controller.lost(); await tester.pump(); - + }, + verify: (game, tester) async { 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 5a4b95dc..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(); @@ -86,6 +87,18 @@ void main() { expect(flutterForest.length, equals(1)); }, ); + + flameTester.test( + 'one ChromeDino', + (game) async { + final board = Board(); + await game.ready(); + await game.ensureAdd(board); + + final chromeDino = board.descendants().whereType(); + expect(chromeDino.length, equals(1)); + }, + ); }); }); } diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index 293062ae..724cefe9 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,11 @@ void main() { }); group('bonus letter activation', () { - final gameBloc = MockGameBloc(); + late GameBloc gameBloc; final tester = flameBlocTester(gameBloc: () => gameBloc); setUp(() { + gameBloc = MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -204,22 +206,24 @@ 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, @@ -237,14 +241,15 @@ void main() { 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, @@ -259,7 +264,9 @@ void main() { bonusLetter.onNewState(state); await tester.pump(); - + }, + verify: (game, tester) async { + final bonusLetter = game.descendants().whereType().first; expect( bonusLetter.children.whereType().length, equals(1), @@ -267,9 +274,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 +289,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/chrome_dino_test.dart b/test/game/components/chrome_dino_test.dart new file mode 100644 index 00000000..af53ffea --- /dev/null +++ b/test/game/components/chrome_dino_test.dart @@ -0,0 +1,23 @@ +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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(Forge2DGame.new); + + group('ChromeDino', () { + flameTester.test( + 'loads correctly', + (game) async { + final chromeDino = ChromeDino(); + + await game.ready(); + await game.ensureAdd(chromeDino); + + expect(game.contains(chromeDino), isTrue); + }, + ); + }); +} 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..3f4db6ff 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 { @@ -55,10 +69,12 @@ void main() { ); }); - tester.widgetTest( + tester.testGameWidget( 'listens when a Bonus.dashNest is added', - (game, tester) async { + setUp: (game, tester) async { await game.ready(); + }, + verify: (game, tester) async { final flutterForest = game.descendants().whereType().first; @@ -69,7 +85,6 @@ void main() { activatedDashNests: {}, bonusHistory: [GameBonus.dashNest], ); - expect( flutterForest.listenWhen(const GameState.initial(), state), isTrue, @@ -91,15 +106,16 @@ 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))) 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..c6787be6 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -233,7 +233,7 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); expect(jointDef.bodyB, equals(anchor.body)); }, @@ -250,7 +250,7 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); expect(jointDef.enableLimit, isTrue); }, @@ -267,7 +267,7 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); expect(jointDef.lowerTranslation, equals(double.negativeInfinity)); }, @@ -284,7 +284,7 @@ void main() { plunger: plunger, anchor: anchor, ); - game.world.createJoint(jointDef); + game.world.createJoint(PrismaticJoint(jointDef)); expect(jointDef.collideConnected, isTrue); }, @@ -292,11 +292,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 +306,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 +328,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/spaceship_test.dart b/test/game/components/spaceship_exit_rail_test.dart similarity index 55% rename from test/game/components/spaceship_test.dart rename to test/game/components/spaceship_exit_rail_test.dart index 785e63b9..99afc808 100644 --- a/test/game/components/spaceship_test.dart +++ b/test/game/components/spaceship_exit_rail_test.dart @@ -1,67 +1,59 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:pinball/flame/priority.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; void main() { - group('Spaceship', () { - late Filter filterData; - late Fixture fixture; - late Body body; + group('SpaceshipExitRail', () { late PinballGame game; + late SpaceshipExitRailEnd exitRailEnd; late Ball ball; - late SpaceshipEntrance entrance; - late SpaceshipHole hole; + late Body body; + late Fixture fixture; + late Filter filterData; setUp(() { - filterData = MockFilter(); - - fixture = MockFixture(); - when(() => fixture.filterData).thenReturn(filterData); - - body = MockBody(); - when(() => body.fixtures).thenReturn([fixture]); - game = MockPinballGame(); + exitRailEnd = MockSpaceshipExitRailEnd(); + ball = MockBall(); + body = MockBody(); when(() => ball.gameRef).thenReturn(game); when(() => ball.body).thenReturn(body); - entrance = MockSpaceshipEntrance(); - hole = MockSpaceshipHole(); + fixture = MockFixture(); + filterData = MockFilter(); + when(() => body.fixtures).thenReturn([fixture]); + when(() => fixture.filterData).thenReturn(filterData); }); - group('SpaceshipEntranceBallContactCallback', () { + group('SpaceshipExitHoleBallContactCallback', () { test('changes the ball priority on contact', () { - when(() => ball.priority).thenReturn(1); - when(() => entrance.priority).thenReturn(2); + when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board); - SpaceshipEntranceBallContactCallback().begin( - entrance, + SpaceshipExitRailEndBallContactCallback().begin( + exitRailEnd, ball, MockContact(), ); - verify(() => ball.showInFrontOf(entrance)).called(1); + verify(() => ball.priority = 1).called(1); }); - }); - group('SpaceshipHoleBallContactCallback', () { - test('changes the ball priority on contact', () { - when(() => ball.priority).thenReturn(1); + test('reorders the game children', () { + when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board); - SpaceshipHoleBallContactCallback().begin( - hole, + SpaceshipExitRailEndBallContactCallback().begin( + exitRailEnd, ball, MockContact(), ); - verify(() => ball.sendToBack()).called(1); + 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/mocks.dart b/test/helpers/mocks.dart index 7d4f64a7..69983d10 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 {} @@ -68,12 +66,10 @@ class MockFilter extends Mock implements Filter {} class MockFixture extends Mock implements Fixture {} -class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {} +class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {} -class MockSpaceshipHole extends Mock implements SpaceshipHole {} +class MockComponentSet extends Mock implements ComponentSet {} class MockComponent extends Mock implements Component {} -class MockComponentSet extends Mock implements ComponentSet {} - class MockDashNestBumper extends Mock implements DashNestBumper {} 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: [