Merge branch 'main' into feat/plunger-animation

pull/200/head
RuiAlonso 3 years ago
commit 2316dc3f64

@ -12,7 +12,6 @@ class GameBloc extends Bloc<GameEvent, GameState> {
on<BallLost>(_onBallLost);
on<Scored>(_onScored);
on<BonusActivated>(_onBonusActivated);
on<DashNestActivated>(_onDashNestActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
}
@ -34,31 +33,6 @@ class GameBloc extends Bloc<GameEvent, GameState> {
);
}
void _onDashNestActivated(DashNestActivated event, Emitter emit) {
final newNests = {
...state.activatedDashNests,
event.nestId,
};
final achievedBonus = newNests.length == 3;
if (achievedBonus) {
emit(
state.copyWith(
balls: state.balls + 1,
activatedDashNests: {},
bonusHistory: [
...state.bonusHistory,
GameBonus.dashNest,
],
),
);
} else {
emit(
state.copyWith(activatedDashNests: newNests),
);
}
}
Future<void> _onSparkyTurboChargeActivated(
SparkyTurboChargeActivated event,
Emitter emit,

@ -42,15 +42,6 @@ class BonusActivated extends GameEvent {
List<Object?> get props => [bonus];
}
class DashNestActivated extends GameEvent {
const DashNestActivated(this.nestId);
final String nestId;
@override
List<Object?> get props => [nestId];
}
class SparkyTurboChargeActivated extends GameEvent {
const SparkyTurboChargeActivated();

@ -23,14 +23,12 @@ class GameState extends Equatable {
required this.score,
required this.balls,
required this.bonusHistory,
required this.activatedDashNests,
}) : assert(score >= 0, "Score can't be negative"),
assert(balls >= 0, "Number of balls can't be negative");
const GameState.initial()
: score = 0,
balls = 3,
activatedDashNests = const {},
bonusHistory = const [];
/// The current score of the game.
@ -41,9 +39,6 @@ class GameState extends Equatable {
/// When the number of balls is 0, the game is over.
final int balls;
/// Active dash nests.
final Set<String> activatedDashNests;
/// Holds the history of all the [GameBonus]es earned by the player during a
/// PinballGame.
final List<GameBonus> bonusHistory;
@ -54,7 +49,6 @@ class GameState extends Equatable {
GameState copyWith({
int? score,
int? balls,
Set<String>? activatedDashNests,
List<GameBonus>? bonusHistory,
}) {
assert(
@ -65,7 +59,6 @@ class GameState extends Equatable {
return GameState(
score: score ?? this.score,
balls: balls ?? this.balls,
activatedDashNests: activatedDashNests ?? this.activatedDashNests,
bonusHistory: bonusHistory ?? this.bonusHistory,
);
}
@ -74,7 +67,6 @@ class GameState extends Equatable {
List<Object?> get props => [
score,
balls,
activatedDashNests,
bonusHistory,
];
}

@ -1,9 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -15,9 +13,8 @@ import 'package:pinball_flame/pinball_flame.dart';
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [BigDashNestBumper] releases a new [Ball].
/// {@endtemplate}
// TODO(alestiago): Make a [Blueprint] once [Blueprint] inherits from
// [Component].
class FlutterForest extends Component with Controls<_FlutterForestController> {
class FlutterForest extends Component
with Controls<_FlutterForestController>, HasGameRef<PinballGame> {
/// {@macro flutter_forest}
FlutterForest() {
controller = _FlutterForestController(this);
@ -26,17 +23,16 @@ class FlutterForest extends Component with Controls<_FlutterForestController> {
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_DashNestBumperBallContactCallback());
final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, -58.3);
final bigNest = _ControlledBigDashNestBumper(
id: 'big_nest_bumper',
)..initialPosition = Vector2(18.55, -59.35);
final smallLeftNest = _ControlledSmallDashNestBumper.a(
id: 'small_nest_bumper_a',
)..initialPosition = Vector2(8.95, -51.95);
final smallRightNest = _ControlledSmallDashNestBumper.b(
id: 'small_nest_bumper_b',
)..initialPosition = Vector2(23.3, -46.75);
final bigNest = _BigDashNestBumper()
..initialPosition = Vector2(18.55, -59.35);
final smallLeftNest = _SmallDashNestBumper.a()
..initialPosition = Vector2(8.95, -51.95);
final smallRightNest = _SmallDashNestBumper.b()
..initialPosition = Vector2(23.3, -46.75);
final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66);
await addAll([
@ -50,31 +46,31 @@ class FlutterForest extends Component with Controls<_FlutterForestController> {
}
class _FlutterForestController extends ComponentController<FlutterForest>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
with HasGameRef<PinballGame> {
_FlutterForestController(FlutterForest flutterForest) : super(flutterForest);
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_ControlledDashNestBumperBallContactCallback());
}
final _activatedBumpers = <DashNestBumper>{};
@override
bool listenWhen(GameState? previousState, GameState newState) {
return (previousState?.bonusHistory.length ?? 0) <
newState.bonusHistory.length &&
newState.bonusHistory.last == GameBonus.dashNest;
}
void activateBumper(DashNestBumper dashNestBumper) {
if (!_activatedBumpers.add(dashNestBumper)) return;
@override
void onNewState(GameState state) {
super.onNewState(state);
dashNestBumper.activate();
component.firstChild<DashAnimatronic>()?.playing = true;
final activatedBonus = _activatedBumpers.length == 3;
if (activatedBonus) {
_addBonusBall();
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.dashNest));
_activatedBumpers
..forEach((bumper) => bumper.deactivate())
..clear();
component.firstChild<DashAnimatronic>()?.playing = true;
}
}
Future<void> _addBonusBall() async {
// TODO(alestiago): Remove hardcoded duration.
await Future<void>.delayed(const Duration(milliseconds: 700));
await gameRef.add(
ControlledBall.bonus(theme: gameRef.theme)
@ -83,83 +79,29 @@ class _FlutterForestController extends ComponentController<FlutterForest>
}
}
class _ControlledBigDashNestBumper extends BigDashNestBumper
with Controls<DashNestBumperController>, ScorePoints {
_ControlledBigDashNestBumper({required String id}) : super() {
controller = DashNestBumperController(this, id: id);
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _BigDashNestBumper extends BigDashNestBumper with ScorePoints {
@override
int get points => 20;
}
class _ControlledSmallDashNestBumper extends SmallDashNestBumper
with Controls<DashNestBumperController>, ScorePoints {
_ControlledSmallDashNestBumper.a({required String id}) : super.a() {
controller = DashNestBumperController(this, id: id);
}
class _SmallDashNestBumper extends SmallDashNestBumper with ScorePoints {
_SmallDashNestBumper.a() : super.a();
_ControlledSmallDashNestBumper.b({required String id}) : super.b() {
controller = DashNestBumperController(this, id: id);
}
_SmallDashNestBumper.b() : super.b();
@override
int get points => 10;
int get points => 20;
}
/// {@template dash_nest_bumper_controller}
/// Controls a [DashNestBumper].
/// {@endtemplate}
@visibleForTesting
class DashNestBumperController extends ComponentController<DashNestBumper>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
/// {@macro dash_nest_bumper_controller}
DashNestBumperController(
DashNestBumper dashNestBumper, {
required this.id,
}) : super(dashNestBumper);
/// Unique identifier for the controlled [DashNestBumper].
///
/// Used to identify [DashNestBumper]s in [GameState.activatedDashNests].
final String id;
class _DashNestBumperBallContactCallback
extends ContactCallback<DashNestBumper, Ball> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
final wasActive = previousState?.activatedDashNests.contains(id) ?? false;
final isActive = newState.activatedDashNests.contains(id);
return wasActive != isActive;
void begin(DashNestBumper dashNestBumper, _, __) {
final parent = dashNestBumper.parent;
if (parent is FlutterForest) {
parent.controller.activateBumper(dashNestBumper);
}
@override
void onNewState(GameState state) {
super.onNewState(state);
if (state.activatedDashNests.contains(id)) {
component.activate();
} else {
component.deactivate();
}
}
/// Registers when a [DashNestBumper] is hit by a [Ball].
///
/// Triggered by [_ControlledDashNestBumperBallContactCallback].
void hit() {
gameRef.read<GameBloc>().add(DashNestActivated(id));
}
}
/// Listens when a [Ball] bounces bounces against a [DashNestBumper].
class _ControlledDashNestBumperBallContactCallback
extends ContactCallback<Controls<DashNestBumperController>, Ball> {
@override
void begin(
Controls<DashNestBumperController> controlledDashNestBumper,
Ball _,
Contact __,
) {
controlledDashNestBumper.controller.hit();
}
}

@ -34,10 +34,7 @@ class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
ScorePoints scorePoints,
Contact __,
) {
_gameRef.read<GameBloc>().add(
Scored(points: scorePoints.points),
);
_gameRef.read<GameBloc>().add(Scored(points: scorePoints.points));
_gameRef.audio.score();
}
}

@ -14,13 +14,18 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
/// {@macro ball}
Ball({
required this.baseColor,
}) {
}) : super(
children: [
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
],
) {
// TODO(ruimiguel): while developing Ball can be launched by clicking mouse,
// and default layer is Layer.all. But on final game Ball will be always be
// be launched from Plunger and LauncherRamp will modify it to Layer.board.
// We need to see what happens if Ball appears from other place like nest
// bumper, it will need to explicit change layer to Layer.board then.
layer = Layer.board;
renderBody = false;
}
/// Render priority for the [Ball] while it's on the board.
@ -47,20 +52,6 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
double _boostTimer = 0;
static const _boostDuration = 2.0;
final _BallSpriteComponent _spriteComponent = _BallSpriteComponent();
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(
_spriteComponent..tint(baseColor.withOpacity(0.5)),
);
renderBody = false;
}
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
@ -132,7 +123,10 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor;
_spriteComponent.scale = Vector2.all(scaleFactor);
// TODO(alestiago): Revisit and see if there's a better way to do this.
final spriteComponent = firstChild<_BallSpriteComponent>();
spriteComponent?.scale = Vector2.all(scaleFactor);
}
void _setPositionalGravity() {

@ -11,7 +11,12 @@ class Baseboard extends BodyComponent with InitialPosition {
/// {@macro baseboard}
Baseboard({
required BoardSide side,
}) : _side = side;
}) : _side = side,
super(
children: [_BaseboardSpriteComponent(side: side)],
) {
renderBody = false;
}
/// Whether the [Baseboard] is on the left or right side of the board.
final BoardSide _side;
@ -79,13 +84,6 @@ class Baseboard extends BodyComponent with InitialPosition {
return fixturesDef;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_BaseboardSpriteComponent(side: _side));
}
@override
Body createBody() {
const angle = 37.1 * (math.pi / 180);

@ -24,7 +24,13 @@ class Boundaries extends Forge2DBlueprint {
/// {@endtemplate bottom_boundary}
class _BottomBoundary extends BodyComponent with InitialPosition {
/// {@macro bottom_boundary}
_BottomBoundary() : super(priority: 1);
_BottomBoundary()
: super(
priority: 1,
children: [_BottomBoundarySpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[];
@ -60,13 +66,6 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_BottomBoundarySpriteComponent());
}
}
class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
@ -89,7 +88,13 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
/// {@endtemplate outer_boundary}
class _OuterBoundary extends BodyComponent with InitialPosition {
/// {@macro outer_boundary}
_OuterBoundary() : super(priority: Ball.launchRampPriority - 1);
_OuterBoundary()
: super(
priority: Ball.launchRampPriority - 1,
children: [_OuterBoundarySpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[];
@ -131,13 +136,6 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_OuterBoundarySpriteComponent());
}
}
class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef {

@ -19,8 +19,8 @@ export 'joint_anchor.dart';
export 'kicker.dart';
export 'launch_ramp.dart';
export 'layer.dart';
export 'layer_sensor.dart';
export 'plunger.dart';
export 'ramp_opening.dart';
export 'rocket.dart';
export 'score_text.dart';
export 'shapes/shapes.dart';

@ -29,7 +29,13 @@ class DinoWalls extends Forge2DBlueprint {
/// {@endtemplate}
class _DinoTopWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall}
_DinoTopWall() : super(priority: 1);
_DinoTopWall()
: super(
priority: 1,
children: [_DinoTopWallSpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
@ -98,14 +104,6 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_DinoTopWallSpriteComponent());
}
}
class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef {
@ -126,7 +124,12 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef {
/// {@endtemplate}
class _DinoBottomWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall}
_DinoBottomWall();
_DinoBottomWall()
: super(
children: [_DinoBottomWallSpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
@ -206,14 +209,6 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_DinoBottomWallSpriteComponent());
}
}
class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef {

@ -13,7 +13,11 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
/// {@macro flipper}
Flipper({
required this.side,
});
}) : super(
children: [_FlipperSpriteComponent(side: side)],
) {
renderBody = false;
}
/// The size of the [Flipper].
static final size = Vector2(13.5, 4.3);
@ -112,10 +116,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await _anchorToJoint();
await add(_FlipperSpriteComponent(side: side));
}
@override

@ -6,12 +6,12 @@ import 'package:pinball_components/pinball_components.dart';
/// A sign, found in the Flutter Forest.
/// {@endtemplate}
class FlutterSignPost extends BodyComponent with InitialPosition {
@override
Future<void> onLoad() async {
await super.onLoad();
/// {@macro flutter_sign_post}
FlutterSignPost()
: super(
children: [_FlutterSignPostSpriteComponent()],
) {
renderBody = false;
await add(_FlutterSignPostSpriteComponent());
}
@override

@ -16,7 +16,12 @@ class Kicker extends BodyComponent with InitialPosition {
/// {@macro kicker}
Kicker({
required BoardSide side,
}) : _side = side;
}) : _side = side,
super(
children: [_KickerSpriteComponent(side: side)],
) {
renderBody = false;
}
/// The size of the [Kicker] body.
static final Vector2 size = Vector2(4.4, 15);
@ -120,13 +125,6 @@ class Kicker extends BodyComponent with InitialPosition {
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_KickerSpriteComponent(side: _side));
}
}
class _KickerSpriteComponent extends SpriteComponent with HasGameRef {

@ -15,7 +15,7 @@ class LaunchRamp extends Forge2DBlueprint {
@override
void build(_) {
addAllContactCallback([
RampOpeningBallContactCallback<_LaunchRampExit>(),
LayerSensorBallContactCallback<_LaunchRampExit>(),
]);
final launchRampBase = _LaunchRampBase();
@ -236,10 +236,10 @@ class _LaunchRampCloseWall extends BodyComponent with InitialPosition, Layered {
}
/// {@template launch_ramp_exit}
/// [RampOpening] with [Layer.launcher] to filter [Ball]s exiting the
/// [LayerSensor] with [Layer.launcher] to filter [Ball]s exiting the
/// [LaunchRamp].
/// {@endtemplate}
class _LaunchRampExit extends RampOpening {
class _LaunchRampExit extends LayerSensor {
/// {@macro launch_ramp_exit}
_LaunchRampExit({
required double rotation,
@ -247,7 +247,7 @@ class _LaunchRampExit extends RampOpening {
super(
insideLayer: Layer.launcher,
outsideLayer: Layer.board,
orientation: RampOrientation.down,
orientation: LayerEntranceOrientation.down,
insidePriority: Ball.launchRampPriority,
outsidePriority: 0,
) {

@ -0,0 +1,110 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template layer_entrance_orientation}
/// Determines if a layer entrance is oriented [up] or [down] on the board.
/// {@endtemplate}
enum LayerEntranceOrientation {
/// Facing up on the Board.
up,
/// Facing down on the Board.
down,
}
/// {@template layer_sensor}
/// [BodyComponent] located at the entrance and exit of a [Layer].
///
/// [LayerSensorBallContactCallback] detects when a [Ball] passes
/// through this sensor.
///
/// By default the base [layer] is set to [Layer.board] and the
/// [outsidePriority] is set to the lowest possible [Layer].
/// {@endtemplate}
abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
/// {@macro layer_sensor}
LayerSensor({
required Layer insideLayer,
Layer? outsideLayer,
required int insidePriority,
int? outsidePriority,
required this.orientation,
}) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board,
_insidePriority = insidePriority,
_outsidePriority = outsidePriority ?? Ball.boardPriority {
layer = Layer.opening;
}
final Layer _insideLayer;
final Layer _outsideLayer;
final int _insidePriority;
final int _outsidePriority;
/// Mask bits value for collisions on [Layer].
Layer get insideLayer => _insideLayer;
/// Mask bits value for collisions outside of [Layer].
Layer get outsideLayer => _outsideLayer;
/// Render priority for the [Ball] on [Layer].
int get insidePriority => _insidePriority;
/// Render priority for the [Ball] outside of [Layer].
int get outsidePriority => _outsidePriority;
/// The [Shape] of the [LayerSensor].
Shape get shape;
/// {@macro layer_entrance_orientation}
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
// collision calculations.
final LayerEntranceOrientation orientation;
@override
Body createBody() {
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@template layer_sensor_ball_contact_callback}
/// Detects when a [Ball] enters or exits a [Layer] through a [LayerSensor].
///
/// Modifies [Ball]'s [Layer] and render priority depending on whether the
/// [Ball] is on or outside of a [Layer].
/// {@endtemplate}
class LayerSensorBallContactCallback<LayerEntrance extends LayerSensor>
extends ContactCallback<Ball, LayerEntrance> {
@override
void begin(Ball ball, LayerEntrance layerEntrance, Contact _) {
if (ball.layer != layerEntrance.insideLayer) {
final isBallEnteringOpening =
(layerEntrance.orientation == LayerEntranceOrientation.down &&
ball.body.linearVelocity.y < 0) ||
(layerEntrance.orientation == LayerEntranceOrientation.up &&
ball.body.linearVelocity.y > 0);
if (isBallEnteringOpening) {
ball
..layer = layerEntrance.insideLayer
..priority = layerEntrance.insidePriority
..reorderChildren();
}
} else {
ball
..layer = layerEntrance.outsideLayer
..priority = layerEntrance.outsidePriority
..reorderChildren();
}
}
}

@ -1,130 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_orientation}
/// Determines if a ramp is facing [up] or [down] on the Board.
/// {@endtemplate}
enum RampOrientation {
/// Facing up on the Board.
up,
/// Facing down on the Board.
down,
}
/// {@template ramp_opening}
/// [BodyComponent] located at the entrance and exit of a ramp.
///
/// [RampOpeningBallContactCallback] detects when a [Ball] passes
/// through this opening.
///
/// 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 insideLayer,
Layer? outsideLayer,
required int insidePriority,
int? outsidePriority,
required this.orientation,
}) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board,
_insidePriority = insidePriority,
_outsidePriority = outsidePriority ?? Ball.boardPriority {
layer = Layer.opening;
}
final Layer _insideLayer;
final Layer _outsideLayer;
final int _insidePriority;
final int _outsidePriority;
/// Mask of category bits for collision inside ramp.
Layer get insideLayer => _insideLayer;
/// Mask of category bits for collision outside ramp.
Layer get outsideLayer => _outsideLayer;
/// Priority for the [Ball] inside ramp.
int get insidePriority => _insidePriority;
/// Priority for the [Ball] outside ramp.
int get outsidePriority => _outsidePriority;
/// The [Shape] of the [RampOpening].
Shape get shape;
/// {@macro ramp_orientation}
// TODO(ruimiguel): Try to remove the need of [RampOrientation] for collision
// calculations.
final RampOrientation orientation;
@override
Body createBody() {
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@template ramp_opening_ball_contact_callback}
/// 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.
/// {@endtemplate}
class RampOpeningBallContactCallback<Opening extends RampOpening>
extends ContactCallback<Ball, Opening> {
/// [Ball]s currently inside the ramp.
final _ballsInside = <Ball>{};
@override
void begin(Ball ball, Opening opening, Contact _) {
Layer layer;
if (!_ballsInside.contains(ball)) {
layer = opening.insideLayer;
_ballsInside.add(ball);
ball
..sendTo(opening.insidePriority)
..layer = layer;
} else {
_ballsInside.remove(ball);
}
}
@override
void end(Ball ball, Opening opening, Contact _) {
if (!_ballsInside.contains(ball)) {
ball.layer = opening.outsideLayer;
} else {
// TODO(ruimiguel): change this code. Check what happens with ball that
// slightly touch Opening and goes out again. With InitialPosition change
// now doesn't work position.y comparison
final isBallOutsideOpening =
(opening.orientation == RampOrientation.down &&
ball.body.linearVelocity.y > 0) ||
(opening.orientation == RampOrientation.up &&
ball.body.linearVelocity.y < 0);
if (isBallOutsideOpening) {
ball
..sendTo(opening.outsidePriority)
..layer = opening.outsideLayer;
_ballsInside.remove(ball);
}
}
}
}

@ -42,15 +42,17 @@ class Slingshot extends BodyComponent with InitialPosition {
required String spritePath,
}) : _length = length,
_angle = angle,
_spritePath = spritePath,
super(priority: 1);
super(
priority: 1,
children: [_SlinghsotSpriteComponent(spritePath, angle: angle)],
) {
renderBody = false;
}
final double _length;
final double _angle;
final String _spritePath;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
const circleRadius = 1.55;
@ -104,24 +106,25 @@ class Slingshot extends BodyComponent with InitialPosition {
return body;
}
}
class _SlinghsotSpriteComponent extends SpriteComponent with HasGameRef {
_SlinghsotSpriteComponent(
String path, {
required double angle,
}) : _path = path,
super(
angle: -angle,
anchor: Anchor.center,
);
final String _path;
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
}
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(_spritePath);
await add(
SpriteComponent(
sprite: sprite,
size: sprite.originalSize / 10,
anchor: Anchor.center,
angle: -_angle,
),
);
final sprite = await gameRef.loadSprite(_path);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

@ -25,19 +25,19 @@ class Spaceship extends Forge2DBlueprint {
@override
void build(_) {
addAllContactCallback([
SpaceshipHoleBallContactCallback(),
SpaceshipEntranceBallContactCallback(),
LayerSensorBallContactCallback<_SpaceshipEntrance>(),
LayerSensorBallContactCallback<_SpaceshipHole>(),
]);
addAll([
SpaceshipSaucer()..initialPosition = position,
SpaceshipEntrance()..initialPosition = position,
_SpaceshipEntrance()..initialPosition = position,
AndroidHead()..initialPosition = position,
SpaceshipHole(
_SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail,
outsidePriority: Ball.spaceshipRailPriority,
)..initialPosition = position - Vector2(5.2, -4.8),
SpaceshipHole()..initialPosition = position - Vector2(-7.2, -0.8),
_SpaceshipHole()..initialPosition = position - Vector2(-7.2, -0.8),
SpaceshipWall()..initialPosition = position,
]);
}
@ -92,16 +92,13 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@endtemplate}
class AndroidHead extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_bridge}
AndroidHead() : super(priority: Ball.spaceshipPriority + 1) {
layer = Layer.spaceship;
}
@override
Future<void> onLoad() async {
await super.onLoad();
AndroidHead()
: super(
priority: Ball.spaceshipPriority + 1,
children: [_AndroidHeadSpriteAnimation()],
) {
renderBody = false;
await add(_AndroidHeadSpriteAnimation());
layer = Layer.spaceship;
}
@override
@ -143,17 +140,11 @@ class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent
}
}
/// {@template spaceship_entrance}
/// A sensor [BodyComponent] used to detect when the ball enters the
/// the spaceship area in order to modify its filter data so the ball
/// can correctly collide only with the Spaceship
/// {@endtemplate}
class SpaceshipEntrance extends RampOpening {
/// {@macro spaceship_entrance}
SpaceshipEntrance()
class _SpaceshipEntrance extends LayerSensor {
_SpaceshipEntrance()
: super(
insideLayer: Layer.spaceship,
orientation: RampOrientation.up,
orientation: LayerEntranceOrientation.up,
insidePriority: Ball.spaceshipPriority,
) {
layer = Layer.spaceship;
@ -177,17 +168,12 @@ class SpaceshipEntrance extends RampOpening {
}
}
/// {@template spaceship_hole}
/// A sensor [BodyComponent] responsible for sending the [Ball]
/// out from the [Spaceship].
/// {@endtemplate}
class SpaceshipHole extends RampOpening {
/// {@macro spaceship_hole}
SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1})
class _SpaceshipHole extends LayerSensor {
_SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1})
: super(
insideLayer: Layer.spaceship,
outsideLayer: outsideLayer,
orientation: RampOrientation.up,
orientation: LayerEntranceOrientation.down,
insidePriority: 4,
outsidePriority: outsidePriority,
) {
@ -259,33 +245,3 @@ class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// [ContactCallback] that handles the contact between the [Ball]
/// and the [SpaceshipEntrance].
///
/// It modifies the [Ball] priority and filter data so it can appear on top of
/// the spaceship and also only collide with the spaceship.
class SpaceshipEntranceBallContactCallback
extends ContactCallback<SpaceshipEntrance, Ball> {
@override
void begin(SpaceshipEntrance entrance, Ball ball, _) {
ball
..sendTo(entrance.insidePriority)
..layer = Layer.spaceship;
}
}
/// [ContactCallback] that handles the contact between the [Ball]
/// and a [SpaceshipHole].
///
/// 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
..sendTo(hole.outsidePriority)
..layer = hole.outsideLayer;
}
}

@ -18,11 +18,11 @@ class SpaceshipRail extends Forge2DBlueprint {
@override
void build(_) {
addAllContactCallback([
SpaceshipRailExitBallContactCallback(),
LayerSensorBallContactCallback<_SpaceshipRailExit>(),
]);
final railRamp = _SpaceshipRailRamp();
final railEnd = SpaceshipRailExit();
final railEnd = _SpaceshipRailExit();
final topBase = _SpaceshipRailBase(radius: 0.55)
..initialPosition = Vector2(-26.15, -18.65);
final bottomBase = _SpaceshipRailBase(radius: 0.8)
@ -201,15 +201,10 @@ class _SpaceshipRailBase extends BodyComponent with InitialPosition, Layered {
}
}
/// {@template spaceship_rail_exit}
/// A sensor [BodyComponent] responsible for sending the [Ball]
/// back to the board.
/// {@endtemplate}
class SpaceshipRailExit extends RampOpening {
/// {@macro spaceship_rail_exit}
SpaceshipRailExit()
class _SpaceshipRailExit extends LayerSensor {
_SpaceshipRailExit()
: super(
orientation: RampOrientation.down,
orientation: LayerEntranceOrientation.down,
insideLayer: Layer.spaceshipExitRail,
insidePriority: Ball.spaceshipRailPriority,
) {
@ -227,18 +222,3 @@ class SpaceshipRailExit extends RampOpening {
);
}
}
/// [ContactCallback] that handles the contact between the [Ball]
/// and a [SpaceshipRailExit].
///
/// It resets the [Ball] priority and filter data so it will "be back" on the
/// board.
class SpaceshipRailExitBallContactCallback
extends ContactCallback<SpaceshipRailExit, Ball> {
@override
void begin(SpaceshipRailExit exitRail, Ball ball, _) {
ball
..sendTo(exitRail.outsidePriority)
..layer = exitRail.outsideLayer;
}
}

@ -18,7 +18,7 @@ class SpaceshipRamp extends Forge2DBlueprint {
@override
void build(_) {
addAllContactCallback([
RampOpeningBallContactCallback<_SpaceshipRampOpening>(),
LayerSensorBallContactCallback<_SpaceshipRampOpening>(),
]);
final rightOpening = _SpaceshipRampOpening(
@ -170,8 +170,12 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
class _SpaceshipRampForegroundRailing extends BodyComponent
with InitialPosition, Layered {
_SpaceshipRampForegroundRailing()
: super(priority: Ball.spaceshipRampPriority + 1) {
: super(
priority: Ball.spaceshipRampPriority + 1,
children: [_SpaceshipRampForegroundRailingSpriteComponent()],
) {
layer = Layer.spaceshipEntranceRamp;
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() {
@ -222,14 +226,6 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_SpaceshipRampForegroundRailingSpriteComponent());
}
}
class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
@ -277,10 +273,10 @@ class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered {
}
/// {@template spaceship_ramp_opening}
/// [RampOpening] with [Layer.spaceshipEntranceRamp] to filter [Ball] collisions
/// [LayerSensor] with [Layer.spaceshipEntranceRamp] to filter [Ball] collisions
/// inside [_SpaceshipRampBackground].
/// {@endtemplate}
class _SpaceshipRampOpening extends RampOpening {
class _SpaceshipRampOpening extends LayerSensor {
/// {@macro spaceship_ramp_opening}
_SpaceshipRampOpening({
Layer? outsideLayer,
@ -290,7 +286,7 @@ class _SpaceshipRampOpening extends RampOpening {
super(
insideLayer: Layer.spaceshipEntranceRamp,
outsideLayer: outsideLayer,
orientation: RampOrientation.down,
orientation: LayerEntranceOrientation.down,
insidePriority: Ball.spaceshipRampPriority,
outsidePriority: outsidePriority,
) {

@ -13,12 +13,6 @@ class MockBall extends Mock implements Ball {}
class MockGame extends Mock implements Forge2DGame {}
class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {}
class MockSpaceshipHole extends Mock implements SpaceshipHole {}
class MockSpaceshipRailExit extends Mock implements SpaceshipRailExit {}
class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock

@ -18,6 +18,7 @@ void main() {
await game.addFromBlueprint(Boundaries());
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 3.9;
await game.ready();
},
verify: (game, tester) async {
await expectLater(

@ -19,6 +19,7 @@ void main() {
await game.addFromBlueprint(DinoWalls());
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 6.5;
await game.ready();
},
verify: (game, tester) async {
await expectLater(

@ -0,0 +1,181 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
class TestLayerSensor extends LayerSensor {
TestLayerSensor({
required LayerEntranceOrientation orientation,
required int insidePriority,
required Layer insideLayer,
}) : super(
insideLayer: insideLayer,
insidePriority: insidePriority,
orientation: orientation,
);
@override
Shape get shape => PolygonShape()..setAsBoxXY(1, 1);
}
class TestLayerSensorBallContactCallback
extends LayerSensorBallContactCallback<TestLayerSensor> {
TestLayerSensorBallContactCallback() : super();
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
const insidePriority = 1;
group('LayerSensor', () {
flameTester.test(
'loads correctly',
(game) async {
final layerSensor = TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insidePriority: insidePriority,
insideLayer: Layer.spaceshipEntranceRamp,
);
await game.ensureAdd(layerSensor);
expect(game.contains(layerSensor), isTrue);
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final layerSensor = TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insidePriority: insidePriority,
insideLayer: Layer.spaceshipEntranceRamp,
);
await game.ensureAdd(layerSensor);
expect(layerSensor.body.bodyType, equals(BodyType.static));
},
);
group('first fixture', () {
const pathwayLayer = Layer.spaceshipEntranceRamp;
const openingLayer = Layer.opening;
flameTester.test(
'exists',
(game) async {
final layerSensor = TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insidePriority: insidePriority,
insideLayer: pathwayLayer,
)..layer = openingLayer;
await game.ensureAdd(layerSensor);
expect(layerSensor.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'shape is a polygon',
(game) async {
final layerSensor = TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insidePriority: insidePriority,
insideLayer: pathwayLayer,
)..layer = openingLayer;
await game.ensureAdd(layerSensor);
final fixture = layerSensor.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.polygon));
},
);
flameTester.test(
'is sensor',
(game) async {
final layerSensor = TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insidePriority: insidePriority,
insideLayer: pathwayLayer,
)..layer = openingLayer;
await game.ensureAdd(layerSensor);
final fixture = layerSensor.body.fixtures[0];
expect(fixture.isSensor, isTrue);
},
);
});
});
});
group('LayerSensorBallContactCallback', () {
late Ball ball;
late Body body;
setUp(() {
ball = MockBall();
body = MockBody();
when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => ball.layer).thenReturn(Layer.board);
});
flameTester.test(
'changes ball layer and priority '
'when a ball enters and exits a downward oriented LayerSensor',
(game) async {
final sensor = TestLayerSensor(
orientation: LayerEntranceOrientation.down,
insidePriority: insidePriority,
insideLayer: Layer.spaceshipEntranceRamp,
)..initialPosition = Vector2(0, 10);
final callback = TestLayerSensorBallContactCallback();
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = sensor.insideLayer).called(1);
verify(() => ball.priority = sensor.insidePriority).called(1);
verify(ball.reorderChildren).called(1);
when(() => ball.layer).thenReturn(sensor.insideLayer);
callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.priority = Ball.boardPriority).called(1);
verify(ball.reorderChildren).called(1);
});
flameTester.test(
'changes ball layer and priority '
'when a ball enters and exits an upward oriented LayerSensor',
(game) async {
final sensor = TestLayerSensor(
orientation: LayerEntranceOrientation.up,
insidePriority: insidePriority,
insideLayer: Layer.spaceshipEntranceRamp,
)..initialPosition = Vector2(0, 10);
final callback = TestLayerSensorBallContactCallback();
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = sensor.insideLayer).called(1);
verify(() => ball.priority = sensor.insidePriority).called(1);
verify(ball.reorderChildren).called(1);
when(() => ball.layer).thenReturn(sensor.insideLayer);
callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = Layer.board);
verify(() => ball.priority = Ball.boardPriority).called(1);
verify(ball.reorderChildren).called(1);
});
});
}

@ -1,249 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
class TestRampOpening extends RampOpening {
TestRampOpening({
required RampOrientation orientation,
required int insidePriority,
required Layer pathwayLayer,
}) : super(
insideLayer: pathwayLayer,
insidePriority: insidePriority,
orientation: orientation,
);
@override
Shape get shape => PolygonShape()
..set([
Vector2(0, 0),
Vector2(0, 1),
Vector2(1, 1),
Vector2(1, 0),
]);
}
class TestRampOpeningBallContactCallback
extends RampOpeningBallContactCallback<TestRampOpening> {
TestRampOpeningBallContactCallback() : super();
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
const insidePriority = 1;
group('RampOpening', () {
flameTester.test(
'loads correctly',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
insidePriority: insidePriority,
pathwayLayer: Layer.spaceshipEntranceRamp,
);
await game.ready();
await game.ensureAdd(ramp);
expect(game.contains(ramp), isTrue);
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
insidePriority: insidePriority,
pathwayLayer: Layer.spaceshipEntranceRamp,
);
await game.ensureAdd(ramp);
expect(ramp.body.bodyType, equals(BodyType.static));
},
);
group('first fixture', () {
const pathwayLayer = Layer.spaceshipEntranceRamp;
const openingLayer = Layer.opening;
flameTester.test(
'exists',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
insidePriority: insidePriority,
pathwayLayer: pathwayLayer,
)..layer = openingLayer;
await game.ensureAdd(ramp);
expect(ramp.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'shape is a polygon',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
insidePriority: insidePriority,
pathwayLayer: pathwayLayer,
)..layer = openingLayer;
await game.ensureAdd(ramp);
final fixture = ramp.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.polygon));
},
);
flameTester.test(
'is sensor',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
insidePriority: insidePriority,
pathwayLayer: pathwayLayer,
)..layer = openingLayer;
await game.ensureAdd(ramp);
final fixture = ramp.body.fixtures[0];
expect(fixture.isSensor, isTrue);
},
);
});
});
});
group('RampOpeningBallContactCallback', () {
flameTester.test(
'changes ball layer '
'when a ball enters upwards into a downward ramp opening',
(game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.down,
insidePriority: insidePriority,
pathwayLayer: Layer.spaceshipEntranceRamp,
);
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);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.insideLayer).called(1);
});
flameTester.test(
'changes ball layer '
'when a ball enters downwards into a upward ramp opening',
(game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.up,
insidePriority: insidePriority,
pathwayLayer: Layer.spaceshipEntranceRamp,
);
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);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.insideLayer).called(1);
});
flameTester.test(
'changes ball layer '
'when a ball exits from a downward oriented ramp', (game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.down,
insidePriority: insidePriority,
pathwayLayer: Layer.spaceshipEntranceRamp,
)..initialPosition = Vector2(0, 10);
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);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.insideLayer).called(1);
callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board);
});
flameTester.test(
'changes ball layer '
'when a ball exits from a upward oriented ramp', (game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.up,
insidePriority: insidePriority,
pathwayLayer: Layer.spaceshipEntranceRamp,
)..initialPosition = Vector2(0, 10);
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);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.insideLayer).called(1);
callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board);
});
flameTester.test(
'change ball layer from pathwayLayer to Layer.board '
'when a ball enters and exits from ramp', (game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.down,
insidePriority: insidePriority,
pathwayLayer: Layer.spaceshipEntranceRamp,
)..initialPosition = Vector2(0, 10);
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);
callback.begin(ball, area, MockContact());
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.insideLayer);
callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board);
});
});
}

