fix: fixed merge conflicts on spaceship

pull/83/head
RuiAlonso 4 years ago
commit a779bf195d

3
.gitignore vendored

@ -131,3 +131,6 @@ app.*.map.json
test/.test_runner.dart test/.test_runner.dart
web/__/firebase/init.js web/__/firebase/init.js
# Application exceptions
!/packages/pinball_components/assets/images/flutter_sign_post.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

@ -1,33 +1,63 @@
import 'package:flame/components.dart'; 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/game/game.dart';
import 'package:pinball_components/pinball_components.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} /// {@template ball_blueprint}
/// [Blueprint] which cretes a ball game object /// [Blueprint] which cretes a ball game object.
/// {@endtemplate} /// {@endtemplate}
class BallBlueprint extends Blueprint<PinballGame> { class BallBlueprint extends Blueprint<PinballGame> {
/// {@macro ball_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; final Vector2 position;
/// {@macro ball_type}
final BallType type;
@override @override
void build(PinballGame gameRef) { void build(PinballGame gameRef) {
final baseColor = gameRef.theme.characterTheme.ballColor; 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)); add(ball..initialPosition = position + Vector2(0, ball.size.y / 2));
} }
} }
/// {@template ball} /// {@template ball_controller}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the /// Controller attached to a [Ball] that handles its game related logic.
/// [PinballGame].
/// {@endtemplate} /// {@endtemplate}
class BallController extends Component with HasGameRef<PinballGame> { class BallController extends Component with HasGameRef<PinballGame> {
/// {@macro ball_controller}
BallController({required this.type});
/// {@macro ball_type}
final BallType type;
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left. /// any are left.
/// ///
@ -35,9 +65,11 @@ class BallController extends Component with HasGameRef<PinballGame> {
/// a [BottomWall]. /// a [BottomWall].
void lost() { void lost() {
parent?.shouldRemove = true; 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<GameBloc>()..add(const BallLost()); final bloc = gameRef.read<GameBloc>()..add(const BallLost());
final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver; final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver;
if (shouldBallRespwan) { if (shouldBallRespwan) {
gameRef.spawnBall(); gameRef.spawnBall();

@ -1,7 +1,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template baseboard} /// {@template baseboard}

@ -1,12 +1,14 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template board} /// {@template board}
/// The main flat surface of the [PinballGame]. /// The main flat surface of the [PinballGame].
/// {endtemplate} /// {endtemplate}
class Board extends Component { class Board extends Component {
/// {@macro board} /// {@macro board}
Board(); // TODO(alestiago): Make Board a Blueprint and sort out priorities.
Board() : super(priority: 5);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -21,8 +23,16 @@ class Board extends Component {
final flutterForest = FlutterForest(); 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([ await addAll([
bottomGroup, bottomGroup,
dino,
flutterForest, flutterForest,
]); ]);
} }
@ -87,12 +97,15 @@ class _BottomGroupSide extends Component {
final flipper = Flipper( final flipper = Flipper(
side: _side, side: _side,
)..initialPosition = _position; )..initialPosition = _position;
await flipper.add(FlipperController(flipper));
final baseboard = Baseboard(side: _side) final baseboard = Baseboard(side: _side)
..initialPosition = _position + ..initialPosition = _position +
Vector2( Vector2(
(Baseboard.size.x / 1.6 * direction), (Baseboard.size.x / 1.6 * direction),
Baseboard.size.y - 2, Baseboard.size.y - 2,
); );
final kicker = Kicker( final kicker = Kicker(
side: _side, side: _side,
)..initialPosition = _position + )..initialPosition = _position +

@ -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<void> onLoad() async {
await super.onLoad();
final joint = await _anchorToJoint();
await add(
TimerComponent(
period: 1,
onTick: joint.swivel,
repeat: true,
),
);
}
List<FixtureDef> _createFixtureDefs() {
final fixtureDefs = <FixtureDef>[];
// 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);
}
}

@ -1,16 +1,14 @@
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board.dart'; export 'board.dart';
export 'board_side.dart';
export 'bonus_word.dart'; export 'bonus_word.dart';
export 'flipper.dart'; export 'chrome_dino.dart';
export 'flipper_controller.dart';
export 'flutter_forest.dart'; export 'flutter_forest.dart';
export 'jetpack_ramp.dart'; export 'jetpack_ramp.dart';
export 'joint_anchor.dart';
export 'kicker.dart'; export 'kicker.dart';
export 'launcher_ramp.dart'; export 'launcher_ramp.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'ramp_opening.dart';
export 'score_points.dart'; export 'score_points.dart';
export 'spaceship.dart'; export 'spaceship_exit_rail.dart';
export 'wall.dart'; export 'wall.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<LogicalKeyboardKey> _keys;
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> 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<LogicalKeyboardKey> get flipperKeys {
switch (this) {
case BoardSide.left:
return [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
];
case BoardSide.right:
return [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
];
}
}
}

@ -6,7 +6,6 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -35,6 +34,7 @@ class FlutterForest extends Component
gameRef.addFromBlueprint( gameRef.addFromBlueprint(
BallBlueprint( BallBlueprint(
position: Vector2(17.2, 52.7), position: Vector2(17.2, 52.7),
type: BallType.extra,
), ),
); );
} }
@ -43,6 +43,8 @@ class FlutterForest extends Component
Future<void> onLoad() async { Future<void> onLoad() async {
gameRef.addContactCallback(DashNestBumperBallContactCallback()); gameRef.addContactCallback(DashNestBumperBallContactCallback());
final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3);
// TODO(alestiago): adjust positioning once sprites are added. // TODO(alestiago): adjust positioning once sprites are added.
final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest') final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest')
..initialPosition = Vector2(8.95, 51.95); ..initialPosition = Vector2(8.95, 51.95);
@ -52,6 +54,7 @@ class FlutterForest extends Component
..initialPosition = Vector2(18.55, 59.35); ..initialPosition = Vector2(18.55, 59.35);
await addAll([ await addAll([
signPost,
smallLeftNest, smallLeftNest,
smallRightNest, smallRightNest,
bigNest, bigNest,

@ -5,7 +5,6 @@ import 'dart:math' as math;
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -115,7 +114,7 @@ class _JetpackRampOpening extends RampOpening {
final double _rotation; final double _rotation;
static final Vector2 _size = Vector2(JetpackRamp.width / 3, .1); static final Vector2 _size = Vector2(JetpackRamp.width / 4, .1);
@override @override
Shape get shape => PolygonShape() Shape get shape => PolygonShape()

@ -4,7 +4,6 @@ import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geometry/geometry.dart' as geometry show centroid; import 'package:geometry/geometry.dart' as geometry show centroid;
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template kicker} /// {@template kicker}

@ -5,7 +5,6 @@ import 'dart:math' as math;
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';

@ -85,7 +85,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
plunger: this, plunger: this,
anchor: anchor, anchor: anchor,
); );
world.createJoint(jointDef); world.createJoint(PrismaticJoint(jointDef));
} }
@override @override

@ -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<FixtureDef> _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 = <FixtureDef>[];
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<SpaceshipExitRailEnd, Ball> {
@override
void begin(SpaceshipExitRailEnd exitRail, Ball ball, _) {
ball
..priority = 1
..gameRef.reorderChildren()
..layer = exitRail.outsideLayer;
}
}

@ -8,11 +8,10 @@ extension PinballGameAssetsX on PinballGame {
Future<void> preLoadAssets() async { Future<void> preLoadAssets() async {
await Future.wait([ await Future.wait([
images.load(components.Assets.images.ball.keyName), images.load(components.Assets.images.ball.keyName),
images.load(Assets.images.components.flipper.path), images.load(components.Assets.images.flutterSignPost.keyName),
images.load(Assets.images.components.spaceship.androidTop.path), images.load(components.Assets.images.flipper.left.keyName),
images.load(Assets.images.components.spaceship.androidBottom.path), images.load(components.Assets.images.flipper.right.keyName),
images.load(Assets.images.components.spaceship.lower.path), images.load(Assets.images.components.background.path),
images.load(Assets.images.components.spaceship.upper.path),
]); ]);
} }
} }

@ -2,13 +2,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.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 class PinballGame extends Forge2DGame
with FlameBloc, HasKeyboardHandlerComponents { with FlameBloc, HasKeyboardHandlerComponents {
@ -44,11 +46,24 @@ class PinballGame extends Forge2DGame
unawaited(_addPlunger()); unawaited(_addPlunger());
unawaited(_addBonusWord()); unawaited(_addBonusWord());
unawaited(_addPaths()); 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. // Fix camera on the center of the board.
camera camera
..followVector2(Vector2.zero()) ..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16; ..zoom = size.y / 16;
} }
@ -86,15 +101,48 @@ class PinballGame extends Forge2DGame
} }
void spawnBall() { void spawnBall() {
addFromBlueprint(BallBlueprint(position: plunger.body.position)); addFromBlueprint(
BallBlueprint(
position: plunger.body.position,
type: BallType.normal,
),
);
} }
} }
class DebugPinballGame extends PinballGame with TapDetector { class DebugPinballGame extends PinballGame with TapDetector {
DebugPinballGame({required PinballTheme theme}) : super(theme: theme); DebugPinballGame({required PinballTheme theme}) : super(theme: theme);
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadBackground();
}
// TODO(alestiago): Move to PinballGame once we have the real background
// component.
Future<void> _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 @override
void onTapUp(TapUpInfo info) { void onTapUp(TapUpInfo info) {
addFromBlueprint(BallBlueprint(position: info.eventPosition.game)); addFromBlueprint(
BallBlueprint(
position: info.eventPosition.game,
type: BallType.extra,
),
);
} }
} }

