Merge branch 'main' into feat/launcher-ramp-assets

pull/120/head
Allison Ryan 4 years ago
commit 63fc95b355

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.

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

@ -0,0 +1,37 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
/// {@template component_controller}
/// A [ComponentController] is a [Component] in charge of handling the logic
/// associated with another [Component].
///
/// [ComponentController]s usually implement [BlocComponent].
/// {@endtemplate}
abstract class ComponentController<T extends Component> extends Component {
/// {@macro component_controller}
ComponentController(this.component);
/// The [Component] controlled by this [ComponentController].
final T component;
@override
Future<void> addToParent(Component parent) async {
assert(
parent == component,
'ComponentController should be child of $component.',
);
await super.addToParent(parent);
}
}
/// Mixin that attaches a single [ComponentController] to a [Component].
mixin Controls<T extends ComponentController> on Component {
/// The [ComponentController] attached to this [Component].
late final T controller;
@override
Future<void> onLoad() async {
await super.onLoad();
await add(controller);
}
}

@ -0,0 +1 @@
export 'component_controller.dart';

@ -55,9 +55,6 @@ class GameState extends Equatable {
/// Determines when the game is over. /// Determines when the game is over.
bool get isGameOver => balls == 0; bool get isGameOver => balls == 0;
/// Determines when the player has only one ball left.
bool get isLastBall => balls == 1;
/// Shortcut method to check if the given [i] /// Shortcut method to check if the given [i]
/// is activated. /// is activated.
bool isLetterActivated(int i) => activatedBonusLetters.contains(i); bool isLetterActivated(int i) => activatedBonusLetters.contains(i);

@ -1,88 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template ball_type}
/// Specifies the type of [Ball].
///
/// Different [BallType]s are affected by different game mechanics.
/// {@endtemplate}
enum BallType {
/// A [Ball] spawned from the [Plunger].
///
/// [normal] balls decrease the [GameState.balls] when they fall through the
/// the [BottomWall].
normal,
/// A [Ball] that does not alter [GameState.balls].
///
/// For example, a [Ball] spawned by Dash in the [FlutterForest].
extra,
}
/// {@template ball_blueprint}
/// [Blueprint] which cretes a ball game object.
/// {@endtemplate}
class BallBlueprint extends Blueprint<PinballGame> {
/// {@macro ball_blueprint}
BallBlueprint({
required this.position,
required this.type,
});
/// The initial position of the [Ball].
final Vector2 position;
/// {@macro ball_type}
final BallType type;
@override
void build(PinballGame gameRef) {
final baseColor = gameRef.theme.characterTheme.ballColor;
final ball = Ball(baseColor: baseColor)
..add(
BallController(type: type),
);
add(ball..initialPosition = position + Vector2(0, ball.size.y / 2));
}
}
/// {@template ball_controller}
/// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate}
class BallController extends Component with HasGameRef<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
/// any are left.
///
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
void lost() {
parent?.shouldRemove = true;
// TODO(alestiago): Consider adding test for this logic once we remove the
// BallX extension.
if (type != BallType.normal) return;
final bloc = gameRef.read<GameBloc>()..add(const BallLost());
final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver;
if (shouldBallRespwan) {
gameRef.spawnBall();
}
}
}
/// Adds helper methods to the [Ball]
extension BallX on Ball {
/// Returns the controller instance of the ball
// TODO(erickzanardo): Remove the need of an extension.
BallController get controller {
return children.whereType<BallController>().first;
}
}

