fix: fixed merge conflicts with priorities

pull/126/head
RuiAlonso 4 years ago
commit 5ef4f0e6c7

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

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

@ -31,7 +31,7 @@ class ChromeDino extends BodyComponent with InitialPosition {
anchor: anchor,
);
final joint = _ChromeDinoJoint(jointDef);
world.createJoint2(joint);
world.createJoint(joint);
return joint;
}
@ -154,15 +154,3 @@ class _ChromeDinoJoint extends RevoluteJoint {
setMotorSpeed(-motorSpeed);
}
}
extension on World {
// TODO(alestiago): Remove once Forge2D supports custom joints.
void createJoint2(Joint joint) {
assert(!isLocked, '');
joints.add(joint);
joint.bodyA.joints.add(joint);
joint.bodyB.joints.add(joint);
}
}

@ -1,4 +1,3 @@
export 'baseboard.dart';
export 'board.dart';
export 'bonus_word.dart';
export 'chrome_dino.dart';

@ -1,4 +1,5 @@
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';
@ -28,19 +29,19 @@ class ControlledBall extends Ball with Controls<BallController> {
ControlledBall.bonus({
required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) {
controller = BallController(this);
controller = BonusBallController(this);
}
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = BallController(this);
controller = BonusBallController(this);
}
}
/// {@template ball_controller}
/// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate}
class BallController extends ComponentController<Ball> {
abstract class BallController extends ComponentController<Ball> {
/// {@macro ball_controller}
BallController(Ball ball) : super(ball);
@ -50,30 +51,52 @@ class BallController extends ComponentController<Ball> {
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
/// {@endtemplate}
@mustCallSuper
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> {
/// {@macro ball_controller}
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() {
super.lost();
final bloc = gameRef.read<GameBloc>()..add(const BallLost());
// TODO(alestiago): Consider the use of onNewState instead.
final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver;
if (shouldBallRespwan) gameRef.spawnBall();
gameRef.read<GameBloc>().add(const BallLost());
}
}

@ -21,6 +21,11 @@ class Jetpack extends Forge2DBlueprint {
@override
void build(_) {
final position = Vector2(
BoardDimensions.bounds.left + 40.5,
BoardDimensions.bounds.top - 31.5,
);
addAllContactCallback([
RampOpeningBallContactCallback<_JetpackRampOpening>(),
]);
@ -250,10 +255,10 @@ class _JetpackRampOpening extends RampOpening {
required double rotation,
}) : _rotation = rotation,
super(
pathwayLayer: Layer.jetpack,
insideLayer: Layer.jetpack,
outsideLayer: outsideLayer,
orientation: RampOrientation.down,
pathwayPriority: Jetpack.ballPriorityInsideRamp,
insidePriority: Jetpack.ballPriorityInsideRamp,
outsidePriority: outsidePriority,
);

@ -13,8 +13,8 @@ class Launcher extends Forge2DBlueprint {
@override
void build(_) {
final position = Vector2(
PinballGame.boardBounds.right - 31.3,
PinballGame.boardBounds.bottom + 33,
BoardDimensions.bounds.right - 31.3,
BoardDimensions.bounds.bottom + 33,
);
addAllContactCallback([
@ -67,8 +67,8 @@ class LauncherRamp extends BodyComponent with InitialPosition, Layered {
final rightStraightShape = EdgeShape()
..set(
startPosition..rotate(PinballGame.boardPerspectiveAngle),
endPosition..rotate(PinballGame.boardPerspectiveAngle),
startPosition..rotate(BoardDimensions.perspectiveAngle),
endPosition..rotate(BoardDimensions.perspectiveAngle),
);
final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef);
@ -122,7 +122,7 @@ class _LauncherRampOpening extends RampOpening {
required double rotation,
}) : _rotation = rotation,
super(
pathwayLayer: Layer.launcher,
insideLayer: Layer.launcher,
orientation: RampOrientation.down,
pathwayPriority: 3,
);

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

@ -169,9 +169,9 @@ class SpaceshipExitRailEnd extends RampOpening {
/// {@macro spaceship_exit_rail_end}
SpaceshipExitRailEnd()
: super(
pathwayLayer: Layer.spaceshipExitRail,
insideLayer: Layer.spaceshipExitRail,
orientation: RampOrientation.down,
pathwayPriority: 3,
insidePriority: 3,
) {
layer = Layer.spaceshipExitRail;
}

@ -2,9 +2,8 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/components/components.dart';
import 'package:pinball/game/pinball_game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template wall}
/// A continuous generic and [BodyType.static] barrier that divides a game area.
@ -42,13 +41,12 @@ class Wall extends BodyComponent {
/// Create top, left, and right [Wall]s for the game board.
List<Wall> createBoundaries(Forge2DGame game) {
final topLeft =
PinballGame.boardBounds.topLeft.toVector2() + Vector2(18.6, 0);
final bottomRight = PinballGame.boardBounds.bottomRight.toVector2();
final topLeft = BoardDimensions.bounds.topLeft.toVector2() + Vector2(18.6, 0);
final bottomRight = BoardDimensions.bounds.bottomRight.toVector2();
final topRight =
PinballGame.boardBounds.topRight.toVector2() - Vector2(18.6, 0);
final bottomLeft = PinballGame.boardBounds.bottomLeft.toVector2();
BoardDimensions.bounds.topRight.toVector2() - Vector2(18.6, 0);
final bottomLeft = BoardDimensions.bounds.bottomLeft.toVector2();
return [
Wall(start: topLeft, end: topRight),
@ -67,8 +65,8 @@ class BottomWall extends Wall {
/// {@macro bottom_wall}
BottomWall()
: super(
start: PinballGame.boardBounds.bottomLeft.toVector2(),
end: PinballGame.boardBounds.bottomRight.toVector2(),
start: BoardDimensions.bounds.bottomLeft.toVector2(),
end: BoardDimensions.bounds.bottomRight.toVector2(),
);
}

@ -11,6 +11,10 @@ extension PinballGameAssetsX on PinballGame {
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(components.Assets.images.baseboard.left.keyName),
images.load(components.Assets.images.baseboard.right.keyName),
images.load(components.Assets.images.dino.dinoLandTop.keyName),
images.load(components.Assets.images.dino.dinoLandBottom.keyName),
images.load(Assets.images.components.background.path),
images.load(Assets.images.components.spaceshipRamp.path),
images.load(Assets.images.components.spaceshipRailingBg.path),

@ -1,6 +1,5 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
@ -22,15 +21,6 @@ class PinballGame extends Forge2DGame
late final Plunger plunger;
static final boardSize = Vector2(101.6, 143.8);
static final boardBounds = Rect.fromCenter(
center: Offset.zero,
width: boardSize.x,
height: -boardSize.y,
);
static final boardPerspectiveAngle =
-math.atan(18.6 / PinballGame.boardBounds.height);
@override
void onAttach() {
super.onAttach();
@ -76,11 +66,19 @@ class PinballGame extends Forge2DGame
Future<void> _addGameBoundaries() async {
await add(BottomWall());
createBoundaries(this).forEach(add);
unawaited(
addFromBlueprint(
DinoWalls(
position: Vector2(-2.4, 0),
),
),
);
}
Future<void> _addPlunger() async {
plunger = Plunger(compressionDistance: 29)
..initialPosition = boardBounds.center.toVector2() + Vector2(41.5, -49);
..initialPosition =
BoardDimensions.bounds.center.toVector2() + Vector2(41.5, -49);
await add(plunger);
}
@ -88,8 +86,8 @@ class PinballGame extends Forge2DGame
await add(
BonusWord(
position: Vector2(
boardBounds.center.dx - 3.07,
boardBounds.center.dy - 2.4,
BoardDimensions.bounds.center.dx - 3.07,
BoardDimensions.bounds.center.dy - 2.4,
),
),
);

@ -82,7 +82,7 @@ List<Vector2> calculateEllipse({
/// For more information read: https://en.wikipedia.org/wiki/B%C3%A9zier_curve
List<Vector2> calculateBezierCurve({
required List<Vector2> controlPoints,
double step = 0.001,
double step = 0.01,
}) {
assert(
0 <= step && step <= 1,

@ -98,14 +98,14 @@ void main() {
);
});
test('returns by default 1000 points as indicated by step', () {
test('returns by default 100 points as indicated by step', () {
final points = calculateBezierCurve(
controlPoints: [
Vector2(0, 0),
Vector2(10, 10),
],
);
expect(points.length, 1000);
expect(points.length, 100);
});
test('returns as many points as indicated by step', () {
@ -114,9 +114,9 @@ void main() {
Vector2(0, 0),
Vector2(10, 10),
],
step: 0.01,
step: 0.001,
);
expect(points.length, 100);
expect(points.length, 1000);
});
});

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -13,6 +13,8 @@ class $AssetsImagesGen {
/// File path: assets/images/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
/// File path: assets/images/flutter_sign_post.png
@ -28,6 +30,30 @@ class $AssetsImagesGen {
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 $AssetsImagesDinoGen {
const $AssetsImagesDinoGen();
/// File path: assets/images/dino/dino-land-bottom.png
AssetGenImage get dinoLandBottom =>
const AssetGenImage('assets/images/dino/dino-land-bottom.png');
/// File path: assets/images/dino/dino-land-top.png
AssetGenImage get dinoLandTop =>
const AssetGenImage('assets/images/dino/dino-land-top.png');
}
class $AssetsImagesFlipperGen {
const $AssetsImagesFlipperGen();

@ -23,13 +23,14 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
}
/// The size of the [Ball]
static final Vector2 size = Vector2.all(3);
static final Vector2 size = Vector2.all(4.5);
/// The base [Color] used to tint this [Ball]
final Color baseColor;
double _boostTimer = 0;
static const _boostDuration = 2.0;
late SpriteComponent _spriteComponent;
@override
Future<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 tint = baseColor.withOpacity(0.5);
await add(
SpriteComponent(
_spriteComponent = SpriteComponent(
sprite: sprite,
size: size,
size: size * 1.15,
anchor: Anchor.center,
)..tint(tint),
);
@ -88,6 +89,8 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
unawaited(gameRef.add(effect));
}
_rescale();
}
/// Applies a boost on this [Ball].
@ -95,4 +98,18 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
body.applyLinearImpulse(impulse);
_boostTimer = _boostDuration;
}
void _rescale() {
final boardHeight = BoardDimensions.size.y;
const maxShrinkAmount = BoardDimensions.perspectiveShrinkFactor;
final adjustedYPosition = body.position.y + (boardHeight / 2);
final scaleFactor = ((boardHeight - adjustedYPosition) /
BoardDimensions.shrinkAdjustedHeight) +
maxShrinkAmount;
body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor;
_spriteComponent.scale = Vector2.all(scaleFactor);
}
}

@ -1,5 +1,6 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
@ -12,9 +13,6 @@ class Baseboard extends BodyComponent with InitialPosition {
required BoardSide 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.
final BoardSide _side;
@ -24,50 +22,55 @@ class Baseboard extends BodyComponent with InitialPosition {
final arcsAngle = -1.11 * direction;
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;
topCircleShape.position.setValues(11.39 * direction, 6.05);
topCircleShape.position.setValues(9.71 * direction, 4.95);
final topCircleFixtureDef = FixtureDef(topCircleShape);
fixturesDef.add(topCircleFixtureDef);
final innerEdgeShape = EdgeShape()
..set(
Vector2(10.86 * direction, 6.45),
Vector2(6.96 * direction, 0.25),
Vector2(9.01 * direction, 5.35),
Vector2(5.29 * direction, -0.95),
);
final innerEdgeShapeFixtureDef = FixtureDef(innerEdgeShape);
fixturesDef.add(innerEdgeShapeFixtureDef);
final outerEdgeShape = EdgeShape()
..set(
Vector2(11.96 * direction, 5.85),
Vector2(5.48 * direction, -4.85),
Vector2(10.41 * direction, 4.75),
Vector2(3.79 * direction, -5.95),
);
final outerEdgeShapeFixtureDef = FixtureDef(outerEdgeShape);
fixturesDef.add(outerEdgeShapeFixtureDef);
final upperArcShape = ArcShape(
center: Vector2(1.76 * direction, 3.25),
center: Vector2(0.09 * direction, 2.15),
arcRadius: 6.1,
angle: arcsAngle,
rotation: arcsRotation,
);
final upperArcFixtureDefs = FixtureDef(upperArcShape);
fixturesDef.add(upperArcFixtureDefs);
final upperArcFixtureDef = FixtureDef(upperArcShape);
fixturesDef.add(upperArcFixtureDef);
final lowerArcShape = ArcShape(
center: Vector2(1.85 * direction, -2.15),
center: Vector2(0.09 * direction, -3.35),
arcRadius: 4.5,
angle: arcsAngle,
rotation: arcsRotation,
);
final lowerArcFixtureDefs = FixtureDef(lowerArcShape);
fixturesDef.add(lowerArcFixtureDefs);
final lowerArcFixtureDef = FixtureDef(lowerArcShape);
fixturesDef.add(lowerArcFixtureDef);
final bottomRectangle = PolygonShape()
..setAsBox(
7,
6.8,
2,
Vector2(-5.14 * direction, -4.75),
Vector2(-6.3 * direction, -5.85),
0,
);
final bottomRectangleFixtureDef = FixtureDef(bottomRectangle);
@ -76,11 +79,31 @@ class Baseboard extends BodyComponent with InitialPosition {
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
Body createBody() {
// TODO(allisonryan0002): share sweeping angle with flipper when components
// are grouped.
const angle = math.pi / 5;
const angle = 37.1 * (math.pi / 180);
final bodyDef = BodyDef()
..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,5 +1,8 @@
export 'ball.dart';
export 'baseboard.dart';
export 'board_dimensions.dart';
export 'board_side.dart';
export 'dino_walls.dart';
export 'fire_effect.dart';
export 'flipper.dart';
export 'flutter_sign_post.dart';

@ -0,0 +1,225 @@
// ignore_for_file: comment_references, avoid_renaming_method_parameters
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template dinowalls}
/// A [Blueprint] which creates walls for the [ChromeDino].
/// {@endtemplate}
class DinoWalls extends Forge2DBlueprint {
/// {@macro dinowalls}
DinoWalls({required this.position});
/// The [position] where the elements will be created
final Vector2 position;
@override
void build(_) {
addAll([
_DinoTopWall()..initialPosition = position,
_DinoBottomWall()..initialPosition = position,
]);
}
}
/// {@template dino_top_wall}
/// Wall segment located above [ChromeDino].
/// {@endtemplate}
class _DinoTopWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall}
_DinoTopWall() : super(priority: 2);
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final topStraightShape = EdgeShape()
..set(
Vector2(29.5, 35.1),
Vector2(28.4, 35.1),
);
final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef);
final topCurveShape = BezierCurveShape(
controlPoints: [
topStraightShape.vertex1,
Vector2(17.4, 26.38),
Vector2(25.5, 20.7),
],
);
fixturesDef.add(FixtureDef(topCurveShape));
final middleCurveShape = BezierCurveShape(
controlPoints: [
topCurveShape.vertices.last,
Vector2(27.8, 20.1),
Vector2(26.8, 19.5),
],
);
fixturesDef.add(FixtureDef(middleCurveShape));
final bottomCurveShape = BezierCurveShape(
controlPoints: [
middleCurveShape.vertices.last,
Vector2(21.15, 16),
Vector2(25.6, 15.2),
],
);
fixturesDef.add(FixtureDef(bottomCurveShape));
final bottomStraightShape = EdgeShape()
..set(
bottomCurveShape.vertices.last,
Vector2(31, 14.5),
);
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
fixturesDef.add(bottomStraightFixtureDef);
return fixturesDef;
}
@override
Body createBody() {
renderBody = false;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(
(fixture) => body.createFixture(
fixture
..restitution = 0.1
..friction = 0,
),
);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
}
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.dino.dinoLandTop.keyName,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: Vector2(10.6, 27.7),
anchor: Anchor.center,
position: Vector2(27, -28.2),
);
await add(spriteComponent);
}
}
/// {@template dino_bottom_wall}
/// Wall segment located below [ChromeDino].
/// {@endtemplate}
class _DinoBottomWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall}
_DinoBottomWall() : super(priority: 2);
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final topStraightControlPoints = [
Vector2(32.4, 8.3),
Vector2(25, 7.7),
];
final topStraightShape = EdgeShape()
..set(
topStraightControlPoints.first,
topStraightControlPoints.last,
);
final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef);
final topLeftCurveControlPoints = [
topStraightControlPoints.last,
Vector2(21.8, 7),
Vector2(29.5, -13.8),
];
final topLeftCurveShape = BezierCurveShape(
controlPoints: topLeftCurveControlPoints,
);
fixturesDef.add(FixtureDef(topLeftCurveShape));
final bottomLeftStraightControlPoints = [
topLeftCurveControlPoints.last,
Vector2(31.8, -44.1),
];
final bottomLeftStraightShape = EdgeShape()
..set(
bottomLeftStraightControlPoints.first,
bottomLeftStraightControlPoints.last,
);
final bottomLeftStraightFixtureDef = FixtureDef(bottomLeftStraightShape);
fixturesDef.add(bottomLeftStraightFixtureDef);
final bottomStraightControlPoints = [
bottomLeftStraightControlPoints.last,
Vector2(37.8, -44.1),
];
final bottomStraightShape = EdgeShape()
..set(
bottomStraightControlPoints.first,
bottomStraightControlPoints.last,
);
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
fixturesDef.add(bottomStraightFixtureDef);
return fixturesDef;
}
@override
Body createBody() {
renderBody = false;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(
(fixture) => body.createFixture(
fixture
..restitution = 0.1
..friction = 0,
),
);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
}
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.dino.dinoLandBottom.keyName,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: Vector2(15.6, 54.8),
anchor: Anchor.center,
)..position = Vector2(31.7, 18);
await add(spriteComponent);
}
}

@ -68,7 +68,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
anchor: anchor,
);
final joint = _FlipperJoint(jointDef);
world.createJoint2(joint);
world.createJoint(joint);
unawaited(mounted.whenComplete(joint.unlock));
}
@ -219,15 +219,3 @@ class _FlipperJoint extends RevoluteJoint {
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);
}
}

