Merge branch 'main' into refactor/priority-layer

pull/83/head
RuiAlonso 4 years ago
commit 4f321ee6f7

@ -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,87 +0,0 @@
import 'package:flame/components.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;
}
}

@ -12,22 +12,15 @@ class Board extends Component {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
// TODO(alestiago): adjust positioning once sprites are added. final bottomGroup = _BottomGroup();
final bottomGroup = _BottomGroup(
position: Vector2(
PinballGame.boardBounds.center.dx,
PinballGame.boardBounds.bottom + 10,
),
spacing: 2,
);
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
// 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([
@ -46,27 +39,15 @@ class Board extends Component {
// TODO(alestiago): Consider renaming once entire Board is defined. // TODO(alestiago): Consider renaming once entire Board is defined.
class _BottomGroup extends Component { class _BottomGroup extends Component {
/// {@macro bottom_group} /// {@macro bottom_group}
_BottomGroup({ _BottomGroup();
required this.position,
required this.spacing,
});
/// The amount of space between the line of symmetry.
final double spacing;
/// The position of this [_BottomGroup].
final Vector2 position;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
final spacing = this.spacing + Flipper.size.x / 2;
final rightSide = _BottomGroupSide( final rightSide = _BottomGroupSide(
side: BoardSide.right, side: BoardSide.right,
position: position + Vector2(spacing, 0),
); );
final leftSide = _BottomGroupSide( final leftSide = _BottomGroupSide(
side: BoardSide.left, side: BoardSide.left,
position: position + Vector2(-spacing, 0),
); );
await addAll([rightSide, leftSide]); await addAll([rightSide, leftSide]);
@ -82,37 +63,29 @@ class _BottomGroupSide extends Component {
/// {@macro bottom_group_side} /// {@macro bottom_group_side}
_BottomGroupSide({ _BottomGroupSide({
required BoardSide side, required BoardSide side,
required Vector2 position, }) : _side = side;
}) : _side = side,
_position = position;
final BoardSide _side; final BoardSide _side;
final Vector2 _position;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
final direction = _side.direction; final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? 0 : -6.5;
final flipper = Flipper( final flipper = ControlledFlipper(
side: _side, side: _side,
)..initialPosition = _position; )..initialPosition = Vector2((11.0 * direction) + centerXAdjustment, -42.4);
await flipper.add(FlipperController(flipper));
final baseboard = Baseboard(side: _side) final baseboard = Baseboard(side: _side)
..initialPosition = _position + ..initialPosition = Vector2(
Vector2( (25.58 * direction) + centerXAdjustment,
(Baseboard.size.x / 1.6 * direction), -28.69,
Baseboard.size.y - 2, );
);
final kicker = Kicker( final kicker = Kicker(
side: _side, side: _side,
)..initialPosition = _position + )..initialPosition = Vector2(
Vector2( (22.0 * direction) + centerXAdjustment,
(Flipper.size.x) * direction, -26,
Flipper.size.y + Kicker.size.y, );
);
await addAll([flipper, baseboard, kicker]); await addAll([flipper, baseboard, kicker]);
} }

@ -31,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;
} }
@ -154,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,8 +1,7 @@
export 'ball.dart';
export 'baseboard.dart';
export 'board.dart'; export 'board.dart';
export 'bonus_word.dart'; export 'bonus_word.dart';
export 'chrome_dino.dart'; export 'chrome_dino.dart';
export 'controlled_ball.dart';
export 'flipper_controller.dart'; export 'flipper_controller.dart';
export 'flutter_forest.dart'; export 'flutter_forest.dart';
export 'jetpack_ramp.dart'; export 'jetpack_ramp.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());
}
}