@ -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,20 +1,22 @@
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 {
// TODO(alestiago): adjust positioning once sprites are added. // TODO(alestiago): adjust positioning once sprites are added.
final bottomGroup = _BottomGroup( final bottomGroup = _BottomGroup(
position: Vector2( position: Vector2(
PinballGame.boardBounds.center.dx, BoardDimensions.bounds.center.dx,
PinballGame.boardBounds.bottom + 10, BoardDimensions.bounds.bottom + 10,
), ),
spacing: 2, spacing: 2,
); );
@ -24,8 +26,8 @@ class Board extends Component {
// TODO(alestiago): adjust positioning to real design. // TODO(alestiago): adjust positioning to real design.
final dino = ChromeDino() final dino = ChromeDino()
..initialPosition = Vector2( ..initialPosition = Vector2(
PinballGame.boardBounds.center.dx + 25, BoardDimensions.bounds.center.dx + 25,
PinballGame.boardBounds.center.dy + 10, BoardDimensions.bounds.center.dy + 10,
); );
await addAll([ await addAll([
@ -92,15 +94,17 @@ class _BottomGroupSide extends Component {
Future<void> onLoad() async { Future<void> onLoad() async {
final direction = _side.direction; final direction = _side.direction;
final flipper = Flipper( final flipper = ControlledFlipper(
side: _side, side: _side,
)..initialPosition = _position; )..initialPosition = _position;
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 +

@ -4,7 +4,6 @@ import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart' hide Timer; import 'package:flame_forge2d/flame_forge2d.dart' hide Timer;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template chrome_dino} /// {@template chrome_dino}
@ -32,7 +31,7 @@ class ChromeDino extends BodyComponent with InitialPosition {
anchor: anchor, anchor: anchor,
); );
final joint = _ChromeDinoJoint(jointDef); final joint = _ChromeDinoJoint(jointDef);
world.createJoint2(joint); world.createJoint(joint);
return joint; return joint;
} }
@ -155,15 +154,3 @@ class _ChromeDinoJoint extends RevoluteJoint {
setMotorSpeed(-motorSpeed); 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,17 +1,14 @@
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 'chrome_dino.dart'; export 'chrome_dino.dart';
export 'flipper.dart'; export 'controlled_ball.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,102 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template controlled_ball}
/// A [Ball] with a [BallController] attached.
/// {@endtemplate}
class ControlledBall extends Ball with Controls<BallController> {
/// A [Ball] that launches from the [Plunger].
///
/// When a launched [Ball] is lost, it will decrease the [GameState.balls]
/// count, and a new [Ball] is spawned.
ControlledBall.launch({
required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) {
controller = LaunchedBallController(this);
}
/// {@template bonus_ball}
/// {@macro controlled_ball}
///
/// When a bonus [Ball] is lost, the [GameState.balls] doesn't change.
/// {@endtemplate}
ControlledBall.bonus({
required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) {
controller = BonusBallController(this);
}
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = BonusBallController(this);
}
}
/// {@template ball_controller}
/// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate}
abstract class BallController extends ComponentController<Ball> {
/// {@macro ball_controller}
BallController(Ball ball) : super(ball);
/// Removes the [Ball] from a [PinballGame].
///
/// {@template ball_controller_lost}
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
/// {@endtemplate}
void lost();
}
/// {@template bonus_ball_controller}
/// {@macro ball_controller}
///
/// A [BonusBallController] doesn't change the [GameState.balls] count.
/// {@endtemplate}
class BonusBallController extends BallController {
/// {@macro bonus_ball_controller}
BonusBallController(Ball<Forge2DGame> component) : super(component);
@override
void lost() {
component.shouldRemove = true;
}
}
/// {@template launched_ball_controller}
/// {@macro ball_controller}
///
/// A [LaunchedBallController] changes the [GameState.balls] count.
/// {@endtemplate}
class LaunchedBallController extends BallController
with HasGameRef<PinballGame>, BlocComponent<GameBloc, GameState> {
/// {@macro launched_ball_controller}
LaunchedBallController(Ball<Forge2DGame> ball) : super(ball);
@override
bool listenWhen(GameState? previousState, GameState newState) {
return (previousState?.balls ?? 0) > newState.balls;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
component.shouldRemove = true;
if (state.balls > 1) gameRef.spawnBall();
}
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left.
///
/// {@macro ball_controller_lost}
@override
void lost() {
gameRef.read<GameBloc>().add(const BallLost());
}
}

@ -0,0 +1,65 @@
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template controlled_flipper}
/// A [Flipper] with a [FlipperController] attached.
/// {@endtemplate}
class ControlledFlipper extends Flipper with Controls<FlipperController> {
/// {@macro controlled_flipper}
ControlledFlipper({
required BoardSide side,
}) : super(side: side) {
controller = FlipperController(this);
}
}
/// {@template flipper_controller}
/// A [ComponentController] that controls a [Flipper]s movement.
/// {@endtemplate}
class FlipperController extends ComponentController<Flipper>
with KeyboardHandler {
/// {@macro flipper_controller}
FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys,
super(flipper);
/// The [LogicalKeyboardKey]s that will control the [Flipper].
///
/// [onKeyEvent] method listens to when one of these keys is pressed.
final List<LogicalKeyboardKey> _keys;
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
component.moveUp();
} else if (event is RawKeyUpEvent) {
component.moveDown();
}
return false;
}
}
extension on BoardSide {
List<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';
@ -32,11 +31,11 @@ class FlutterForest extends Component
@override @override
void onNewState(GameState state) { void onNewState(GameState state) {
super.onNewState(state); super.onNewState(state);
gameRef.addFromBlueprint(
BallBlueprint( add(
position: Vector2(17.2, 52.7), ControlledBall.bonus(
type: BallType.extra, theme: gameRef.theme,
), )..initialPosition = Vector2(17.2, 52.7),
); );
} }
@ -44,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);
@ -53,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';
@ -14,8 +13,8 @@ class Jetpack extends Forge2DBlueprint {
@override @override
void build(_) { void build(_) {
final position = Vector2( final position = Vector2(
PinballGame.boardBounds.left + 40.5, BoardDimensions.bounds.left + 40.5,
PinballGame.boardBounds.top - 31.5, BoardDimensions.bounds.top - 31.5,
); );
addAllContactCallback([ addAllContactCallback([
@ -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}

@ -6,7 +6,6 @@ import 'package:flame/components.dart';
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/gen/assets.gen.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;

@ -1,7 +1,6 @@
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:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template plunger} /// {@template plunger}
@ -26,10 +25,10 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
1.35, 1.35,
0.5, 0.5,
Vector2.zero(), Vector2.zero(),
PinballGame.boardPerspectiveAngle, BoardDimensions.perspectiveAngle,
); );
final fixtureDef = FixtureDef(shape)..density = 20; final fixtureDef = FixtureDef(shape)..density = 80;
final bodyDef = BodyDef() final bodyDef = BodyDef()
..position = initialPosition ..position = initialPosition
@ -50,7 +49,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
/// The velocity's magnitude depends on how far the [Plunger] has been pulled /// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition]. /// from its original [initialPosition].
void _release() { void _release() {
final velocity = (initialPosition.y - body.position.y) * 4; final velocity = (initialPosition.y - body.position.y) * 5;
body.linearVelocity = Vector2(0, velocity); body.linearVelocity = Vector2(0, velocity);
} }
@ -85,7 +84,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
@ -127,12 +126,12 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
plunger.body, plunger.body,
anchor.body, anchor.body,
anchor.body.position, anchor.body.position,
Vector2(18.6, PinballGame.boardBounds.height), Vector2(18.6, BoardDimensions.bounds.height),
); );
enableLimit = true; enableLimit = true;
lowerTranslation = double.negativeInfinity; lowerTranslation = double.negativeInfinity;
enableMotor = true; enableMotor = true;
motorSpeed = 80; motorSpeed = 1000;
maxMotorForce = motorSpeed; maxMotorForce = motorSpeed;
collideConnected = true; collideConnected = true;
} }

@ -18,16 +18,23 @@ mixin ScorePoints<T extends Forge2DGame> on BodyComponent<T> {
} }
} }
/// {@template ball_score_points_callbacks}
/// Adds points to the score when a [Ball] collides with a [BodyComponent] that /// Adds points to the score when a [Ball] collides with a [BodyComponent] that
/// implements [ScorePoints]. /// implements [ScorePoints].
/// {@endtemplate}
class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> { class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
/// {@macro ball_score_points_callbacks}
BallScorePointsCallback(PinballGame game) : _gameRef = game;
final PinballGame _gameRef;
@override @override
void begin( void begin(
Ball ball, Ball _,
ScorePoints scorePoints, ScorePoints scorePoints,
Contact _, Contact __,
) { ) {
ball.controller.gameRef.read<GameBloc>().add( _gameRef.read<GameBloc>().add(
Scored(points: scorePoints.points), Scored(points: scorePoints.points),
); );
} }

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

@ -3,7 +3,6 @@
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:pinball/game/components/components.dart'; import 'package:pinball/game/components/components.dart';
import 'package:pinball/game/pinball_game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template wall} /// {@template wall}
@ -42,13 +41,12 @@ class Wall extends BodyComponent {
/// Create top, left, and right [Wall]s for the game board. /// Create top, left, and right [Wall]s for the game board.
List<Wall> createBoundaries(Forge2DGame game) { List<Wall> createBoundaries(Forge2DGame game) {
final topLeft = final topLeft = BoardDimensions.bounds.topLeft.toVector2() + Vector2(18.6, 0);
PinballGame.boardBounds.topLeft.toVector2() + Vector2(18.6, 0); final bottomRight = BoardDimensions.bounds.bottomRight.toVector2();
final bottomRight = PinballGame.boardBounds.bottomRight.toVector2();
final topRight = final topRight =
PinballGame.boardBounds.topRight.toVector2() - Vector2(18.6, 0); BoardDimensions.bounds.topRight.toVector2() - Vector2(18.6, 0);
final bottomLeft = PinballGame.boardBounds.bottomLeft.toVector2(); final bottomLeft = BoardDimensions.bounds.bottomLeft.toVector2();
return [ return [
Wall(start: topLeft, end: topRight), Wall(start: topLeft, end: topRight),
@ -67,8 +65,8 @@ class BottomWall extends Wall {
/// {@macro bottom_wall} /// {@macro bottom_wall}
BottomWall() BottomWall()
: super( : super(
start: PinballGame.boardBounds.bottomLeft.toVector2(), start: BoardDimensions.bounds.bottomLeft.toVector2(),
end: PinballGame.boardBounds.bottomRight.toVector2(), end: BoardDimensions.bounds.bottomRight.toVector2(),
); );
} }
@ -78,6 +76,7 @@ class BottomWall extends Wall {
class BottomWallBallContactCallback extends ContactCallback<Ball, BottomWall> { class BottomWallBallContactCallback extends ContactCallback<Ball, BottomWall> {
@override @override
void begin(Ball ball, BottomWall wall, Contact contact) { void begin(Ball ball, BottomWall wall, Contact contact) {
ball.controller.lost(); // TODO(alestiago): replace with .firstChild when available.
ball.children.whereType<BallController>().first.lost();
} }
} }

@ -8,7 +8,9 @@ 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(components.Assets.images.flipper.left.keyName),
images.load(components.Assets.images.flipper.right.keyName),
images.load(Assets.images.components.background.path), images.load(Assets.images.components.background.path),
images.load(Assets.images.components.launchRamp.launchRamp.path), images.load(Assets.images.components.launchRamp.launchRamp.path),
images.load(Assets.images.components.launchRamp.launchRailFG.path), images.load(Assets.images.components.launchRamp.launchRailFG.path),

@ -1,15 +1,14 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart'; 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/gen/assets.gen.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; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class PinballGame extends Forge2DGame class PinballGame extends Forge2DGame
@ -22,15 +21,6 @@ class PinballGame extends Forge2DGame
late final Plunger plunger; late final Plunger plunger;
static final boardSize = Vector2(101.6, 143.8);
static final boardBounds = Rect.fromCenter(
center: Offset.zero,
width: boardSize.x,
height: -boardSize.y,
);
static final boardPerspectiveAngle =
-math.atan(18.6 / PinballGame.boardBounds.height);
@override @override
void onAttach() { void onAttach() {
super.onAttach(); super.onAttach();
@ -46,7 +36,20 @@ 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
@ -55,7 +58,7 @@ class PinballGame extends Forge2DGame
} }
void _addContactCallbacks() { void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback()); addContactCallback(BallScorePointsCallback(this));
addContactCallback(BottomWallBallContactCallback()); addContactCallback(BottomWallBallContactCallback());
addContactCallback(BonusLetterBallContactCallback()); addContactCallback(BonusLetterBallContactCallback());
} }
@ -67,7 +70,8 @@ class PinballGame extends Forge2DGame
Future<void> _addPlunger() async { Future<void> _addPlunger() async {
plunger = Plunger(compressionDistance: 29) plunger = Plunger(compressionDistance: 29)
..initialPosition = boardBounds.center.toVector2() + Vector2(41.5, -49); ..initialPosition =
BoardDimensions.bounds.center.toVector2() + Vector2(41.5, -49);
await add(plunger); await add(plunger);
} }
@ -75,8 +79,8 @@ class PinballGame extends Forge2DGame
await add( await add(
BonusWord( BonusWord(
position: Vector2( position: Vector2(
boardBounds.center.dx - 3.07, BoardDimensions.bounds.center.dx - 3.07,
boardBounds.center.dy - 2.4, BoardDimensions.bounds.center.dy - 2.4,
), ),
), ),
); );
@ -88,12 +92,13 @@ class PinballGame extends Forge2DGame
} }
void spawnBall() { void spawnBall() {
addFromBlueprint( final ball = ControlledBall.launch(
BallBlueprint( theme: theme,
position: plunger.body.position, )..initialPosition = Vector2(
type: BallType.normal, plunger.body.position.x,
), plunger.body.position.y + Ball.size.y,
); );
add(ball);
} }
} }
@ -125,11 +130,8 @@ class DebugPinballGame extends PinballGame with TapDetector {
@override @override
void onTapUp(TapUpInfo info) { void onTapUp(TapUpInfo info) {
addFromBlueprint( add(
BallBlueprint( ControlledBall.debug()..initialPosition = info.eventPosition.game,
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);
}
},
); );
} }
} }