@ -20,38 +20,39 @@ enum RampOrientation {
/// [RampOpeningBallContactCallback] detects when a [Ball] passes
/// through this opening.
///
/// By default the base [layer] is set to [Layer.board].
/// By default the base [layer] is set to [Layer.board] and the
/// [outsidePriority] is set to the lowest possible [Layer].
/// {@endtemplate}
// TODO(ruialonso): Consider renaming the class.
abstract class RampOpening extends BodyComponent with InitialPosition, Layered {
/// {@macro ramp_opening}
RampOpening({
required Layer pathwayLayer,
required int pathwayPriority,
required Layer insideLayer,
Layer? outsideLayer,
int? insidePriority,
int? outsidePriority,
required this.orientation,
}) : _pathwayLayer = pathwayLayer,
}) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board,
_pathwayPriority = pathwayPriority,
_outsidePriority = outsidePriority ?? 1 {
layer = Layer.board;
_insidePriority = insidePriority ?? 0,
_outsidePriority = outsidePriority ?? 0 {
layer = Layer.opening;
}
final int _pathwayPriority;
final int _outsidePriority;
final Layer _pathwayLayer;
final Layer _insideLayer;
final Layer _outsideLayer;
final int _insidePriority;
final int _outsidePriority;
/// Mask of category bits for collision inside pathway.
Layer get pathwayLayer => _pathwayLayer;
/// Mask of category bits for collision inside ramp.
Layer get insideLayer => _insideLayer;
/// Mask of category bits for collision outside pathway.
/// Mask of category bits for collision outside ramp.
Layer get outsideLayer => _outsideLayer;
/// Mask of category bits for collision outside pathway.
int get pathwayPriority => _pathwayPriority;
/// Priority for the [Ball] inside ramp.
int get insidePriority => _insidePriority;
/// Mask of category bits for collision outside pathway.
/// Priority for the [Ball] outside ramp.
int get outsidePriority => _outsidePriority;
/// The [Shape] of the [RampOpening].
@ -76,8 +77,7 @@ abstract class RampOpening extends BodyComponent with InitialPosition, Layered {
}
/// {@template ramp_opening_ball_contact_callback}
/// Detects when a [Ball] enters or exits a pathway ramp through a
/// [RampOpening].
/// Detects when a [Ball] enters or exits a ramp through a [RampOpening].
///
/// Modifies [Ball]'s [Layer] accordingly depending on whether the [Ball] is
/// outside or inside a ramp.
@ -92,11 +92,11 @@ class RampOpeningBallContactCallback<Opening extends RampOpening>
Layer layer;
if (!_ballsInside.contains(ball)) {
layer = opening.pathwayLayer;
layer = opening.insideLayer;
_ballsInside.add(ball);
ball
..layer = layer
..priority = opening.pathwayPriority;
..sendTo(opening.insidePriority)
..layer = layer;
} else {
_ballsInside.remove(ball);
}
@ -118,8 +118,8 @@ class RampOpeningBallContactCallback<Opening extends RampOpening>
if (isBallOutsideOpening) {
ball
..layer = opening.outsideLayer
..priority = opening.outsidePriority;
..sendTo(opening.outsidePriority)
..layer = opening.outsideLayer;
_ballsInside.remove(ball);
}
}