@ -1,16 +1,29 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball_components/pinball_components.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} /// {@template flipper_controller}
/// A [Component] that controls the [Flipper]s movement. /// A [ComponentController] that controls a [Flipper]s movement.
/// {@endtemplate} /// {@endtemplate}
class FlipperController extends Component with KeyboardHandler { class FlipperController extends ComponentController<Flipper>
with KeyboardHandler {
/// {@macro flipper_controller} /// {@macro flipper_controller}
FlipperController(this.flipper) : _keys = flipper.side.flipperKeys; FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys,
/// The [Flipper] this controller is controlling. super(flipper);
final Flipper flipper;
/// The [LogicalKeyboardKey]s that will control the [Flipper]. /// The [LogicalKeyboardKey]s that will control the [Flipper].
/// ///
@ -25,9 +38,9 @@ class FlipperController extends Component with KeyboardHandler {
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {
flipper.moveUp(); component.moveUp();
} else if (event is RawKeyUpEvent) { } else if (event is RawKeyUpEvent) {
flipper.moveDown(); component.moveDown();
} }
return false; return false;

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

@ -13,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([

@ -13,8 +13,8 @@ class Launcher extends Forge2DBlueprint {
@override @override
void build(_) { void build(_) {
final position = Vector2( final position = Vector2(
PinballGame.boardBounds.right - 31.3, BoardDimensions.bounds.right - 31.3,
PinballGame.boardBounds.bottom + 33, BoardDimensions.bounds.bottom + 33,
); );
addAllContactCallback([ addAllContactCallback([
@ -67,8 +67,8 @@ class LauncherRamp extends BodyComponent with InitialPosition, Layered {
final rightStraightShape = EdgeShape() final rightStraightShape = EdgeShape()
..set( ..set(
startPosition..rotate(PinballGame.boardPerspectiveAngle), startPosition..rotate(BoardDimensions.perspectiveAngle),
endPosition..rotate(PinballGame.boardPerspectiveAngle), endPosition..rotate(BoardDimensions.perspectiveAngle),
); );
final rightStraightFixtureDef = FixtureDef(rightStraightShape); final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef); fixturesDef.add(rightStraightFixtureDef);

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

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

@ -11,6 +11,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.flutterSignPost.keyName), images.load(components.Assets.images.flutterSignPost.keyName),
images.load(components.Assets.images.flipper.left.keyName), images.load(components.Assets.images.flipper.left.keyName),
images.load(components.Assets.images.flipper.right.keyName), images.load(components.Assets.images.flipper.right.keyName),
images.load(components.Assets.images.baseboard.left.keyName),
images.load(components.Assets.images.baseboard.right.keyName),
images.load(Assets.images.components.background.path), images.load(Assets.images.components.background.path),
]); ]);
} }

@ -1,6 +1,5 @@
// 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';
@ -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();
@ -68,7 +58,7 @@ class PinballGame extends Forge2DGame
} }
void _addContactCallbacks() { void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback()); addContactCallback(BallScorePointsCallback(this));
addContactCallback(BottomWallBallContactCallback()); addContactCallback(BottomWallBallContactCallback());
addContactCallback(BonusLetterBallContactCallback()); addContactCallback(BonusLetterBallContactCallback());
} }
@ -80,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);
} }
@ -88,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,
), ),
), ),
); );
@ -101,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);
} }
} }
@ -138,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,
),
); );
} }
} }

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: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

@ -13,6 +13,8 @@ class $AssetsImagesGen {
/// File path: assets/images/ball.png /// File path: assets/images/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
/// File path: assets/images/flutter_sign_post.png /// File path: assets/images/flutter_sign_post.png
@ -28,6 +30,18 @@ class $AssetsImagesGen {
const AssetGenImage('assets/images/spaceship_saucer.png'); const AssetGenImage('assets/images/spaceship_saucer.png');
} }
class $AssetsImagesBaseboardGen {
const $AssetsImagesBaseboardGen();
/// File path: assets/images/baseboard/left.png
AssetGenImage get left =>
const AssetGenImage('assets/images/baseboard/left.png');
/// File path: assets/images/baseboard/right.png
AssetGenImage get right =>
const AssetGenImage('assets/images/baseboard/right.png');
}
class $AssetsImagesFlipperGen { class $AssetsImagesFlipperGen {
const $AssetsImagesFlipperGen(); const $AssetsImagesFlipperGen();

@ -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,6 +89,8 @@ 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].
@ -95,4 +98,18 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
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);
}
} }