@ -20,6 +20,7 @@ void main() {
setUp: (game, tester) async {
await game.addFromBlueprint(Slingshots());
game.camera.followVector2(Vector2.zero());
await game.ready();
},
verify: (game, tester) async {
await expectLater(

@ -3,7 +3,6 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -42,57 +41,4 @@ void main() {
},
);
});
// TODO(alestiago): Make ContactCallback private and use `beginContact`
// instead.
group('SpaceshipRailExitBallContactCallback', () {
late Forge2DGame game;
late SpaceshipRailExit railExit;
late Ball ball;
late Body body;
late Fixture fixture;
late Filter filterData;
setUp(() {
game = MockGame();
railExit = MockSpaceshipRailExit();
ball = MockBall();
body = MockBody();
when(() => ball.gameRef).thenReturn(game);
when(() => ball.body).thenReturn(body);
fixture = MockFixture();
filterData = MockFilter();
when(() => body.fixtures).thenReturn([fixture]);
when(() => fixture.filterData).thenReturn(filterData);
});
setUp(() {
when(() => ball.priority).thenReturn(1);
when(() => railExit.outsideLayer).thenReturn(Layer.board);
when(() => railExit.outsidePriority).thenReturn(0);
});
test('changes the ball priority on contact', () {
SpaceshipRailExitBallContactCallback().begin(
railExit,
ball,
MockContact(),
);
verify(() => ball.sendTo(railExit.outsidePriority)).called(1);
});
test('changes the ball layer on contact', () {
SpaceshipRailExitBallContactCallback().begin(
railExit,
ball,
MockContact(),
);
verify(() => ball.layer = railExit.outsideLayer).called(1);
});
});
}