@ -21,6 +21,9 @@ class Spaceship extends Forge2DBlueprint {
/// The [position] where the elements will be created
final Vector2 position;
/// Base priority for wall while be on spaceship.
static const ballPriorityWhenOnSpaceship = 4;
@override
void build(_) {
addAllContactCallback([
@ -33,8 +36,8 @@ class Spaceship extends Forge2DBlueprint {
SpaceshipEntrance()..initialPosition = position,
AndroidHead()..initialPosition = position,
SpaceshipHole(
onExitLayer: Layer.spaceshipExitRail,
onExitElevation: 2,
outsideLayer: Layer.spaceshipExitRail,
outsidePriority: 2,
)..initialPosition = position - Vector2(5.2, 4.8),
SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8),
SpaceshipWall()..initialPosition = position,
@ -47,8 +50,8 @@ class Spaceship extends Forge2DBlueprint {
/// {@endtemplate}
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_saucer}
// TODO(ruimiguel): apply Elevated when PR merged.
SpaceshipSaucer() : super(priority: 3) {
SpaceshipSaucer()
: super(priority: Spaceship.ballPriorityWhenOnSpaceship - 1) {
layer = Layer.spaceship;
}
@ -92,7 +95,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@endtemplate}
class AndroidHead extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_bridge}
AndroidHead() : super(priority: 4) {
AndroidHead() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) {
layer = Layer.spaceship;
}
@ -147,9 +150,9 @@ class SpaceshipEntrance extends RampOpening {
/// {@macro spaceship_entrance}
SpaceshipEntrance()
: super(
pathwayLayer: Layer.spaceship,
insideLayer: Layer.spaceship,
orientation: RampOrientation.up,
pathwayPriority: 4,
insidePriority: Spaceship.ballPriorityWhenOnSpaceship,
) {
layer = Layer.spaceship;
}
@ -178,21 +181,18 @@ class SpaceshipEntrance extends RampOpening {
/// {@endtemplate}
class SpaceshipHole extends RampOpening {
/// {@macro spaceship_hole}
SpaceshipHole({Layer? onExitLayer, this.onExitElevation = 1})
SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1})
: super(
pathwayLayer: Layer.spaceship,
outsideLayer: onExitLayer,
insideLayer: Layer.spaceship,
outsideLayer: outsideLayer,
orientation: RampOrientation.up,
pathwayPriority: 4,
outsidePriority: onExitElevation,
insidePriority: 4,
outsidePriority: outsidePriority,
) {
renderBody = false;
layer = Layer.spaceship;
}
/// Priority order for [SpaceshipHole] on exit.
// TODO(ruimiguel): apply Elevated when PR merged.
final int onExitElevation;
@override
Shape get shape {
return ArcShape(
@ -234,8 +234,7 @@ class _SpaceshipWallShape extends ChainShape {
/// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_wall}
// TODO(ruimiguel): apply Elevated when PR merged
SpaceshipWall() : super(priority: 4) {
SpaceshipWall() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) {
layer = Layer.spaceship;
}
@ -268,8 +267,7 @@ class SpaceshipEntranceBallContactCallback
@override
void begin(SpaceshipEntrance entrance, Ball ball, _) {
ball
..priority = entrance.pathwayPriority
..gameRef.reorderChildren()
..sendTo(entrance.insidePriority)
..layer = Layer.spaceship;
}
}
@ -277,16 +275,14 @@ class SpaceshipEntranceBallContactCallback
/// [ContactCallback] that handles the contact between the [Ball]
/// and a [SpaceshipHole].
///
/// It sets the [Ball] priority and filter data so it will "be back" on the
/// board.
/// It sets the [Ball] priority and filter data so it will outside of the
/// [Spaceship].
class SpaceshipHoleBallContactCallback
extends ContactCallback<SpaceshipHole, Ball> {
@override
void begin(SpaceshipHole hole, Ball ball, _) {
ball
// TODO(ruimiguel): apply Elevated when PR merged.
..priority = hole.outsidePriority
..gameRef.reorderChildren()
..sendTo(hole.outsidePriority)
..layer = hole.outsideLayer;
}
}

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

@ -0,0 +1,39 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
/// Helper methods to change the [priority] of a [Component].
extension ComponentPriorityX on Component {
static const _lowestPriority = 0;
/// Changes the priority to a specific one.
void sendTo(int destinationPriority) {
if (priority != destinationPriority) {
priority = math.max(destinationPriority, _lowestPriority);
reorderChildren();
}
}
/// Changes the priority to the lowest possible.
void sendToBack() {
if (priority != _lowestPriority) {
priority = _lowestPriority;
reorderChildren();
}
}
/// Decreases the priority to be lower than another [Component].
void showBehindOf(Component other) {
if (priority >= other.priority) {
priority = math.max(other.priority - 1, _lowestPriority);
reorderChildren();
}
}
/// Increases the priority to be higher than another [Component].
void showInFrontOf(Component other) {
if (priority <= other.priority) {
priority = other.priority + 1;
reorderChildren();
}
}
}

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

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

@ -1,5 +1,6 @@
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
@ -24,3 +25,5 @@ class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock
implements ContactCallback<Object, Object> {}
class MockComponent extends Mock implements Component {}

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

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

@ -0,0 +1,28 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('DinoWalls', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
flameTester.test(
'loads correctly',
(game) async {
final dinoWalls = DinoWalls(position: Vector2.zero());
await game.addFromBlueprint(dinoWalls);
await game.ready();
for (final wall in dinoWalls.components) {
expect(game.contains(wall), isTrue);
}
},
);
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@ -12,7 +12,7 @@ class TestRampOpening extends RampOpening {
required RampOrientation orientation,
required Layer pathwayLayer,
}) : super(
pathwayLayer: pathwayLayer,
insideLayer: pathwayLayer,
orientation: orientation,
);
@ -129,14 +129,12 @@ void main() {
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero());
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
verify(() => ball.layer = area.insideLayer).called(1);
});
flameTester.test(
@ -152,14 +150,12 @@ void main() {
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero());
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
verify(() => ball.layer = area.insideLayer).called(1);
});
flameTester.test(
@ -174,15 +170,13 @@ void main() {
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero());
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
verify(() => ball.layer = area.insideLayer).called(1);
callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board);
@ -200,15 +194,13 @@ void main() {
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero());
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
verify(() => ball.layer = area.insideLayer).called(1);
callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board);
@ -226,21 +218,19 @@ void main() {
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero());
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
verify(() => ball.layer = area.insideLayer).called(1);
callback.end(ball, area, MockContact());
verifyNever(() => ball.layer = Layer.board);
callback.begin(ball, area, MockContact());
verifyNever(() => ball.layer = area.pathwayLayer);
verifyNever(() => ball.layer = area.insideLayer);
callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board);

@ -59,7 +59,8 @@ void main() {
group('SpaceshipEntranceBallContactCallback', () {
test('changes the ball priority on contact', () {
when(() => entrance.onEnterElevation).thenReturn(3);
when(() => ball.priority).thenReturn(2);
when(() => entrance.insidePriority).thenReturn(3);
SpaceshipEntranceBallContactCallback().begin(
entrance,
@ -67,39 +68,15 @@ void main() {
MockContact(),
);
verify(() => ball.priority = entrance.onEnterElevation).called(1);
});
test('re order the game children', () {
when(() => entrance.onEnterElevation).thenReturn(3);
SpaceshipEntranceBallContactCallback().begin(
entrance,
ball,
MockContact(),
);
verify(game.reorderChildren).called(1);
verify(() => ball.sendTo(entrance.insidePriority)).called(1);
});
});
group('SpaceshipHoleBallContactCallback', () {
test('changes the ball priority on contact', () {
when(() => ball.priority).thenReturn(2);
when(() => hole.outsideLayer).thenReturn(Layer.board);
when(() => hole.onExitElevation).thenReturn(1);
SpaceshipHoleBallContactCallback().begin(
hole,
ball,
MockContact(),
);
verify(() => ball.priority = hole.onExitElevation).called(1);
});
test('re order the game children', () {
when(() => hole.outsideLayer).thenReturn(Layer.board);
when(() => hole.onExitElevation).thenReturn(1);
when(() => hole.outsidePriority).thenReturn(1);
SpaceshipHoleBallContactCallback().begin(
hole,
@ -107,7 +84,7 @@ void main() {
MockContact(),
);
verify(game.reorderChildren).called(1);
verify(() => ball.sendTo(hole.outsidePriority)).called(1);
});
});
});

@ -0,0 +1,221 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/src/flame/priority.dart';
import '../../helpers/helpers.dart';
class TestBodyComponent extends BodyComponent {
@override
Body createBody() {
final fixtureDef = FixtureDef(CircleShape());
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}
void main() {
final flameTester = FlameTester(Forge2DGame.new);
group('ComponentPriorityX', () {
group('sendTo', () {
flameTester.test(
'changes the priority correctly to other level',
(game) async {
const newPriority = 5;
final component = TestBodyComponent()..priority = 4;
component.sendTo(newPriority);
expect(component.priority, equals(newPriority));
},
);
flameTester.test(
'calls reorderChildren if the new priority is different',
(game) async {
const newPriority = 5;
final component = MockComponent();
when(() => component.priority).thenReturn(4);
component.sendTo(newPriority);
verify(component.reorderChildren).called(1);
},
);
flameTester.test(
"doesn't call reorderChildren if the priority is the same",
(game) async {
const newPriority = 5;
final component = MockComponent();
when(() => component.priority).thenReturn(newPriority);
component.sendTo(newPriority);
verifyNever(component.reorderChildren);
},
);
});
group('sendToBack', () {
flameTester.test(
'changes the priority correctly to board level',
(game) async {
final component = TestBodyComponent()..priority = 4;
component.sendToBack();
expect(component.priority, equals(0));
},
);
flameTester.test(
'calls reorderChildren if the priority is greater than lowest level',
(game) async {
final component = MockComponent();
when(() => component.priority).thenReturn(4);
component.sendToBack();
verify(component.reorderChildren).called(1);
},
);
flameTester.test(
"doesn't call reorderChildren if the priority is the lowest level",
(game) async {
final component = MockComponent();
when(() => component.priority).thenReturn(0);
component.sendToBack();
verifyNever(component.reorderChildren);
},
);
});
group('showBehindOf', () {
flameTester.test(
'changes the priority if it is greater than other component',
(game) async {
const startPriority = 2;
final component = TestBodyComponent()..priority = startPriority;
final otherComponent = TestBodyComponent()
..priority = startPriority - 1;
component.showBehindOf(otherComponent);
expect(component.priority, equals(otherComponent.priority - 1));
},
);
flameTester.test(
"doesn't change the priority if it is lower than other component",
(game) async {
const startPriority = 2;
final component = TestBodyComponent()..priority = startPriority;
final otherComponent = TestBodyComponent()
..priority = startPriority + 1;
component.showBehindOf(otherComponent);
expect(component.priority, equals(startPriority));
},
);
flameTester.test(
'calls reorderChildren if the priority is greater than other component',
(game) async {
const startPriority = 2;
final component = MockComponent();
final otherComponent = MockComponent();
when(() => component.priority).thenReturn(startPriority);
when(() => otherComponent.priority).thenReturn(startPriority - 1);
component.showBehindOf(otherComponent);
verify(component.reorderChildren).called(1);
},
);
flameTester.test(
"doesn't call reorderChildren if the priority is lower than other "
'component',
(game) async {
const startPriority = 2;
final component = MockComponent();
final otherComponent = MockComponent();
when(() => component.priority).thenReturn(startPriority);
when(() => otherComponent.priority).thenReturn(startPriority + 1);
component.showBehindOf(otherComponent);
verifyNever(component.reorderChildren);
},
);
});
group('showInFrontOf', () {
flameTester.test(
'changes the priority if it is lower than other component',
(game) async {
const startPriority = 2;
final component = TestBodyComponent()..priority = startPriority;
final otherComponent = TestBodyComponent()
..priority = startPriority + 1;
component.showInFrontOf(otherComponent);
expect(component.priority, equals(otherComponent.priority + 1));
},
);
flameTester.test(
"doesn't change the priority if it is greater than other component",
(game) async {
const startPriority = 2;
final component = TestBodyComponent()..priority = startPriority;
final otherComponent = TestBodyComponent()
..priority = startPriority - 1;
component.showInFrontOf(otherComponent);
expect(component.priority, equals(startPriority));
},
);
flameTester.test(
'calls reorderChildren if the priority is lower than other component',
(game) async {
const startPriority = 2;
final component = MockComponent();
final otherComponent = MockComponent();
when(() => component.priority).thenReturn(startPriority);
when(() => otherComponent.priority).thenReturn(startPriority + 1);
component.showInFrontOf(otherComponent);
verify(component.reorderChildren).called(1);
},
);
flameTester.test(
"doesn't call reorderChildren if the priority is greater than other "
'component',
(game) async {
const startPriority = 2;
final component = MockComponent();
final otherComponent = MockComponent();
when(() => component.priority).thenReturn(startPriority);
when(() => otherComponent.priority).thenReturn(startPriority - 1);
component.showInFrontOf(otherComponent);
verifyNever(component.reorderChildren);
},
);
});
});
}

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

@ -196,7 +196,7 @@ void main() {
group('bonus letter activation', () {
late GameBloc gameBloc;
final tester = flameBlocTester(
final tester = flameBlocTester<PinballGame>(
// TODO(alestiago): Use TestGame once BonusLetter has controller.
game: PinballGameTest.create,
gameBloc: () => gameBloc,
@ -217,13 +217,8 @@ void main() {
await game.ready();
final bonusLetter = game.descendants().whereType<BonusLetter>().first;
await game.add(bonusLetter);
await game.ready();
bonusLetter.activate();
await game.ready();
await tester.pump();
},
verify: (game, tester) async {
verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1);

@ -15,20 +15,26 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('BallController', () {
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 = BallController(ball);
await ball.add(controller);
await game.ready();
final controller = BonusBallController(ball);
await ball.ensureAdd(controller);
controller.lost();
await game.ready();
@ -39,13 +45,20 @@ void main() {
});
group('LaunchedBallController', () {
group('lost', () {
late GameBloc gameBloc;
test('can be instantiated', () {
expect(
LaunchedBallController(MockBall()),
isA<LaunchedBallController>(),
);
});
group('description', () {
late Ball ball;
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
ball = Ball(baseColor: const Color(0xFF00FFFF));
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
@ -59,81 +72,126 @@ void main() {
);
tester.testGameWidget(
'removes ball',
verify: (game, tester) async {
await game.add(ball);
'lost adds BallLost to GameBloc',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ready();
await game.ensureAdd(ball);
controller.lost();
await game.ready();
expect(game.contains(ball), isFalse);
},
verify: (game, tester) async {
verify(() => gameBloc.add(const BallLost())).called(1);
},
);
group('listenWhen', () {
tester.testGameWidget(
'adds BallLost to GameBloc',
verify: (game, tester) async {
'listens when a ball has been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.add(ball);
await game.ready();
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
controller.lost();
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(2);
verify(() => gameBloc.add(const BallLost())).called(1);
expect(controller.listenWhen(previousState, newState), isTrue);
},
);
tester.testGameWidget(
'adds a new ball if the game is not over',
verify: (game, tester) async {
'does not listen when a ball has not been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.add(ball);
await game.ready();
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousBalls = game.descendants().whereType<Ball>().length;
controller.lost();
await game.ready();
final currentBalls = game.descendants().whereType<Ball>().length;
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(3);
expect(previousBalls, equals(currentBalls));
expect(controller.listenWhen(previousState, newState), isFalse);
},
);
});
group('onNewState', () {
tester.testGameWidget(
'no ball is added on game over',
'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 {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState(
score: 10,
balls: 1,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
expect(game.contains(ball), isFalse);
},
);
final controller = BallController(ball);
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.add(ball);
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.lost();
controller.onNewState(state);
await game.ready();
final currentBalls = game.descendants().whereType<Ball>().length;
final currentBalls = game.descendants().whereType<Ball>();
expect(currentBalls.contains(ball), isFalse);
expect(
currentBalls,
currentBalls.length,
equals((previousBalls..remove(ball)).length),
);
},
);
});
});
});
}

@ -66,10 +66,6 @@ class MockFilter extends Mock implements Filter {}
class MockFixture extends Mock implements Fixture {}
class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {}
class MockSpaceshipHole extends Mock implements SpaceshipHole {}
class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {}
class MockComponentSet extends Mock implements ComponentSet {}

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

Loading…
Cancel
Save