@ -1,5 +1,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -12,9 +13,6 @@ class Baseboard extends BodyComponent with InitialPosition {
required BoardSide side, required BoardSide side,
}) : _side = side; }) : _side = side;
/// The size of the [Baseboard].
static final size = Vector2(24.2, 13.5);
/// Whether the [Baseboard] is on the left or right side of the board. /// Whether the [Baseboard] is on the left or right side of the board.
final BoardSide _side; final BoardSide _side;
@ -24,50 +22,55 @@ class Baseboard extends BodyComponent with InitialPosition {
final arcsAngle = -1.11 * direction; final arcsAngle = -1.11 * direction;
const arcsRotation = math.pi / 2.08; const arcsRotation = math.pi / 2.08;
final pegBumperShape = CircleShape()..radius = 0.7;
pegBumperShape.position.setValues(11.11 * direction, 7.15);
final pegBumperFixtureDef = FixtureDef(pegBumperShape);
fixturesDef.add(pegBumperFixtureDef);
final topCircleShape = CircleShape()..radius = 0.7; final topCircleShape = CircleShape()..radius = 0.7;
topCircleShape.position.setValues(11.39 * direction, 6.05); topCircleShape.position.setValues(9.71 * direction, 4.95);
final topCircleFixtureDef = FixtureDef(topCircleShape); final topCircleFixtureDef = FixtureDef(topCircleShape);
fixturesDef.add(topCircleFixtureDef); fixturesDef.add(topCircleFixtureDef);
final innerEdgeShape = EdgeShape() final innerEdgeShape = EdgeShape()
..set( ..set(
Vector2(10.86 * direction, 6.45), Vector2(9.01 * direction, 5.35),
Vector2(6.96 * direction, 0.25), Vector2(5.29 * direction, -0.95),
); );
final innerEdgeShapeFixtureDef = FixtureDef(innerEdgeShape); final innerEdgeShapeFixtureDef = FixtureDef(innerEdgeShape);
fixturesDef.add(innerEdgeShapeFixtureDef); fixturesDef.add(innerEdgeShapeFixtureDef);
final outerEdgeShape = EdgeShape() final outerEdgeShape = EdgeShape()
..set( ..set(
Vector2(11.96 * direction, 5.85), Vector2(10.41 * direction, 4.75),
Vector2(5.48 * direction, -4.85), Vector2(3.79 * direction, -5.95),
); );
final outerEdgeShapeFixtureDef = FixtureDef(outerEdgeShape); final outerEdgeShapeFixtureDef = FixtureDef(outerEdgeShape);
fixturesDef.add(outerEdgeShapeFixtureDef); fixturesDef.add(outerEdgeShapeFixtureDef);
final upperArcShape = ArcShape( final upperArcShape = ArcShape(
center: Vector2(1.76 * direction, 3.25), center: Vector2(0.09 * direction, 2.15),
arcRadius: 6.1, arcRadius: 6.1,
angle: arcsAngle, angle: arcsAngle,
rotation: arcsRotation, rotation: arcsRotation,
); );
final upperArcFixtureDefs = FixtureDef(upperArcShape); final upperArcFixtureDef = FixtureDef(upperArcShape);
fixturesDef.add(upperArcFixtureDefs); fixturesDef.add(upperArcFixtureDef);
final lowerArcShape = ArcShape( final lowerArcShape = ArcShape(
center: Vector2(1.85 * direction, -2.15), center: Vector2(0.09 * direction, -3.35),
arcRadius: 4.5, arcRadius: 4.5,
angle: arcsAngle, angle: arcsAngle,
rotation: arcsRotation, rotation: arcsRotation,
); );
final lowerArcFixtureDefs = FixtureDef(lowerArcShape); final lowerArcFixtureDef = FixtureDef(lowerArcShape);
fixturesDef.add(lowerArcFixtureDefs); fixturesDef.add(lowerArcFixtureDef);
final bottomRectangle = PolygonShape() final bottomRectangle = PolygonShape()
..setAsBox( ..setAsBox(
7, 6.8,
2, 2,
Vector2(-5.14 * direction, -4.75), Vector2(-6.3 * direction, -5.85),
0, 0,
); );
final bottomRectangleFixtureDef = FixtureDef(bottomRectangle); final bottomRectangleFixtureDef = FixtureDef(bottomRectangle);
@ -76,11 +79,31 @@ class Baseboard extends BodyComponent with InitialPosition {
return fixturesDef; return fixturesDef;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
(_side.isLeft)
? Assets.images.baseboard.left.keyName
: Assets.images.baseboard.right.keyName,
);
await add(
SpriteComponent(
sprite: sprite,
size: Vector2(27.5, 17.9),
anchor: Anchor.center,
position: Vector2(_side.isLeft ? 0.4 : -0.4, 0),
),
);
renderBody = false;
}
@override @override
Body createBody() { Body createBody() {
// TODO(allisonryan0002): share sweeping angle with flipper when components const angle = 37.1 * (math.pi / 180);
// are grouped.
const angle = math.pi / 5;
final bodyDef = BodyDef() final bodyDef = BodyDef()
..position = initialPosition ..position = initialPosition

@ -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,6 @@
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart';
export 'board_dimensions.dart';
export 'board_side.dart'; export 'board_side.dart';
export 'fire_effect.dart'; export 'fire_effect.dart';
export 'flipper.dart'; export 'flipper.dart';

@ -68,7 +68,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
anchor: anchor, anchor: anchor,
); );
final joint = _FlipperJoint(jointDef); final joint = _FlipperJoint(jointDef);
world.createJoint2(joint); world.createJoint(joint);
unawaited(mounted.whenComplete(joint.unlock)); unawaited(mounted.whenComplete(joint.unlock));
} }
@ -219,15 +219,3 @@ class _FlipperJoint extends RevoluteJoint {
setLimits(-angle, angle); setLimits(-angle, angle);
} }
} }
// TODO(alestiago): Remove once Forge2D supports custom joints.
extension on World {
void createJoint2(Joint joint) {
assert(!isLocked, '');
joints.add(joint);
joint.bodyA.joints.add(joint);
joint.bodyB.joints.add(joint);
}
}