@ -17,6 +17,7 @@ void main() {
setUp: (game, tester) async {
await game.addFromBlueprint(SpaceshipRamp());
game.camera.followVector2(Vector2(-13, -50));
await game.ready();
},
verify: (game, tester) async {
await expectLater(

@ -15,8 +15,6 @@ void main() {
late Fixture fixture;
late Body body;
late Ball ball;
late SpaceshipEntrance entrance;
late SpaceshipHole hole;
late Forge2DGame game;
setUp(() {
@ -33,9 +31,6 @@ void main() {
ball = MockBall();
when(() => ball.gameRef).thenReturn(game);
when(() => ball.body).thenReturn(body);
entrance = MockSpaceshipEntrance();
hole = MockSpaceshipHole();
});
group('Spaceship', () {
@ -47,6 +42,7 @@ void main() {
final position = Vector2(30, -30);
await game.addFromBlueprint(Spaceship(position: position));
game.camera.followVector2(position);
await game.ready();
},
verify: (game, tester) async {
await expectLater(
@ -56,36 +52,5 @@ void main() {
},
);
});
group('SpaceshipEntranceBallContactCallback', () {
test('changes the ball priority on contact', () {
when(() => ball.priority).thenReturn(2);
when(() => entrance.insidePriority).thenReturn(3);
SpaceshipEntranceBallContactCallback().begin(
entrance,
ball,
MockContact(),
);
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.outsidePriority).thenReturn(1);
SpaceshipHoleBallContactCallback().begin(
hole,
ball,
MockContact(),
);
verify(() => ball.sendTo(hole.outsidePriority)).called(1);
});
});
});
}

@ -3,4 +3,3 @@ library pinball_flame;
export 'src/blueprint.dart';
export 'src/component_controller.dart';
export 'src/keyboard_input_controller.dart';
export 'src/priority.dart';

@ -1,39 +0,0 @@
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();
}
}
}