@ -3,8 +3,6 @@
/// FlutterGen /// FlutterGen
/// ***************************************************** /// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
@ -17,7 +15,6 @@ class $AssetsImagesGen {
class $AssetsImagesComponentsGen { class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
/// File path: assets/images/components/background.png
AssetGenImage get background => AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png'); const AssetGenImage('assets/images/components/background.png');

@ -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.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

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 {
@ -23,13 +23,14 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
} }
/// The size of the [Ball] /// The size of the [Ball]
final Vector2 size = Vector2.all(3); static final Vector2 size = Vector2.all(4.5);
/// The base [Color] used to tint this [Ball] /// The base [Color] used to tint this [Ball]
final Color baseColor; final Color baseColor;
double _boostTimer = 0; double _boostTimer = 0;
static const _boostDuration = 2.0; static const _boostDuration = 2.0;
late SpriteComponent _spriteComponent;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -37,9 +38,9 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
final sprite = await gameRef.loadSprite(Assets.images.ball.keyName); final sprite = await gameRef.loadSprite(Assets.images.ball.keyName);
final tint = baseColor.withOpacity(0.5); final tint = baseColor.withOpacity(0.5);
await add( await add(
SpriteComponent( _spriteComponent = SpriteComponent(
sprite: sprite, sprite: sprite,
size: size, size: size * 1.15,
anchor: Anchor.center, anchor: Anchor.center,
)..tint(tint), )..tint(tint),
); );
@ -88,11 +89,27 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
unawaited(gameRef.add(effect)); unawaited(gameRef.add(effect));
} }
_rescale();
} }
/// 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;
} }
void _rescale() {
final boardHeight = BoardDimensions.size.y;
const maxShrinkAmount = BoardDimensions.perspectiveShrinkFactor;
final adjustedYPosition = body.position.y + (boardHeight / 2);
final scaleFactor = ((boardHeight - adjustedYPosition) /
BoardDimensions.shrinkAdjustedHeight) +
maxShrinkAmount;
body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor;
_spriteComponent.scale = Vector2.all(scaleFactor);
}
} }