@ -26,6 +26,7 @@ flutter:
generate: true generate: true
assets: assets:
- assets/images/ - assets/images/
- assets/images/baseboard/
- assets/images/flipper/ - assets/images/flipper/
flutter_gen: flutter_gen:

@ -6,7 +6,6 @@
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
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/spaceship/spaceship.dart'; import 'package:sandbox/stories/spaceship/spaceship.dart';
import 'package:sandbox/stories/stories.dart'; import 'package:sandbox/stories/stories.dart';
@ -18,5 +17,6 @@ void main() {
addEffectsStories(dashbook); addEffectsStories(dashbook);
addFlipperStories(dashbook); addFlipperStories(dashbook);
addSpaceshipStories(dashbook); addSpaceshipStories(dashbook);
addBaseboardStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -0,0 +1,15 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/baseboard/basic.dart';
void addBaseboardStories(Dashbook dashbook) {
dashbook.storiesOf('Baseboard').add(
'Basic',
(context) => GameWidget(
game: BasicBaseboardGame(),
),
codeLink: buildSourceLink('baseboard/basic.dart'),
info: BasicBaseboardGame.info,
);
}

@ -0,0 +1,26 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BasicBaseboardGame extends BasicGame {
static const info = '''
Basic example of how a Baseboard works.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final leftBaseboard = Baseboard(side: BoardSide.left)
..initialPosition = center - Vector2(25, 0);
final rightBaseboard = Baseboard(side: BoardSide.right)
..initialPosition = center + Vector2(25, 0);
await addAll([
leftBaseboard,
rightBaseboard,
]);
}
}

@ -1,3 +1,5 @@
export 'ball/ball.dart'; export 'ball/ball.dart';
export 'baseboard/baseboard.dart';
export 'effects/effects.dart';
export 'flipper/flipper.dart'; export 'flipper/flipper.dart';
export 'layer/layer.dart'; export 'layer/layer.dart';

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

@ -3,13 +3,16 @@
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'; import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() { void main() {
group('Baseboard', () { group('Baseboard', () {
// TODO(allisonryan0002): Add golden tests.
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new); final flameTester = FlameTester(TestGame.new);
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
@ -62,14 +65,14 @@ void main() {
group('fixtures', () { group('fixtures', () {
flameTester.test( flameTester.test(
'has six', 'has seven',
(game) async { (game) async {
final baseboard = Baseboard( final baseboard = Baseboard(
side: BoardSide.left, side: BoardSide.left,
); );
await game.ensureAdd(baseboard); await game.ensureAdd(baseboard);
expect(baseboard.body.fixtures.length, equals(6)); expect(baseboard.body.fixtures.length, equals(7));
}, },
); );
}); });

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

@ -48,10 +48,11 @@ void main() {
await tester.pump(); await tester.pump();
}, },
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( // FIXME(erickzanardo): Failing pipeline.
find.byGame<Forge2DGame>(), // await expectLater(
matchesGoldenFile('golden/spaceship.png'), // find.byGame<Forge2DGame>(),
); // matchesGoldenFile('golden/spaceship.png'),
// );
}, },
); );
}); });

@ -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,87 +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.testGameWidget(
'adds BallLost to GameBloc',
setUp: (game, tester) async {
await game.ready();
},
verify: (game, tester) async {
game.children.whereType<Ball>().first.controller.lost();
await tester.pump();
verify(() => gameBloc.add(const BallLost())).called(1);
},
);
tester.testGameWidget(
'resets the ball if the game is not over',
setUp: (game, tester) async {
await game.ready();
game.children.whereType<Ball>().first.controller.lost();
await game.ready(); // Making sure that all additions are done
},
verify: (game, tester) async {
expect(
game.children.whereType<Ball>().length,
equals(1),
);
},
);
tester.testGameWidget(
'no ball is added on game over',
setUp: (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();
},
verify: (game, tester) async {
expect(
game.children.whereType<Ball>().length,
equals(0),
);
},
);
});
});
}

@ -195,7 +195,12 @@ void main() {
group('bonus letter activation', () { group('bonus letter activation', () {
late GameBloc gameBloc; 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(); gameBloc = MockGameBloc();
@ -211,10 +216,9 @@ void main() {
setUp: (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: (game, tester) async {
verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1); verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1);
@ -237,8 +241,10 @@ 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();
}, },
@ -258,15 +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 { verify: (game, tester) async {
final bonusLetter = game.descendants().whereType<BonusLetter>().first; // 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),

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

@ -59,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(
@ -71,12 +74,8 @@ void main() {
tester.testGameWidget( tester.testGameWidget(
'listens when a Bonus.dashNest is added', 'listens when a Bonus.dashNest is added',
setUp: (game, tester) async {
await game.ready();
},
verify: (game, tester) async { verify: (game, tester) async {
final flutterForest = final flutterForest = FlutterForest();
game.descendants().whereType<FlutterForest>().first;
const state = GameState( const state = GameState(
score: 0, score: 0,
@ -96,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(
@ -118,8 +121,9 @@ void main() {
final contactCallback = DashNestBumperBallContactCallback(); 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);
}, },
); );
}); });

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

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

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

@ -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 = '';
}
}
Loading…
Cancel
Save