@ -1,221 +0,0 @@
// 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_flame/pinball_flame.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);
},
);
});
});
}

@ -21,7 +21,6 @@ void main() {
const GameState(
score: 0,
balls: 2,
activatedDashNests: {},
bonusHistory: [],
),
],
@ -40,13 +39,11 @@ void main() {
const GameState(
score: 2,
balls: 3,
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 5,
balls: 3,
activatedDashNests: {},
bonusHistory: [],
),
],
@ -66,56 +63,22 @@ void main() {
const GameState(
score: 0,
balls: 2,
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 1,
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 0,
activatedDashNests: {},
bonusHistory: [],
),
],
);
});
group('DashNestActivated', () {
blocTest<GameBloc, GameState>(
'adds the bonus when all nests are activated',
build: GameBloc.new,
act: (bloc) => bloc
..add(const DashNestActivated('0'))
..add(const DashNestActivated('1'))
..add(const DashNestActivated('2')),
expect: () => const [
GameState(
score: 0,
balls: 3,
activatedDashNests: {'0'},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedDashNests: {'0', '1'},
bonusHistory: [],
),
GameState(
score: 0,
balls: 4,
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
),
],
);
});
group(
'BonusActivated',
() {
@ -129,13 +92,11 @@ void main() {
GameState(
score: 0,
balls: 3,
activatedDashNests: {},
bonusHistory: [GameBonus.googleWord],
),
GameState(
score: 0,
balls: 3,
activatedDashNests: {},
bonusHistory: [GameBonus.googleWord, GameBonus.dashNest],
),
],
@ -152,7 +113,6 @@ void main() {
GameState(
score: 0,
balls: 3,
activatedDashNests: {},
bonusHistory: [GameBonus.sparkyTurboCharge],
),
],

@ -59,23 +59,6 @@ void main() {
});
});
group('DashNestActivated', () {
test('can be instantiated', () {
expect(const DashNestActivated('0'), isNotNull);
});
test('supports value equality', () {
expect(
DashNestActivated('0'),
equals(DashNestActivated('0')),
);
expect(
DashNestActivated('0'),
isNot(equals(DashNestActivated('1'))),
);
});
});
group('SparkyTurboChargeActivated', () {
test('can be instantiated', () {
expect(const SparkyTurboChargeActivated(), isNotNull);

@ -10,14 +10,12 @@ void main() {
GameState(
score: 0,
balls: 0,
activatedDashNests: const {},
bonusHistory: const [],
),
equals(
const GameState(
score: 0,
balls: 0,
activatedDashNests: {},
bonusHistory: [],
),
),
@ -30,7 +28,6 @@ void main() {
const GameState(
score: 0,
balls: 0,
activatedDashNests: {},
bonusHistory: [],
),
isNotNull,
@ -46,7 +43,6 @@ void main() {
() => GameState(
balls: -1,
score: 0,
activatedDashNests: const {},
bonusHistory: const [],
),
throwsAssertionError,
@ -62,7 +58,6 @@ void main() {
() => GameState(
balls: 0,
score: -1,
activatedDashNests: const {},
bonusHistory: const [],
),
throwsAssertionError,
@ -77,7 +72,6 @@ void main() {
const gameState = GameState(
balls: 0,
score: 0,
activatedDashNests: {},
bonusHistory: [],
);
expect(gameState.isGameOver, isTrue);
@ -89,7 +83,6 @@ void main() {
const gameState = GameState(
balls: 1,
score: 0,
activatedDashNests: {},
bonusHistory: [],
);
expect(gameState.isGameOver, isFalse);
@ -104,7 +97,6 @@ void main() {
const gameState = GameState(
balls: 0,
score: 2,
activatedDashNests: {},
bonusHistory: [],
);
expect(
@ -121,7 +113,6 @@ void main() {
const gameState = GameState(
balls: 0,
score: 2,
activatedDashNests: {},
bonusHistory: [],
);
expect(
@ -138,13 +129,11 @@ void main() {
const gameState = GameState(
score: 2,
balls: 0,
activatedDashNests: {},
bonusHistory: [],
);
final otherGameState = GameState(
score: gameState.score + 1,
balls: gameState.balls + 1,
activatedDashNests: const {'1'},
bonusHistory: const [GameBonus.googleWord],
);
expect(gameState, isNot(equals(otherGameState)));
@ -153,7 +142,6 @@ void main() {
gameState.copyWith(
score: otherGameState.score,
balls: otherGameState.balls,
activatedDashNests: otherGameState.activatedDashNests,
bonusHistory: otherGameState.bonusHistory,
),
equals(otherGameState),

@ -21,7 +21,6 @@ void main() {
score: 0,
balls: 0,
bonusHistory: [],
activatedDashNests: {},
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;

@ -22,7 +22,6 @@ void main() {
score: 0,
balls: 0,
bonusHistory: [],
activatedDashNests: {},
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;

@ -79,105 +79,21 @@ void main() {
);
});
group('controller', () {
group('listenWhen', () {
final gameBloc = MockGameBloc();
final flameBlocTester = FlameBlocTester<TestGame, GameBloc>(
gameBuilder: TestGame.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'listens when a Bonus.dashNest and a bonusBall is added',
verify: (game, tester) async {
final flutterForest = FlutterForest();
const state = GameState(
score: 0,
balls: 3,
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
);
expect(
flutterForest.controller
.listenWhen(const GameState.initial(), state),
isTrue,
);
},
);
});
});
flameTester.test(
'onNewState adds a new ball after a duration',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
final previousBalls = game.descendants().whereType<Ball>().length;
flutterForest.controller.onNewState(MockGameState());
await Future<void>.delayed(const Duration(milliseconds: 700));
await game.ready();
expect(
game.descendants().whereType<Ball>().length,
greaterThan(previousBalls),
);
},
);
flameTester.test(
'onNewState starts Dash animatronic',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
flutterForest.controller.onNewState(MockGameState());
final dashAnimatronic =
game.descendants().whereType<DashAnimatronic>().single;
expect(dashAnimatronic.playing, isTrue);
},
);
group('bumpers', () {
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 flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'add DashNestActivated event',
setUp: (game, tester) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
await game.ensureAdd(ball);
final bumpers =
flutterForest.descendants().whereType<DashNestBumper>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
final controller = bumper.firstChild<DashNestBumperController>()!;
verify(
() => gameBloc.add(DashNestActivated(controller.id)),
).called(1);
}
blocBuilder: () {
gameBloc = MockGameBloc();
const state = GameState.initial();
whenListen(gameBloc, Stream.value(state), initialState: state);
return gameBloc;
},
);
@ -185,8 +101,10 @@ void main() {
'add Scored event',
setUp: (game, tester) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
await game.ensureAdd(ball);
await game.ensureAddAll([
flutterForest,
ball,
]);
game.addContactCallback(BallScorePointsCallback(game));
final bumpers = flutterForest.descendants().whereType<ScorePoints>();
@ -201,122 +119,58 @@ void main() {
}
},
);
});
});
group('DashNestBumperController', () {
late DashNestBumper dashNestBumper;
setUp(() {
dashNestBumper = MockDashNestBumper();
});
group(
'listensWhen',
() {
late GameState previousState;
late GameState newState;
setUp(
() {
previousState = MockGameState();
newState = MockGameState();
},
);
test('listens when the id is added to activatedDashNests', () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({});
when(() => newState.activatedDashNests).thenReturn({id});
expect(controller.listenWhen(previousState, newState), isTrue);
});
test('listens when the id is removed from activatedDashNests', () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({id});
when(() => newState.activatedDashNests).thenReturn({});
expect(controller.listenWhen(previousState, newState), isTrue);
});
test("doesn't listen when the id is never in activatedDashNests", () {
final controller = DashNestBumperController(
dashNestBumper,
id: '',
);
when(() => previousState.activatedDashNests).thenReturn({});
when(() => newState.activatedDashNests).thenReturn({});
flameBlocTester.testGameWidget(
'adds GameBonus.dashNest to the game when 3 bumpers are activated',
setUp: (game, _) async {
final ball = Ball(baseColor: const Color(0xFFFF0000));
final flutterForest = FlutterForest();
await game.ensureAddAll([flutterForest, ball]);
expect(controller.listenWhen(previousState, newState), isFalse);
});
final bumpers = flutterForest.children.whereType<DashNestBumper>();
expect(bumpers.length, equals(3));
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
await game.ready();
test("doesn't listen when the id still in activatedDashNests", () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
if (bumper == bumpers.last) {
verify(
() => gameBloc.add(const BonusActivated(GameBonus.dashNest)),
).called(1);
} else {
verifyNever(
() => gameBloc.add(const BonusActivated(GameBonus.dashNest)),
);
when(() => previousState.activatedDashNests).thenReturn({id});
when(() => newState.activatedDashNests).thenReturn({id});
expect(controller.listenWhen(previousState, newState), isFalse);
});
}
}
},
);
group(
'onNewState',
() {
late GameState state;
setUp(() {
state = MockGameState();
});
test(
'activates the bumper when id in activatedDashNests',
() {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => state.activatedDashNests).thenReturn({id});
controller.onNewState(state);
verify(() => dashNestBumper.activate()).called(1);
},
);
flameBlocTester.testGameWidget(
'deactivates bumpers when 3 are active',
setUp: (game, _) async {
final ball = Ball(baseColor: const Color(0xFFFF0000));
final flutterForest = FlutterForest();
await game.ensureAddAll([flutterForest, ball]);
test(
'deactivates the bumper when id not in activatedDashNests',
() {
final controller = DashNestBumperController(
dashNestBumper,
id: '',
);
final bumpers = [
MockDashNestBumper(),
MockDashNestBumper(),
MockDashNestBumper(),
];
when(() => state.activatedDashNests).thenReturn({});
controller.onNewState(state);
for (final bumper in bumpers) {
flutterForest.controller.activateBumper(bumper);
await game.ready();
verify(() => dashNestBumper.deactivate()).called(1);
},
);
if (bumper == bumpers.last) {
for (final bumper in bumpers) {
verify(bumper.deactivate).called(1);
}
}
}
},
);
});
});
}

@ -16,7 +16,6 @@ void main() {
score: 10,
balls: 0,
bonusHistory: const [],
activatedDashNests: const {},
);
final previous = GameState.initial();
@ -66,7 +65,6 @@ void main() {
score: 10,
balls: 0,
bonusHistory: const [],
activatedDashNests: const {},
),
);

@ -31,7 +31,6 @@ void main() {
score: 10,
balls: 3,
bonusHistory: [],
activatedDashNests: {},
);
expect(controller.listenWhen(previous, current), isTrue);
});
@ -44,7 +43,6 @@ void main() {
score: 10,
balls: 3,
bonusHistory: [],
activatedDashNests: {},
);
expect(controller.listenWhen(null, current), isTrue);
},
@ -69,7 +67,6 @@ void main() {
score: 10,
balls: 3,
bonusHistory: [],
activatedDashNests: {},
);
controller.onNewState(state);
@ -87,7 +84,6 @@ void main() {
score: 10,
balls: 3,
bonusHistory: [],
activatedDashNests: {},
),
);
@ -96,7 +92,6 @@ void main() {
score: 14,
balls: 3,
bonusHistory: [],
activatedDashNests: {},
),
);

@ -12,7 +12,6 @@ void main() {
const initialState = GameState(
score: 10,
balls: 2,
activatedDashNests: {},
bonusHistory: [],
);

@ -31,11 +31,6 @@ class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock
implements ContactCallback<Object, Object> {}
class MockRampOpening extends Mock implements RampOpening {}
class MockRampOpeningBallContactCallback extends Mock
implements RampOpeningBallContactCallback {}
class MockGameBloc extends Mock implements GameBloc {}
class MockGameState extends Mock implements GameState {}

Loading…
Cancel
Save