@ -0,0 +1,29 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
/// {@template board_dimensions}
/// Contains various board properties and dimensions for global use.
/// {@endtemplate}
// TODO(allisonryan0002): consider alternatives for global dimensions.
class BoardDimensions {
/// Width and height of the board.
static final size = Vector2(101.6, 143.8);
/// [Rect] for easier access to board boundaries.
static final bounds = Rect.fromCenter(
center: Offset.zero,
width: size.x,
height: -size.y,
);
/// 3D perspective angle of the board in radians.
static final perspectiveAngle = -math.atan(18.6 / bounds.height);
/// Factor the board shrinks by from the closest point to the farthest.
static const perspectiveShrinkFactor = 0.63;
/// Board height based on the [perspectiveShrinkFactor].
static final shrinkAdjustedHeight =
(1 / (1 - perspectiveShrinkFactor)) * size.y;
}

@ -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,12 @@
export 'ball.dart'; export 'ball.dart';
export 'board_dimensions.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.createJoint(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,48 @@ 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,
);
} }
} }

@ -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,23 +5,24 @@ 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/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;
@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(),
@ -30,10 +31,12 @@ class Spaceship extends Forge2DBlueprint {
addAll([ addAll([
SpaceshipSaucer()..initialPosition = position, SpaceshipSaucer()..initialPosition = position,
SpaceshipEntrance()..initialPosition = position, SpaceshipEntrance()..initialPosition = position,
SpaceshipBridge()..initialPosition = position, AndroidHead()..initialPosition = position,
SpaceshipBridgeTop()..initialPosition = position + Vector2(0, 5.5), SpaceshipHole(
SpaceshipHole()..initialPosition = position - Vector2(5, 4), onExitLayer: Layer.spaceshipExitRail,
SpaceshipHole()..initialPosition = position - Vector2(-5, 4), onExitElevation: 2,
)..initialPosition = position - Vector2(5.2, 4.8),
SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8),
SpaceshipWall()..initialPosition = position, SpaceshipWall()..initialPosition = position,
]); ]);
} }
@ -44,32 +47,23 @@ class Spaceship extends Forge2DBlueprint {
/// {@endtemplate} /// {@endtemplate}
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_saucer} /// {@macro spaceship_saucer}
SpaceshipSaucer() : super(priority: 2) { // TODO(ruimiguel): apply Elevated when PR merged.
SpaceshipSaucer() : super(priority: 3) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@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)),
), ),
); );
@ -78,7 +72,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
@ -92,48 +86,13 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
} }
} }
/// {@spaceship_bridge_top}
/// The bridge of the spaceship (the android head) is divided in two
// [BodyComponent]s, this is the top part of it which contains a single sprite
/// {@endtemplate}
class SpaceshipBridgeTop extends BodyComponent with InitialPosition {
/// {@macro spaceship_bridge_top}
SpaceshipBridgeTop() : super(priority: 6);
@override
Future<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() : super(priority: 3) { AndroidHead() : super(priority: 4) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@ -144,17 +103,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,
), ),
); );
@ -162,7 +124,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
@ -191,9 +153,14 @@ class SpaceshipEntrance extends RampOpening {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
/// Priority order for [SpaceshipHole] on enter.
// TODO(ruimiguel): apply Elevated when PR merged.
final int onEnterElevation = 4;
@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(
@ -210,27 +177,52 @@ 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.
// TODO(ruimiguel): apply Elevated when PR merged.
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),
),
],
); );
} }
} }
@ -243,48 +235,21 @@ class SpaceshipHole extends BodyComponent with InitialPosition, Layered {
/// {@endtemplate} /// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered { class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_wall} /// {@macro spaceship_wall}
// TODO(ruimiguel): apply Elevated when PR merged
SpaceshipWall() : super(priority: 4) { SpaceshipWall() : super(priority: 4) {
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)
@ -304,7 +269,8 @@ class SpaceshipEntranceBallContactCallback
@override @override
void begin(SpaceshipEntrance entrance, Ball ball, _) { void begin(SpaceshipEntrance entrance, Ball ball, _) {
ball ball
..priority = 3 // TODO(ruimiguel): apply Elevated when PR merged.
..priority = entrance.onEnterElevation
..gameRef.reorderChildren() ..gameRef.reorderChildren()
..layer = Layer.spaceship; ..layer = Layer.spaceship;
} }
@ -313,15 +279,16 @@ class SpaceshipEntranceBallContactCallback
/// [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
..priority = 1 // TODO(ruimiguel): apply Elevated when PR merged.
..priority = hole.onExitElevation
..gameRef.reorderChildren() ..gameRef.reorderChildren()
..layer = Layer.board; ..layer = hole.outsideLayer;
} }
} }