@ -70,7 +70,10 @@ class _PinballGameViewState extends State<PinballGameView> {
showDialog<void>( showDialog<void>(
context: context, context: context,
builder: (_) { builder: (_) {
return GameOverDialog(theme: widget.theme.characterTheme); return GameOverDialog(
score: state.score,
theme: widget.theme.characterTheme,
);
}, },
); );
} }

@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; 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/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/leaderboard/leaderboard.dart';
@ -9,34 +11,162 @@ import 'package:pinball_theme/pinball_theme.dart';
/// {@endtemplate} /// {@endtemplate}
class GameOverDialog extends StatelessWidget { class GameOverDialog extends StatelessWidget {
/// {@macro game_over_dialog} /// {@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<LeaderboardRepository>(),
),
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; final CharacterTheme theme;
@override
State<GameOverDialogView> createState() => _GameOverDialogViewState();
}
class _GameOverDialogViewState extends State<GameOverDialogView> {
final playerInitialsInputController = TextEditingController();
@override
void dispose() {
playerInitialsInputController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
// TODO(ruimiguel): refactor this view once UI design finished.
return Dialog( return Dialog(
child: SizedBox( child: SizedBox(
width: 200, width: 200,
height: 200, height: 250,
child: Center( child: Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: SingleChildScrollView(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(l10n.gameOver), Text(
TextButton( l10n.gameOver,
onPressed: () => Navigator.of(context).push<void>( style: Theme.of(context).textTheme.headline4,
LeaderboardPage.route(theme: theme),
), ),
child: Text(l10n.leaderboard), 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<LeaderboardBloc, LeaderboardState>(
builder: (context, state) {
switch (state.status) {
case LeaderboardStatus.loading:
return TextButton(
onPressed: () {
context.read<LeaderboardBloc>().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<void>(
LeaderboardPage.route(theme: theme),
),
child: Text(l10n.leaderboard),
);
case LeaderboardStatus.error:
return Text(l10n.error);
}
},
); );
} }
} }

@ -15,25 +15,8 @@ class $AssetsImagesGen {
class $AssetsImagesComponentsGen { class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
AssetGenImage get flipper => AssetGenImage get background =>
const AssetGenImage('assets/images/components/flipper.png'); const AssetGenImage('assets/images/components/background.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');
} }
class Assets { class Assets {

@ -51,5 +51,21 @@
"retry": "Retry", "retry": "Retry",
"@retry": { "@retry": {
"description": "Text displayed on the retry button leaders board page" "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"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@ -3,12 +3,41 @@
/// FlutterGen /// FlutterGen
/// ***************************************************** /// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
/// File path: assets/images/ball.png
AssetGenImage get ball => const AssetGenImage('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 { class Assets {

@ -6,7 +6,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template ball} /// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around /// A solid, [BodyType.dynamic] sphere that rolls and bounces around.
/// {@endtemplate} /// {@endtemplate}
class Ball<T extends Forge2DGame> extends BodyComponent<T> class Ball<T extends Forge2DGame> extends BodyComponent<T>
with Layered, InitialPosition { with Layered, InitialPosition {
@ -90,7 +90,7 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
} }
} }
/// Applies a boost on this [Ball] /// Applies a boost on this [Ball].
void boost(Vector2 impulse) { void boost(Vector2 impulse) {
body.applyLinearImpulse(impulse); body.applyLinearImpulse(impulse);
_boostTimer = _boostDuration; _boostTimer = _boostDuration;

@ -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. /// Indicates a side of the board.
/// ///

@ -1,5 +1,11 @@
export 'ball.dart'; export 'ball.dart';
export 'board_side.dart';
export 'fire_effect.dart'; export 'fire_effect.dart';
export 'flipper.dart';
export 'flutter_sign_post.dart';
export 'initial_position.dart'; export 'initial_position.dart';
export 'joint_anchor.dart';
export 'layer.dart'; export 'layer.dart';
export 'ramp_opening.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'spaceship.dart';

@ -3,20 +3,7 @@ import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.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,
];
/// {@template flipper} /// {@template flipper}
/// A bat, typically found in pairs at the bottom of the board. /// 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} /// {@macro flipper}
Flipper({ Flipper({
required this.side, required this.side,
}) : _keys = side.isLeft ? _leftFlipperKeys : _rightFlipperKeys; });
/// The size of the [Flipper]. /// 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. /// 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. /// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion.
final BoardSide side; final BoardSide side;
/// The [LogicalKeyboardKey]s that will control the [Flipper].
///
/// [onKeyEvent] method listens to when one of these keys is pressed.
final List<LogicalKeyboardKey> _keys;
/// Applies downward linear velocity to the [Flipper], moving it to its /// Applies downward linear velocity to the [Flipper], moving it to its
/// resting position. /// resting position.
void _moveDown() { void moveDown() {
body.linearVelocity = Vector2(0, -_speed); body.linearVelocity = Vector2(0, -_speed);
} }
/// Applies upward linear velocity to the [Flipper], moving it to its highest /// Applies upward linear velocity to the [Flipper], moving it to its highest
/// position. /// position.
void _moveUp() { void moveUp() {
body.linearVelocity = Vector2(0, _speed); body.linearVelocity = Vector2(0, _speed);
} }
/// Loads the sprite that renders with the [Flipper]. /// Loads the sprite that renders with the [Flipper].
Future<void> _loadSprite() async { Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite( 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( final spriteComponent = SpriteComponent(
sprite: sprite, sprite: sprite,
@ -71,10 +55,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
anchor: Anchor.center, anchor: Anchor.center,
); );
if (side.isRight) {
spriteComponent.flipHorizontally();
}
await add(spriteComponent); await add(spriteComponent);
} }
@ -87,30 +67,36 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
flipper: this, flipper: this,
anchor: anchor, anchor: anchor,
); );
final joint = _FlipperJoint(jointDef)..create(world); final joint = _FlipperJoint(jointDef);
world.createJoint2(joint);
// FIXME(erickzanardo): when mounted the initial position is not fully unawaited(mounted.whenComplete(joint.unlock));
// reached.
unawaited(
mounted.whenComplete(joint.unlock),
);
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
final direction = side.direction; 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( bigCircleShape.position.setValues(
((size.x / 2) * direction) + (bigCircleShape.radius * -direction), ((size.x / 2) * direction) +
(bigCircleShape.radius * -direction) +
assetShadow,
0, 0,
); );
final bigCircleFixtureDef = FixtureDef(bigCircleShape); final bigCircleFixtureDef = FixtureDef(bigCircleShape);
fixturesDef.add(bigCircleFixtureDef); fixturesDef.add(bigCircleFixtureDef);
final smallCircleShape = CircleShape()..radius = 0.9; final smallCircleShape = CircleShape()..radius = size.y * 0.23;
smallCircleShape.position.setValues( smallCircleShape.position.setValues(
((size.x / 2) * -direction) + (smallCircleShape.radius * direction), ((size.x / 2) * -direction) +
(smallCircleShape.radius * direction) -
assetShadow,
0, 0,
); );
final smallCircleFixtureDef = FixtureDef(smallCircleShape); final smallCircleFixtureDef = FixtureDef(smallCircleShape);
@ -143,7 +129,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
await super.onLoad(); await super.onLoad();
renderBody = false; renderBody = false;
await Future.wait([ await Future.wait<void>([
_loadSprite(), _loadSprite(),
_anchorToJoint(), _anchorToJoint(),
]); ]);
@ -160,22 +146,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
return body; return body;
} }
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
_moveUp();
} else if (event is RawKeyUpEvent) {
_moveDown();
}
return false;
}
} }
/// {@template flipper_anchor} /// {@template flipper_anchor}
@ -204,45 +174,60 @@ class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
required Flipper flipper, required Flipper flipper,
required _FlipperAnchor anchor, required _FlipperAnchor anchor,
}) : side = flipper.side { }) : side = flipper.side {
enableLimit = true;
initialize( initialize(
flipper.body, flipper.body,
anchor.body, anchor.body,
anchor.body.position, 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; final BoardSide side;
} }
/// {@template flipper_joint}
/// [RevoluteJoint] that controls the arc motion of a [Flipper].
/// {@endtemplate}
class _FlipperJoint extends RevoluteJoint { class _FlipperJoint extends RevoluteJoint {
/// {@macro flipper_joint}
_FlipperJoint(_FlipperAnchorRevoluteJointDef def) _FlipperJoint(_FlipperAnchorRevoluteJointDef def)
: side = def.side, : 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; final BoardSide side;
// TODO(alestiago): Remove once Forge2D supports custom joints. /// Locks the [Flipper] to its resting position.
void create(World world) { ///
world.joints.add(this); /// The joint is locked when initialized in order to force the [Flipper]
bodyA.joints.add(this); /// at its resting position.
bodyB.joints.add(this); void lock() {
const angle = _sweepingAngle / 2;
setLimits(
-angle * side.direction,
-angle * side.direction,
);
} }
/// Unlocks the [Flipper] from its resting position. /// 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() { void unlock() {
setLimits( const angle = _sweepingAngle / 2;
lowerLimit * side.direction, setLimits(-angle, angle);
-upperLimit * side.direction, }
); }
// 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);
} }
} }

@ -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<void> _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<void> 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);
}
}

@ -61,6 +61,9 @@ enum Layer {
/// Collide only with Spaceship group elements. /// Collide only with Spaceship group elements.
spaceship, spaceship,
/// Collide only with Spaceship exit rail group elements.
spaceshipExitRail,
} }
/// {@template layer_mask_bits} /// {@template layer_mask_bits}
@ -89,6 +92,8 @@ extension LayerMaskBits on Layer {
return 0x0005; return 0x0005;
case Layer.spaceship: case Layer.spaceship:
return 0x000A; return 0x000A;
case Layer.spaceshipExitRail:
return 0x0004;
} }
} }
} }

@ -1,17 +1,16 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template ramp_orientation} /// {@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} /// {@endtemplate}
enum RampOrientation { enum RampOrientation {
/// Facing up on the [Board]. /// Facing up on the Board.
up, up,
/// Facing down on the [Board]. /// Facing down on the Board.
down, down,
} }

@ -5,48 +5,42 @@ import 'dart:math';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart'; import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball/flame/priority.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template spaceship}
/// A [Blueprint] which creates the spaceship feature. /// A [Blueprint] which creates the spaceship feature.
/// {@endtemplate}
class Spaceship extends Forge2DBlueprint { class Spaceship extends Forge2DBlueprint {
/// Total size of the spaceship /// {@macro spaceship}
static const radius = 10.0; 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. /// Base priority for wall while be on spaceship.
static const ballPriorityWhenOnSpaceship = 3; static const ballPriorityWhenOnSpaceship = 4;
@override @override
void build(_) { void build(_) {
final position = Vector2(
PinballGame.boardBounds.left + radius + 15,
PinballGame.boardBounds.center.dy + 30,
);
addAllContactCallback([ addAllContactCallback([
SpaceshipHoleBallContactCallback(), SpaceshipHoleBallContactCallback(),
SpaceshipEntranceBallContactCallback(), SpaceshipEntranceBallContactCallback(),
]); ]);
final rendersBehindBall = [ addAll([
SpaceshipEntrance()..initialPosition = position,
SpaceshipSaucer()..initialPosition = position, SpaceshipSaucer()..initialPosition = position,
]; SpaceshipEntrance()..initialPosition = position,
AndroidHead()..initialPosition = position,
final rendersInFrontOfBall = [ SpaceshipHole(
SpaceshipBridge()..initialPosition = position, onExitLayer: Layer.spaceshipExitRail,
SpaceshipBridgeTop()..initialPosition = position + Vector2(0, 5.5), onExitElevation: 2,
)..initialPosition = position - Vector2(5.2, 4.8),
SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8),
SpaceshipWall()..initialPosition = position, 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 @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprites = await Future.wait([ final sprite = await gameRef.loadSprite(
gameRef.loadSprite(Assets.images.components.spaceship.saucer.path), Assets.images.spaceshipSaucer.keyName,
gameRef.loadSprite(Assets.images.components.spaceship.upper.path),
]);
await add(
SpriteComponent(
sprite: sprites.first,
size: Vector2.all(Spaceship.radius * 2),
anchor: Anchor.center,
),
); );
await add( await add(
SpriteComponent( SpriteComponent(
sprite: sprites.last, sprite: sprite,
size: Vector2((Spaceship.radius * 2) + 0.5, Spaceship.radius), size: Spaceship.size,
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -((Spaceship.radius * 2) / 3.5)),
), ),
); );
@ -91,7 +75,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
@override @override
Body createBody() { Body createBody() {
final circleShape = CircleShape()..radius = Spaceship.radius; final circleShape = CircleShape()..radius = 3;
final bodyDef = BodyDef() final bodyDef = BodyDef()
..userData = this ..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<void> 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} /// {@template spaceship_bridge}
/// The main part of the [SpaceshipBridge], this [BodyComponent] /// A [BodyComponent] that provides both the collision and the rotation
/// provides both the collision and the rotation animation for the bridge. /// animation for the bridge.
/// {@endtemplate} /// {@endtemplate}
class SpaceshipBridge extends BodyComponent with InitialPosition, Layered { class AndroidHead extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_bridge} /// {@macro spaceship_bridge}
SpaceshipBridge() AndroidHead() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) {
: super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@ -159,17 +106,20 @@ class SpaceshipBridge extends BodyComponent with InitialPosition, Layered {
renderBody = false; renderBody = false;
final sprite = await gameRef.images.load( final sprite = await gameRef.images.load(
Assets.images.components.spaceship.androidBottom.path, Assets.images.spaceshipBridge.keyName,
); );
await add( await add(
SpriteAnimationComponent.fromFrameData( SpriteAnimationComponent.fromFrameData(
sprite, sprite,
SpriteAnimationData.sequenced( SpriteAnimationData.sequenced(
amount: 14, amount: 72,
stepTime: 0.2, amountPerRow: 24,
textureSize: Vector2(160, 114), 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, anchor: Anchor.center,
), ),
); );
@ -177,7 +127,7 @@ class SpaceshipBridge extends BodyComponent with InitialPosition, Layered {
@override @override
Body createBody() { Body createBody() {
final circleShape = CircleShape()..radius = Spaceship.radius / 2.5; final circleShape = CircleShape()..radius = 2;
final bodyDef = BodyDef() final bodyDef = BodyDef()
..userData = this ..userData = this
@ -203,13 +153,14 @@ class SpaceshipEntrance extends RampOpening {
pathwayLayer: Layer.spaceship, pathwayLayer: Layer.spaceship,
orientation: RampOrientation.up, orientation: RampOrientation.up,
) { ) {
priority = Spaceship.ballPriorityWhenOnSpaceship - 1; priority = Spaceship.ballPriorityWhenOnSpaceship;
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@override @override
Shape get shape { Shape get shape {
const radius = Spaceship.radius * 2; renderBody = false;
final radius = Spaceship.size.y / 2;
return PolygonShape() return PolygonShape()
..setAsEdge( ..setAsEdge(
Vector2( Vector2(
@ -226,27 +177,51 @@ class SpaceshipEntrance extends RampOpening {
/// {@template spaceship_hole} /// {@template spaceship_hole}
/// A sensor [BodyComponent] responsible for sending the [Ball] /// A sensor [BodyComponent] responsible for sending the [Ball]
/// back to the board. /// out from the [Spaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipHole extends BodyComponent with InitialPosition, Layered { class SpaceshipHole extends RampOpening {
/// {@macro spaceship_hole} /// {@macro spaceship_hole}
SpaceshipHole() { SpaceshipHole({Layer? onExitLayer, this.onExitElevation = 1})
: super(
pathwayLayer: Layer.spaceship,
outsideLayer: onExitLayer,
orientation: RampOrientation.up,
) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
/// Priority order for [SpaceshipHole] on exit.
final int onExitElevation;
@override @override
Body createBody() { Shape get shape {
renderBody = false; return ArcShape(
final circleShape = CircleShape()..radius = Spaceship.radius / 40; center: Vector2(0, 4.2),
arcRadius: 6,
angle: 1,
rotation: 60 * pi / 180,
);
}
}
final bodyDef = BodyDef() /// {@template spaceship_wall_shape}
..userData = this /// The [ChainShape] that defines the shape of the [SpaceshipWall].
..position = initialPosition /// {@endtemplate}
..type = BodyType.static; class _SpaceshipWallShape extends ChainShape {
/// {@macro spaceship_wall_shape}
_SpaceshipWallShape() {
final minorRadius = (Spaceship.size.y - 2) / 2;
final majorRadius = (Spaceship.size.x - 2) / 2;
return world.createBody(bodyDef) createChain(
..createFixture( [
FixtureDef(circleShape)..isSensor = true, // 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; layer = Layer.spaceship;
} }
@override
Future<void> 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 @override
Body createBody() { Body createBody() {
renderBody = false; renderBody = false;
final wallShape = ChainShape() final wallShape = _SpaceshipWallShape();
..createChain(
[
for (var angle = 20; angle <= 340; angle++)
Vector2(
Spaceship.radius * cos(angle * pi / 180),
Spaceship.radius * sin(angle * pi / 180),
),
],
);
final bodyDef = BodyDef() final bodyDef = BodyDef()
..userData = this ..userData = this
..position = initialPosition ..position = initialPosition
..angle = 90 * pi / 180 ..angle = 90 * pi / 172
..type = BodyType.static; ..type = BodyType.static;
return world.createBody(bodyDef) return world.createBody(bodyDef)
@ -320,22 +267,24 @@ class SpaceshipEntranceBallContactCallback
@override @override
void begin(SpaceshipEntrance entrance, Ball ball, _) { void begin(SpaceshipEntrance entrance, Ball ball, _) {
ball ball
..layer = Layer.spaceship ..showInFrontOf(entrance)
..showInFrontOf(entrance); ..layer = Layer.spaceship;
} }
} }
/// [ContactCallback] that handles the contact between the [Ball] /// [ContactCallback] that handles the contact between the [Ball]
/// and a [SpaceshipHole]. /// 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. /// board.
class SpaceshipHoleBallContactCallback class SpaceshipHoleBallContactCallback
extends ContactCallback<SpaceshipHole, Ball> { extends ContactCallback<SpaceshipHole, Ball> {
@override @override
void begin(SpaceshipHole hole, Ball ball, _) { void begin(SpaceshipHole hole, Ball ball, _) {
ball ball
..layer = Layer.board // TODO(ruimiguel): apply Elevated when PR merged.
..sendToBack(); ..priority = hole.onExitElevation
..gameRef.reorderChildren()
..layer = hole.outsideLayer;
} }
} }

@ -0,0 +1,2 @@
export 'blueprint.dart';
export 'priority.dart';

@ -1 +1,2 @@
export 'components/components.dart'; export 'components/components.dart';
export 'flame/flame.dart';

@ -7,8 +7,8 @@ environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.16.0 <3.0.0"
dependencies: dependencies:
flame: ^1.1.0-releasecandidate.6 flame: ^1.1.0
flame_forge2d: ^0.9.0-releasecandidate.6 flame_forge2d: ^0.10.0
flutter: flutter:
sdk: flutter sdk: flutter
geometry: geometry:
@ -16,7 +16,7 @@ dependencies:
dev_dependencies: dev_dependencies:
flame_test: ^1.1.0 flame_test: ^1.3.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mocktail: ^0.2.0 mocktail: ^0.2.0
@ -26,6 +26,7 @@ flutter:
generate: true generate: true
assets: assets:
- assets/images/ - assets/images/
- assets/images/flipper/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -5,7 +5,7 @@ import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class BasicGame extends Forge2DGame { abstract class BasicGame extends Forge2DGame {
BasicGame() { BasicGame() {
images.prefix = ''; images.prefix = '';
} }

@ -7,6 +7,7 @@
import 'package:dashbook/dashbook.dart'; import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sandbox/stories/effects/effects.dart'; import 'package:sandbox/stories/effects/effects.dart';
import 'package:sandbox/stories/spaceship/spaceship.dart';
import 'package:sandbox/stories/stories.dart'; import 'package:sandbox/stories/stories.dart';
void main() { void main() {
@ -15,5 +16,7 @@ void main() {
addBallStories(dashbook); addBallStories(dashbook);
addLayerStories(dashbook); addLayerStories(dashbook);
addEffectsStories(dashbook); addEffectsStories(dashbook);
addFlipperStories(dashbook);
addSpaceshipStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -7,8 +7,9 @@ class BasicBallGame extends BasicGame with TapDetector {
BasicBallGame({required this.color}); BasicBallGame({required this.color});
static const info = ''' static const info = '''
Basic example of how a Ball works, tap anywhere on the Basic example of how a Ball works.
screen to spawn a ball into the game.
Tap anywhere on the screen to spawn a ball into the game.
'''; ''';
final Color color; final Color color;

@ -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<void> 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,
]);
}
}

@ -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,
);
}

@ -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<void> 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<RevoluteJoint>().forEach(
(joint) => joint.setLimits(0, 0),
);
body.setType(BodyType.static);
unawaited(
mounted.whenComplete(() {
final sprite = children.whereType<SpriteComponent>().first;
sprite.paint.color = sprite.paint.color.withOpacity(0.5);
}),
);
}
}

@ -8,8 +8,9 @@ class BasicLayerGame extends BasicGame with TapDetector {
BasicLayerGame({required this.color}); BasicLayerGame({required this.color});
static const info = ''' static const info = '''
Basic example of how layers work with a Ball hitting other components, Basic example of how layers work when a Ball hits other components.
tap anywhere on the screen to spawn a ball into the game.
Tap anywhere on the screen to spawn a ball into the game.
'''; ''';
final Color color; final Color color;

@ -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<void> 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,
);
}
}

@ -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,
);
}

@ -1,2 +1,3 @@
export 'ball/ball.dart'; export 'ball/ball.dart';
export 'flipper/flipper.dart';
export 'layer/layer.dart'; export 'layer/layer.dart';

@ -91,14 +91,14 @@ packages:
name: flame name: flame
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-releasecandidate.6" version: "1.1.0"
flame_forge2d: flame_forge2d:
dependency: "direct main" dependency: "direct main"
description: description:
name: flame_forge2d name: flame_forge2d
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.0-releasecandidate.6" version: "0.10.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -134,7 +134,7 @@ packages:
name: forge2d name: forge2d
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.0" version: "0.10.0"
freezed_annotation: freezed_annotation:
dependency: transitive dependency: transitive
description: description:

@ -8,8 +8,8 @@ environment:
dependencies: dependencies:
dashbook: ^0.1.7 dashbook: ^0.1.7
flame: ^1.1.0-releasecandidate.6 flame: ^1.1.0
flame_forge2d: ^0.9.0-releasecandidate.6 flame_forge2d: ^0.10.0
flutter: flutter:
sdk: flutter sdk: flutter
pinball_components: pinball_components:

@ -1,5 +1,26 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
class MockCanvas extends Mock implements Canvas {} 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<Object, Object> {}

@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart';
void main() { void main() {
group( group(

@ -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<double>(
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));
},
);
});
}

@ -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);
},
);
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

@ -3,7 +3,7 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();

@ -2,8 +2,7 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -34,11 +33,11 @@ class TestRampOpeningBallContactCallback
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(TestGame.new);
group('RampOpening', () { group('RampOpening', () {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(TestGame.new);
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',

@ -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<Forge2DGame>(),
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);
});
});
});
}

@ -1,10 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/flame/blueprint.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball/game/game.dart';
import '../helpers/helpers.dart'; import '../../helpers/helpers.dart';
class MyBlueprint extends Blueprint { class MyBlueprint extends Blueprint {
@override @override
@ -52,19 +51,19 @@ void main() {
}); });
test('components can be added to it', () { test('components can be added to it', () {
final blueprint = MyBlueprint()..build(MockPinballGame()); final blueprint = MyBlueprint()..build(MockGame());
expect(blueprint.components.length, equals(3)); expect(blueprint.components.length, equals(3));
}); });
test('blueprints can be added to it', () { test('blueprints can be added to it', () {
final blueprint = MyComposedBlueprint()..build(MockPinballGame()); final blueprint = MyComposedBlueprint()..build(MockGame());
expect(blueprint.blueprints.length, equals(3)); expect(blueprint.blueprints.length, equals(3));
}); });
test('adds the components to a game on attach', () { test('adds the components to a game on attach', () {
final mockGame = MockPinballGame(); final mockGame = MockGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
MyBlueprint().attach(mockGame); MyBlueprint().attach(mockGame);
@ -72,7 +71,7 @@ void main() {
}); });
test('adds components from a child Blueprint the to a game on attach', () { 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 {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
MyComposedBlueprint().attach(mockGame); MyComposedBlueprint().attach(mockGame);
@ -82,7 +81,7 @@ void main() {
test( test(
'throws assertion error when adding to an already attached blueprint', 'throws assertion error when adding to an already attached blueprint',
() async { () async {
final mockGame = MockPinballGame(); final mockGame = MockGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
final blueprint = MyBlueprint(); final blueprint = MyBlueprint();
await blueprint.attach(mockGame); await blueprint.attach(mockGame);
@ -99,13 +98,13 @@ void main() {
}); });
test('callbacks can be added to it', () { test('callbacks can be added to it', () {
final blueprint = MyForge2dBlueprint()..build(MockPinballGame()); final blueprint = MyForge2dBlueprint()..build(MockGame());
expect(blueprint.callbacks.length, equals(3)); expect(blueprint.callbacks.length, equals(3));
}); });
test('adds the callbacks to a game on attach', () async { test('adds the callbacks to a game on attach', () async {
final mockGame = MockPinballGame(); final mockGame = MockGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {}); when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
await MyForge2dBlueprint().attach(mockGame); await MyForge2dBlueprint().attach(mockGame);
@ -116,7 +115,7 @@ void main() {
test( test(
'throws assertion error when adding to an already attached blueprint', 'throws assertion error when adding to an already attached blueprint',
() async { () async {
final mockGame = MockPinballGame(); final mockGame = MockGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {}); when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
final blueprint = MyForge2dBlueprint(); final blueprint = MyForge2dBlueprint();

@ -182,28 +182,28 @@ packages:
name: flame name: flame
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-releasecandidate.6" version: "1.1.0"
flame_bloc: flame_bloc:
dependency: "direct main" dependency: "direct main"
description: description:
name: flame_bloc name: flame_bloc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0-releasecandidate.6" version: "1.2.0"
flame_forge2d: flame_forge2d:
dependency: "direct main" dependency: "direct main"
description: description:
name: flame_forge2d name: flame_forge2d
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.0-releasecandidate.6" version: "0.10.0"
flame_test: flame_test:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flame_test name: flame_test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.3.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -237,7 +237,7 @@ packages:
name: forge2d name: forge2d
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.0" version: "0.10.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:

@ -10,9 +10,9 @@ dependencies:
bloc: ^8.0.2 bloc: ^8.0.2
cloud_firestore: ^3.1.10 cloud_firestore: ^3.1.10
equatable: ^2.0.3 equatable: ^2.0.3
flame: ^1.1.0-releasecandidate.6 flame: ^1.1.0
flame_bloc: ^1.2.0-releasecandidate.6 flame_bloc: ^1.2.0
flame_forge2d: ^0.9.0-releasecandidate.6 flame_forge2d: ^0.10.0
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: ^8.0.1 flutter_bloc: ^8.0.1
@ -30,7 +30,7 @@ dependencies:
dev_dependencies: dev_dependencies:
bloc_test: ^9.0.2 bloc_test: ^9.0.2
flame_test: ^1.1.0 flame_test: ^1.3.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mockingjay: ^0.2.0 mockingjay: ^0.2.0
@ -43,7 +43,6 @@ flutter:
assets: assets:
- assets/images/components/ - assets/images/components/
- assets/images/components/spaceship/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -3,7 +3,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart'; import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/flame/priority.dart'; import 'package:pinball_components/src/flame/priority.dart';
import '../helpers/helpers.dart'; import '../helpers/helpers.dart';

@ -27,11 +27,12 @@ void main() {
final tester = flameBlocTester(gameBloc: () => gameBloc); final tester = flameBlocTester(gameBloc: () => gameBloc);
tester.widgetTest( tester.testGameWidget(
'adds BallLost to GameBloc', 'adds BallLost to GameBloc',
(game, tester) async { setUp: (game, tester) async {
await game.ready(); await game.ready();
},
verify: (game, tester) async {
game.children.whereType<Ball>().first.controller.lost(); game.children.whereType<Ball>().first.controller.lost();
await tester.pump(); await tester.pump();
@ -39,14 +40,15 @@ void main() {
}, },
); );
tester.widgetTest( tester.testGameWidget(
'resets the ball if the game is not over', 'resets the ball if the game is not over',
(game, tester) async { setUp: (game, tester) async {
await game.ready(); await game.ready();
game.children.whereType<Ball>().first.controller.lost(); game.children.whereType<Ball>().first.controller.lost();
await game.ready(); // Making sure that all additions are done await game.ready(); // Making sure that all additions are done
},
verify: (game, tester) async {
expect( expect(
game.children.whereType<Ball>().length, game.children.whereType<Ball>().length,
equals(1), equals(1),
@ -54,9 +56,9 @@ void main() {
}, },
); );
tester.widgetTest( tester.testGameWidget(
'no ball is added on game over', 'no ball is added on game over',
(game, tester) async { setUp: (game, tester) async {
whenListen( whenListen(
gameBloc, gameBloc,
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
@ -72,7 +74,8 @@ void main() {
game.children.whereType<Ball>().first.controller.lost(); game.children.whereType<Ball>().first.controller.lost();
await tester.pump(); await tester.pump();
},
verify: (game, tester) async {
expect( expect(
game.children.whereType<Ball>().length, game.children.whereType<Ball>().length,
equals(0), equals(0),

@ -4,6 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
void main() { void main() {
group('Baseboard', () { group('Baseboard', () {

@ -3,6 +3,7 @@
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -22,9 +23,9 @@ void main() {
}, },
); );
group('children', () { group('loads', () {
flameTester.test( flameTester.test(
'has one left flipper', 'one left flipper',
(game) async { (game) async {
final board = Board(); final board = Board();
await game.ready(); await game.ready();
@ -38,7 +39,7 @@ void main() {
); );
flameTester.test( flameTester.test(
'has one right flipper', 'one right flipper',
(game) async { (game) async {
final board = Board(); final board = Board();
await game.ready(); await game.ready();
@ -51,7 +52,7 @@ void main() {
); );
flameTester.test( flameTester.test(
'has two Baseboards', 'two Baseboards',
(game) async { (game) async {
final board = Board(); final board = Board();
await game.ready(); await game.ready();
@ -63,7 +64,7 @@ void main() {
); );
flameTester.test( flameTester.test(
'has two Kickers', 'two Kickers',
(game) async { (game) async {
final board = Board(); final board = Board();
await game.ready(); await game.ready();
@ -75,7 +76,7 @@ void main() {
); );
flameTester.test( flameTester.test(
'has one FlutterForest', 'one FlutterForest',
(game) async { (game) async {
// TODO(alestiago): change to [NestBumpers] once provided. // TODO(alestiago): change to [NestBumpers] once provided.
final board = Board(); final board = Board();
@ -86,6 +87,18 @@ void main() {
expect(flutterForest.length, equals(1)); 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<ChromeDino>();
expect(chromeDino.length, equals(1));
},
);
}); });
}); });
} }

@ -29,6 +29,7 @@ void main() {
group('listenWhen', () { group('listenWhen', () {
final previousState = MockGameState(); final previousState = MockGameState();
final currentState = MockGameState(); final currentState = MockGameState();
test( test(
'returns true when there is a new word bonus awarded', 'returns true when there is a new word bonus awarded',
() { () {
@ -193,10 +194,11 @@ void main() {
}); });
group('bonus letter activation', () { group('bonus letter activation', () {
final gameBloc = MockGameBloc(); late GameBloc gameBloc;
final tester = flameBlocTester(gameBloc: () => gameBloc); final tester = flameBlocTester(gameBloc: () => gameBloc);
setUp(() { setUp(() {
gameBloc = MockGameBloc();
whenListen( whenListen(
gameBloc, gameBloc,
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
@ -204,22 +206,24 @@ void main() {
); );
}); });
tester.widgetTest( tester.testGameWidget(
'adds BonusLetterActivated to GameBloc when not activated', 'adds BonusLetterActivated to GameBloc when not activated',
(game, tester) async { setUp: (game, tester) async {
await game.ready(); await game.ready();
final bonusLetter = game.descendants().whereType<BonusLetter>().first; final bonusLetter = game.descendants().whereType<BonusLetter>().first;
bonusLetter.activate(); bonusLetter.activate();
await game.ready(); await game.ready();
await tester.pump(); await tester.pump();
},
verify: (game, tester) async {
verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1); verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1);
}, },
); );
tester.widgetTest( tester.testGameWidget(
"doesn't add BonusLetterActivated to GameBloc when already activated", "doesn't add BonusLetterActivated to GameBloc when already activated",
(game, tester) async { setUp: (game, tester) async {
const state = GameState( const state = GameState(
score: 0, score: 0,
balls: 2, balls: 2,
@ -237,14 +241,15 @@ void main() {
final bonusLetter = game.descendants().whereType<BonusLetter>().first; final bonusLetter = game.descendants().whereType<BonusLetter>().first;
bonusLetter.activate(); bonusLetter.activate();
await game.ready(); await game.ready();
},
verify: (game, tester) async {
verifyNever(() => gameBloc.add(const BonusLetterActivated(0))); verifyNever(() => gameBloc.add(const BonusLetterActivated(0)));
}, },
); );
tester.widgetTest( tester.testGameWidget(
'adds a ColorEffect', 'adds a ColorEffect',
(game, tester) async { setUp: (game, tester) async {
const state = GameState( const state = GameState(
score: 0, score: 0,
balls: 2, balls: 2,
@ -259,7 +264,9 @@ void main() {
bonusLetter.onNewState(state); bonusLetter.onNewState(state);
await tester.pump(); await tester.pump();
},
verify: (game, tester) async {
final bonusLetter = game.descendants().whereType<BonusLetter>().first;
expect( expect(
bonusLetter.children.whereType<ColorEffect>().length, bonusLetter.children.whereType<ColorEffect>().length,
equals(1), equals(1),
@ -267,9 +274,14 @@ void main() {
}, },
); );
tester.widgetTest( tester.testGameWidget(
'only listens when there is a change on the letter status', '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<BonusLetter>().first;
bonusLetter.activate();
},
verify: (game, tester) async {
const state = GameState( const state = GameState(
score: 0, score: 0,
balls: 2, balls: 2,
@ -277,11 +289,7 @@ void main() {
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
await game.ready();
final bonusLetter = game.descendants().whereType<BonusLetter>().first; final bonusLetter = game.descendants().whereType<BonusLetter>().first;
bonusLetter.activate();
expect( expect(
bonusLetter.listenWhen(const GameState.initial(), state), bonusLetter.listenWhen(const GameState.initial(), state),
isTrue, isTrue,

@ -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);
},
);
});
}

@ -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);
},
);
});
});
});
});
}

@ -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<double>(
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);
},
);
});
});
});
},
);
}

@ -25,6 +25,20 @@ void main() {
}, },
); );
group('loads', () {
flameTester.test(
'a FlutterSignPost',
(game) async {
await game.ready();
expect(
game.descendants().whereType<FlutterSignPost>().length,
equals(1),
);
},
);
});
flameTester.test( flameTester.test(
'onNewState adds a new ball', 'onNewState adds a new ball',
(game) async { (game) async {
@ -55,10 +69,12 @@ void main() {
); );
}); });
tester.widgetTest( tester.testGameWidget(
'listens when a Bonus.dashNest is added', 'listens when a Bonus.dashNest is added',
(game, tester) async { setUp: (game, tester) async {
await game.ready(); await game.ready();
},
verify: (game, tester) async {
final flutterForest = final flutterForest =
game.descendants().whereType<FlutterForest>().first; game.descendants().whereType<FlutterForest>().first;
@ -69,7 +85,6 @@ void main() {
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [GameBonus.dashNest], bonusHistory: [GameBonus.dashNest],
); );
expect( expect(
flutterForest.listenWhen(const GameState.initial(), state), flutterForest.listenWhen(const GameState.initial(), state),
isTrue, isTrue,
@ -91,15 +106,16 @@ void main() {
); );
}); });
tester.widgetTest( final dashNestBumper = MockDashNestBumper();
tester.testGameWidget(
'adds a DashNestActivated event with DashNestBumper.id', 'adds a DashNestActivated event with DashNestBumper.id',
(game, tester) async { setUp: (game, tester) async {
final contactCallback = DashNestBumperBallContactCallback();
const id = '0'; const id = '0';
final dashNestBumper = MockDashNestBumper();
when(() => dashNestBumper.id).thenReturn(id); when(() => dashNestBumper.id).thenReturn(id);
when(() => dashNestBumper.gameRef).thenReturn(game); when(() => dashNestBumper.gameRef).thenReturn(game);
},
verify: (game, tester) async {
final contactCallback = DashNestBumperBallContactCallback();
contactCallback.begin(dashNestBumper, MockBall(), MockContact()); contactCallback.begin(dashNestBumper, MockBall(), MockContact());
verify(() => gameBloc.add(DashNestActivated(dashNestBumper.id))) verify(() => gameBloc.add(DashNestActivated(dashNestBumper.id)))

@ -4,6 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
void main() { void main() {
group('Kicker', () { group('Kicker', () {

@ -233,7 +233,7 @@ void main() {
plunger: plunger, plunger: plunger,
anchor: anchor, anchor: anchor,
); );
game.world.createJoint(jointDef); game.world.createJoint(PrismaticJoint(jointDef));
expect(jointDef.bodyB, equals(anchor.body)); expect(jointDef.bodyB, equals(anchor.body));
}, },
@ -250,7 +250,7 @@ void main() {
plunger: plunger, plunger: plunger,
anchor: anchor, anchor: anchor,
); );
game.world.createJoint(jointDef); game.world.createJoint(PrismaticJoint(jointDef));
expect(jointDef.enableLimit, isTrue); expect(jointDef.enableLimit, isTrue);
}, },
@ -267,7 +267,7 @@ void main() {
plunger: plunger, plunger: plunger,
anchor: anchor, anchor: anchor,
); );
game.world.createJoint(jointDef); game.world.createJoint(PrismaticJoint(jointDef));
expect(jointDef.lowerTranslation, equals(double.negativeInfinity)); expect(jointDef.lowerTranslation, equals(double.negativeInfinity));
}, },
@ -284,7 +284,7 @@ void main() {
plunger: plunger, plunger: plunger,
anchor: anchor, anchor: anchor,
); );
game.world.createJoint(jointDef); game.world.createJoint(PrismaticJoint(jointDef));
expect(jointDef.collideConnected, isTrue); expect(jointDef.collideConnected, isTrue);
}, },
@ -292,11 +292,11 @@ void main() {
}); });
testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { testRawKeyUpEvents([LogicalKeyboardKey.space], (event) {
flameTester.widgetTest( late final anchor = PlungerAnchor(plunger: plunger);
flameTester.testGameWidget(
'plunger cannot go below anchor', 'plunger cannot go below anchor',
(game, tester) async { setUp: (game, tester) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
// Giving anchor a shape for the plunger to collide with. // Giving anchor a shape for the plunger to collide with.
@ -306,19 +306,20 @@ void main() {
plunger: plunger, plunger: plunger,
anchor: anchor, anchor: anchor,
); );
game.world.createJoint(jointDef); game.world.createJoint(PrismaticJoint(jointDef));
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
},
verify: (game, tester) async {
expect(plunger.body.position.y > anchor.body.position.y, isTrue); expect(plunger.body.position.y > anchor.body.position.y, isTrue);
}, },
); );
}); });
testRawKeyUpEvents([LogicalKeyboardKey.space], (event) { testRawKeyUpEvents([LogicalKeyboardKey.space], (event) {
flameTester.widgetTest( flameTester.testGameWidget(
'plunger cannot excessively exceed starting position', 'plunger cannot excessively exceed starting position',
(game, tester) async { setUp: (game, tester) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger); final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
@ -327,12 +328,13 @@ void main() {
plunger: plunger, plunger: plunger,
anchor: anchor, anchor: anchor,
); );
game.world.createJoint(jointDef); game.world.createJoint(PrismaticJoint(jointDef));
plunger.body.setTransform(Vector2(0, -1), 0); plunger.body.setTransform(Vector2(0, -1), 0);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
},
verify: (game, tester) async {
expect(plunger.body.position.y < 1, isTrue); expect(plunger.body.position.y < 1, isTrue);
}, },
); );

@ -1,67 +1,59 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/flame/priority.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void main() { void main() {
group('Spaceship', () { group('SpaceshipExitRail', () {
late Filter filterData;
late Fixture fixture;
late Body body;
late PinballGame game; late PinballGame game;
late SpaceshipExitRailEnd exitRailEnd;
late Ball ball; late Ball ball;
late SpaceshipEntrance entrance; late Body body;
late SpaceshipHole hole; late Fixture fixture;
late Filter filterData;
setUp(() { setUp(() {
filterData = MockFilter();
fixture = MockFixture();
when(() => fixture.filterData).thenReturn(filterData);
body = MockBody();
when(() => body.fixtures).thenReturn([fixture]);
game = MockPinballGame(); game = MockPinballGame();
exitRailEnd = MockSpaceshipExitRailEnd();
ball = MockBall(); ball = MockBall();
body = MockBody();
when(() => ball.gameRef).thenReturn(game); when(() => ball.gameRef).thenReturn(game);
when(() => ball.body).thenReturn(body); when(() => ball.body).thenReturn(body);
entrance = MockSpaceshipEntrance(); fixture = MockFixture();
hole = MockSpaceshipHole(); filterData = MockFilter();
when(() => body.fixtures).thenReturn([fixture]);
when(() => fixture.filterData).thenReturn(filterData);
}); });
group('SpaceshipEntranceBallContactCallback', () { group('SpaceshipExitHoleBallContactCallback', () {
test('changes the ball priority on contact', () { test('changes the ball priority on contact', () {
when(() => ball.priority).thenReturn(1); when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board);
when(() => entrance.priority).thenReturn(2);
SpaceshipEntranceBallContactCallback().begin( SpaceshipExitRailEndBallContactCallback().begin(
entrance, exitRailEnd,
ball, ball,
MockContact(), MockContact(),
); );
verify(() => ball.showInFrontOf(entrance)).called(1); verify(() => ball.priority = 1).called(1);
});
}); });
group('SpaceshipHoleBallContactCallback', () { test('reorders the game children', () {
test('changes the ball priority on contact', () { when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board);
when(() => ball.priority).thenReturn(1);
SpaceshipHoleBallContactCallback().begin( SpaceshipExitRailEndBallContactCallback().begin(
hole, exitRailEnd,
ball, ball,
MockContact(), MockContact(),
); );
verify(() => ball.sendToBack()).called(1); verify(game.reorderChildren).called(1);
}); });
}); });
}); });

@ -1,44 +1,195 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mockingjay/mockingjay.dart'; import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
void main() { void main() {
group('GameOverDialog', () { group('GameOverDialog', () {
testWidgets('renders correctly', (tester) async { testWidgets('renders GameOverDialogView', (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
await tester.pumpApp( await tester.pumpApp(
const GameOverDialog( GameOverDialog(
score: 1000,
theme: DashTheme(), theme: DashTheme(),
), ),
); );
expect(find.text(l10n.gameOver), findsOneWidget); expect(find.byType(GameOverDialogView), findsOneWidget);
expect(find.text(l10n.leaderboard), findsOneWidget); });
group('GameOverDialogView', () {
late LeaderboardBloc leaderboardBloc;
final leaderboard = [
LeaderboardEntry(
rank: '1',
playerInitials: 'ABC',
score: 5000,
character: DashTheme().characterAsset,
),
];
final entryData = LeaderboardEntryData(
playerInitials: 'VGV',
score: 10000,
character: CharacterType.dash,
);
setUp(() {
leaderboardBloc = MockLeaderboardBloc();
whenListen(
leaderboardBloc,
const Stream<LeaderboardState>.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<LeaderboardState>.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<LeaderboardState>.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<LeaderboardState>.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('tapping on leaderboard button navigates to LeaderBoardPage', testWidgets('navigates to LeaderboardPage when tap on leaderboard button',
(tester) async { (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en')); final l10n = await AppLocalizations.delegate.load(Locale('en'));
final navigator = MockNavigator(); final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {}); when(() => navigator.push<void>(any())).thenAnswer((_) async {});
whenListen(
leaderboardBloc,
const Stream<LeaderboardState>.empty(),
initialState: LeaderboardState(
status: LeaderboardStatus.success,
ranking: LeaderboardRanking(ranking: 1, outOf: 2),
leaderboard: leaderboard,
),
);
await tester.pumpApp( await tester.pumpApp(
const GameOverDialog( BlocProvider.value(
theme: DashTheme(), value: leaderboardBloc,
child: GameOverDialogView(
score: entryData.score,
theme: entryData.character.toTheme,
),
), ),
navigator: navigator, navigator: navigator,
); );
await tester.tap(find.widgetWithText(TextButton, l10n.leaderboard)); final button = find.widgetWithText(TextButton, l10n.leaderboard);
await tester.ensureVisible(button);
await tester.tap(button);
verify(() => navigator.push<void>(any())).called(1); verify(() => navigator.push<void>(any())).called(1);
}); });
}); });
});
} }

@ -1,4 +1,3 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.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 MockThemeCubit extends Mock implements ThemeCubit {}
class MockLeaderboardBloc extends MockBloc<LeaderboardEvent, LeaderboardState> class MockLeaderboardBloc extends Mock implements LeaderboardBloc {}
implements LeaderboardBloc {}
class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} class MockLeaderboardRepository extends Mock implements LeaderboardRepository {}
@ -68,12 +66,10 @@ class MockFilter extends Mock implements Filter {}
class MockFixture extends Mock implements Fixture {} 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 MockComponent extends Mock implements Component {}
class MockComponentSet extends Mock implements ComponentSet {}
class MockDashNestBumper extends Mock implements DashNestBumper {} class MockDashNestBumper extends Mock implements DashNestBumper {}

@ -1,5 +1,6 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -42,7 +43,11 @@ void main() {
testWidgets('renders correctly', (tester) async { testWidgets('renders correctly', (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en')); final l10n = await AppLocalizations.delegate.load(Locale('en'));
when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); whenListen(
leaderboardBloc,
const Stream<LeaderboardState>.empty(),
initialState: LeaderboardState.initial(),
);
await tester.pumpApp( await tester.pumpApp(
BlocProvider.value( BlocProvider.value(
@ -59,7 +64,11 @@ void main() {
testWidgets('renders loading view when bloc emits [loading]', testWidgets('renders loading view when bloc emits [loading]',
(tester) async { (tester) async {
when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); whenListen(
leaderboardBloc,
const Stream<LeaderboardState>.empty(),
initialState: LeaderboardState.initial(),
);
await tester.pumpApp( await tester.pumpApp(
BlocProvider.value( BlocProvider.value(
@ -76,8 +85,12 @@ void main() {
}); });
testWidgets('renders error view when bloc emits [error]', (tester) async { testWidgets('renders error view when bloc emits [error]', (tester) async {
when(() => leaderboardBloc.state).thenReturn( whenListen(
LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), leaderboardBloc,
const Stream<LeaderboardState>.empty(),
initialState: LeaderboardState.initial().copyWith(
status: LeaderboardStatus.error,
),
); );
await tester.pumpApp( await tester.pumpApp(
@ -97,8 +110,10 @@ void main() {
testWidgets('renders success view when bloc emits [success]', testWidgets('renders success view when bloc emits [success]',
(tester) async { (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en')); final l10n = await AppLocalizations.delegate.load(Locale('en'));
when(() => leaderboardBloc.state).thenReturn( whenListen(
LeaderboardState( leaderboardBloc,
const Stream<LeaderboardState>.empty(),
initialState: LeaderboardState(
status: LeaderboardStatus.success, status: LeaderboardStatus.success,
ranking: LeaderboardRanking(ranking: 0, outOf: 0), ranking: LeaderboardRanking(ranking: 0, outOf: 0),
leaderboard: [ leaderboard: [

Loading…
Cancel
Save