@ -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> {}

@ -86,7 +86,7 @@ void main() {
final fixture = ball.body.fixtures[0]; final fixture = ball.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle)); expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(1.5)); expect(fixture.shape.radius, equals(2.25));
}, },
); );

@ -0,0 +1,27 @@
import 'package:flame/extensions.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('BoardDimensions', () {
test('has size', () {
expect(BoardDimensions.size, equals(Vector2(101.6, 143.8)));
});
test('has bounds', () {
expect(BoardDimensions.bounds, isNotNull);
});
test('has perspectiveAngle', () {
expect(BoardDimensions.perspectiveAngle, isNotNull);
});
test('has perspectiveShrinkFactor', () {
expect(BoardDimensions.perspectiveShrinkFactor, equals(0.63));
});
test('has shrinkAdjustedHeight', () {
expect(BoardDimensions.shrinkAdjustedHeight, isNotNull);
});
});
}

@ -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',

@ -1,7 +1,9 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.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/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';
@ -11,10 +13,10 @@ void main() {
late Filter filterData; late Filter filterData;
late Fixture fixture; late Fixture fixture;
late Body body; late Body body;
late PinballGame game;
late Ball ball; late Ball ball;
late SpaceshipEntrance entrance; late SpaceshipEntrance entrance;
late SpaceshipHole hole; late SpaceshipHole hole;
late Forge2DGame game;
setUp(() { setUp(() {
filterData = MockFilter(); filterData = MockFilter();
@ -25,7 +27,7 @@ void main() {
body = MockBody(); body = MockBody();
when(() => body.fixtures).thenReturn([fixture]); when(() => body.fixtures).thenReturn([fixture]);
game = MockPinballGame(); game = MockGame();
ball = MockBall(); ball = MockBall();
when(() => ball.gameRef).thenReturn(game); when(() => ball.gameRef).thenReturn(game);
@ -35,18 +37,42 @@ void main() {
hole = MockSpaceshipHole(); hole = MockSpaceshipHole();
}); });
group('Spaceship', () {
final tester = FlameTester(TestGame.new);
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.addFromBlueprint(Spaceship(position: Vector2(30, -30)));
await game.ready();
await tester.pump();
},
verify: (game, tester) async {
// FIXME(erickzanardo): Failing pipeline.
// await expectLater(
// find.byGame<Forge2DGame>(),
// matchesGoldenFile('golden/spaceship.png'),
// );
},
);
});
group('SpaceshipEntranceBallContactCallback', () { group('SpaceshipEntranceBallContactCallback', () {
test('changes the ball priority on contact', () { test('changes the ball priority on contact', () {
when(() => entrance.onEnterElevation).thenReturn(3);
SpaceshipEntranceBallContactCallback().begin( SpaceshipEntranceBallContactCallback().begin(
entrance, entrance,
ball, ball,
MockContact(), MockContact(),
); );
verify(() => ball.priority = 3).called(1); verify(() => ball.priority = entrance.onEnterElevation).called(1);
}); });
test('re order the game children', () { test('re order the game children', () {
when(() => entrance.onEnterElevation).thenReturn(3);
SpaceshipEntranceBallContactCallback().begin( SpaceshipEntranceBallContactCallback().begin(
entrance, entrance,
ball, ball,
@ -59,16 +85,22 @@ void main() {
group('SpaceshipHoleBallContactCallback', () { group('SpaceshipHoleBallContactCallback', () {
test('changes the ball priority on contact', () { test('changes the ball priority on contact', () {
when(() => hole.outsideLayer).thenReturn(Layer.board);
when(() => hole.onExitElevation).thenReturn(1);
SpaceshipHoleBallContactCallback().begin( SpaceshipHoleBallContactCallback().begin(
hole, hole,
ball, ball,
MockContact(), MockContact(),
); );
verify(() => ball.priority = 1).called(1); verify(() => ball.priority = hole.onExitElevation).called(1);
}); });
test('re order the game children', () { test('re order the game children', () {
when(() => hole.outsideLayer).thenReturn(Layer.board);
when(() => hole.onExitElevation).thenReturn(1);
SpaceshipHoleBallContactCallback().begin( SpaceshipHoleBallContactCallback().begin(
hole, hole,
ball, ball,

@ -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

@ -0,0 +1,66 @@
// ignore_for_file: cascade_invocations
import 'package:flame/game.dart';
import 'package:flame/src/components/component.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/flame/flame.dart';
class TestComponentController extends ComponentController {
TestComponentController(Component component) : super(component);
}
class ControlledComponent extends Component
with Controls<TestComponentController> {
ControlledComponent() : super() {
controller = TestComponentController(this);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(FlameGame.new);
group('ComponentController', () {
flameTester.test(
'can be instantiated',
(game) async {
expect(
TestComponentController(Component()),
isA<ComponentController>(),
);
},
);
flameTester.test(
'throws AssertionError when not attached to controlled component',
(game) async {
final component = Component();
final controller = TestComponentController(component);
final anotherComponet = Component();
await expectLater(
() async => await anotherComponet.add(controller),
throwsAssertionError,
);
},
);
});
group('Controls', () {
flameTester.test(
'can be instantiated',
(game) async {
expect(ControlledComponent(), isA<Component>());
},
);
flameTester.test('adds controller', (game) async {
final component = ControlledComponent();
await game.add(component);
await game.ready();
expect(component.contains(component.controller), isTrue);
});
});
}

@ -103,38 +103,6 @@ void main() {
}); });
}); });
group('isLastBall', () {
test(
'is true '
'when there is only one ball left',
() {
const gameState = GameState(
balls: 1,
score: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
);
expect(gameState.isLastBall, isTrue);
},
);
test(
'is false '
'when there are more balls left',
() {
const gameState = GameState(
balls: 2,
score: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
);
expect(gameState.isLastBall, isFalse);
},
);
});
group('isLetterActivated', () { group('isLetterActivated', () {
test( test(
'is true when the letter is activated', 'is true when the letter is activated',

@ -1,84 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Ball', () {
group('lost', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final tester = flameBlocTester(gameBloc: () => gameBloc);
tester.widgetTest(
'adds BallLost to GameBloc',
(game, tester) async {
await game.ready();
game.children.whereType<Ball>().first.controller.lost();
await tester.pump();
verify(() => gameBloc.add(const BallLost())).called(1);
},
);
tester.widgetTest(
'resets the ball if the game is not over',
(game, tester) async {
await game.ready();
game.children.whereType<Ball>().first.controller.lost();
await game.ready(); // Making sure that all additions are done
expect(
game.children.whereType<Ball>().length,
equals(1),
);
},
);
tester.widgetTest(
'no ball is added on game over',
(game, tester) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState(
score: 10,
balls: 1,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
);
await game.ready();
game.children.whereType<Ball>().first.controller.lost();
await tester.pump();
expect(
game.children.whereType<Ball>().length,
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();
@ -88,7 +89,7 @@ void main() {
); );
flameTester.test( flameTester.test(
'has one ChromeDino', 'one ChromeDino',
(game) async { (game) async {
final board = Board(); final board = Board();
await game.ready(); await game.ready();

@ -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,16 @@ void main() {
}); });
group('bonus letter activation', () { group('bonus letter activation', () {
final gameBloc = MockGameBloc(); late GameBloc gameBloc;
final tester = flameBlocTester(gameBloc: () => gameBloc);
final tester = flameBlocTester<PinballGame>(
// TODO(alestiago): Use TestGame once BonusLetter has controller.
game: PinballGameTest.create,
gameBloc: () => gameBloc,
);
setUp(() { setUp(() {
gameBloc = MockGameBloc();
whenListen( whenListen(
gameBloc, gameBloc,
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
@ -204,22 +211,23 @@ 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(); 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,
@ -233,18 +241,21 @@ void main() {
initialState: state, initialState: state,
); );
final bonusLetter = BonusLetter(letter: '', index: 0);
await game.add(bonusLetter);
await game.ready(); await game.ready();
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,
@ -253,13 +264,19 @@ void main() {
bonusHistory: [], bonusHistory: [],
); );
final bonusLetter = BonusLetter(letter: '', index: 0);
await game.add(bonusLetter);
await game.ready(); await game.ready();
final bonusLetter = game.descendants().whereType<BonusLetter>().first;
bonusLetter.activate(); bonusLetter.activate();
bonusLetter.onNewState(state); bonusLetter.onNewState(state);
await tester.pump(); await tester.pump();
},
verify: (game, tester) async {
// TODO(aleastiago): Look into making `testGameWidget` pass the
// subject.
final bonusLetter = game.descendants().whereType<BonusLetter>().last;
expect( expect(
bonusLetter.children.whereType<ColorEffect>().length, bonusLetter.children.whereType<ColorEffect>().length,
equals(1), equals(1),
@ -267,9 +284,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 +299,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,197 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('BonusBallController', () {
late Ball ball;
setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
});
test('can be instantiated', () {
expect(
BonusBallController(ball),
isA<BonusBallController>(),
);
});
flameTester.test(
'lost removes ball',
(game) async {
await game.add(ball);
final controller = BonusBallController(ball);
await ball.ensureAdd(controller);
controller.lost();
await game.ready();
expect(game.contains(ball), isFalse);
},
);
});
group('LaunchedBallController', () {
test('can be instantiated', () {
expect(
LaunchedBallController(MockBall()),
isA<LaunchedBallController>(),
);
});
group('description', () {
late Ball ball;
late GameBloc gameBloc;
setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final tester = flameBlocTester<PinballGame>(
game: PinballGameTest.create,
gameBloc: () => gameBloc,
);
tester.testGameWidget(
'lost adds BallLost to GameBloc',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
controller.lost();
},
verify: (game, tester) async {
verify(() => gameBloc.add(const BallLost())).called(1);
},
);
group('listenWhen', () {
tester.testGameWidget(
'listens when a ball has been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(2);
expect(controller.listenWhen(previousState, newState), isTrue);
},
);
tester.testGameWidget(
'does not listen when a ball has not been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(3);
expect(controller.listenWhen(previousState, newState), isFalse);
},
);
});
group('onNewState', () {
tester.testGameWidget(
'removes ball',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(1);
controller.onNewState(state);
await game.ready();
},
verify: (game, tester) async {
expect(game.contains(ball), isFalse);
},
);
tester.testGameWidget(
'spawns a new ball when the ball is not the last one',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(2);
final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state);
await game.ready();
final currentBalls = game.descendants().whereType<Ball>();
expect(currentBalls.contains(ball), isFalse);
expect(currentBalls.length, equals(previousBalls.length));
},
);
tester.testGameWidget(
'does not spawn a new ball is the last one',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(1);
final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state);
await game.ready();
final currentBalls = game.descendants().whereType<Ball>();
expect(currentBalls.contains(ball), isFalse);
expect(
currentBalls.length,
equals((previousBalls..remove(ball)).length),
);
},
);
});
});
});
}

@ -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 {
@ -45,7 +59,10 @@ void main() {
group('listenWhen', () { group('listenWhen', () {
final gameBloc = MockGameBloc(); final gameBloc = MockGameBloc();
final tester = flameBlocTester(gameBloc: () => gameBloc); final tester = flameBlocTester(
game: TestGame.new,
gameBloc: () => gameBloc,
);
setUp(() { setUp(() {
whenListen( whenListen(
@ -55,12 +72,10 @@ void main() {
); );
}); });
tester.widgetTest( tester.testGameWidget(
'listens when a Bonus.dashNest is added', 'listens when a Bonus.dashNest is added',
(game, tester) async { verify: (game, tester) async {
await game.ready(); final flutterForest = FlutterForest();
final flutterForest =
game.descendants().whereType<FlutterForest>().first;
const state = GameState( const state = GameState(
score: 0, score: 0,
@ -69,7 +84,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,
@ -81,7 +95,11 @@ void main() {
group('DashNestBumperBallContactCallback', () { group('DashNestBumperBallContactCallback', () {
final gameBloc = MockGameBloc(); final gameBloc = MockGameBloc();
final tester = flameBlocTester(gameBloc: () => gameBloc); final tester = flameBlocTester(
// TODO(alestiago): Use TestGame.new once a controller is implemented.
game: PinballGameTest.create,
gameBloc: () => gameBloc,
);
setUp(() { setUp(() {
whenListen( whenListen(
@ -91,19 +109,21 @@ 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(
.called(1); () => gameBloc.add(DashNestActivated(dashNestBumper.id)),
).called(1);
}, },
); );
}); });

@ -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', () {

@ -2,7 +2,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
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/services.dart'; import 'package:flutter/services.dart';
@ -189,22 +188,14 @@ void main() {
group('PlungerAnchorPrismaticJointDef', () { group('PlungerAnchorPrismaticJointDef', () {
const compressionDistance = 10.0; const compressionDistance = 10.0;
final gameBloc = MockGameBloc();
late Plunger plunger; late Plunger plunger;
setUp(() { setUp(() {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
plunger = Plunger( plunger = Plunger(
compressionDistance: compressionDistance, compressionDistance: compressionDistance,
); );
}); });
final flameTester = flameBlocTester(gameBloc: () => gameBloc);
group('initializes with', () { group('initializes with', () {
flameTester.test( flameTester.test(
'plunger body as bodyA', 'plunger body as bodyA',
@ -233,7 +224,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 +241,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 +258,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 +275,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 +283,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 +297,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 +319,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,4 +1,3 @@
import 'package:flame/components.dart';
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';
@ -7,14 +6,6 @@ import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
class MockGameBloc extends Mock implements GameBloc {}
class MockPinballGame extends Mock implements PinballGame {}
class FakeContact extends Fake implements Contact {}
class FakeGameEvent extends Fake implements GameEvent {}
class FakeScorePoints extends BodyComponent with ScorePoints { class FakeScorePoints extends BodyComponent with ScorePoints {
@override @override
Body createBody() { Body createBody() {
@ -30,16 +21,12 @@ void main() {
late PinballGame game; late PinballGame game;
late GameBloc bloc; late GameBloc bloc;
late Ball ball; late Ball ball;
late ComponentSet componentSet;
late BallController ballController;
late FakeScorePoints fakeScorePoints; late FakeScorePoints fakeScorePoints;
setUp(() { setUp(() {
game = MockPinballGame(); game = MockPinballGame();
bloc = MockGameBloc(); bloc = MockGameBloc();
ball = MockBall(); ball = MockBall();
componentSet = MockComponentSet();
ballController = MockBallController();
fakeScorePoints = FakeScorePoints(); fakeScorePoints = FakeScorePoints();
}); });
@ -51,13 +38,9 @@ void main() {
test( test(
'emits Scored event with points', 'emits Scored event with points',
() { () {
when(() => componentSet.whereType<BallController>())
.thenReturn([ballController]);
when(() => ball.children).thenReturn(componentSet);
when<Forge2DGame>(() => ballController.gameRef).thenReturn(game);
when<GameBloc>(game.read).thenReturn(bloc); when<GameBloc>(game.read).thenReturn(bloc);
BallScorePointsCallback().begin( BallScorePointsCallback(game).begin(
ball, ball,
fakeScorePoints, fakeScorePoints,
FakeContact(), FakeContact(),
@ -71,19 +54,5 @@ void main() {
}, },
); );
}); });
group('end', () {
test("doesn't add events to GameBloc", () {
BallScorePointsCallback().end(
ball,
fakeScorePoints,
FakeContact(),
);
verifyNever(
() => bloc.add(any()),
);
});
});
}); });
} }

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

@ -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,14 +1,14 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'helpers.dart'; FlameTester<T> flameBlocTester<T extends Forge2DGame>({
required T Function() game,
FlameTester<PinballGame> flameBlocTester({
required GameBloc Function() gameBloc, required GameBloc Function() gameBloc,
}) { }) {
return FlameTester<PinballGame>( return FlameTester<T>(
PinballGameTest.create, game,
pumpWidget: (gameWidget, tester) async { pumpWidget: (gameWidget, tester) async {
await tester.pumpWidget( await tester.pumpWidget(
BlocProvider.value( BlocProvider.value(

@ -0,0 +1,7 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
class FakeContact extends Fake implements Contact {}
class FakeGameEvent extends Fake implements GameEvent {}

@ -6,7 +6,9 @@
// license that can be found in the LICENSE file or at // license that can be found in the LICENSE file or at
export 'builders.dart'; export 'builders.dart';
export 'extensions.dart'; export 'extensions.dart';
export 'fakes.dart';
export 'key_testers.dart'; export 'key_testers.dart';
export 'mocks.dart'; export 'mocks.dart';
export 'navigator.dart'; export 'navigator.dart';
export 'pump_app.dart'; export 'pump_app.dart';
export 'test_game.dart';

@ -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 {}
@ -72,6 +70,8 @@ class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {}
class MockSpaceshipHole extends Mock implements SpaceshipHole {} class MockSpaceshipHole extends Mock implements SpaceshipHole {}
class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {}
class MockComponentSet extends Mock implements ComponentSet {} class MockComponentSet extends Mock implements ComponentSet {}
class MockDashNestBumper extends Mock implements DashNestBumper {} class MockDashNestBumper extends Mock implements DashNestBumper {}

@ -0,0 +1,8 @@
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
class TestGame extends Forge2DGame with FlameBloc {
TestGame() {
images.prefix = '';
}
}

@ -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