feat: migrated to new ContactCallbacks

pull/234/head
alestiago 3 years ago
parent b9c2f3a54f
commit d809145152

@ -2,9 +2,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template alien_zone} /// {@template alien_zone}
/// Area positioned below [Spaceship] where the [Ball] /// Area positioned below [Spaceship] where the [Ball]
@ -12,49 +12,36 @@ import 'package:pinball_components/pinball_components.dart';
/// ///
/// When a [Ball] hits an [AlienBumper], the bumper animates. /// When a [Ball] hits an [AlienBumper], the bumper animates.
/// {@endtemplate} /// {@endtemplate}
class AlienZone extends Component with HasGameRef<PinballGame> { class AlienZone extends Blueprint {
/// {@macro alien_zone} /// {@macro alien_zone}
AlienZone(); AlienZone()
: super(
@override components: [
Future<void> onLoad() async { AlienBumper.a(
await super.onLoad(); children: [
ScoringBehaviour(points: 20),
gameRef.addContactCallback(AlienBumperBallContactCallback()); ],
)..initialPosition = Vector2(-32.52, -9.1),
final lowerBumper = _AlienBumper.a() AlienBumper.b(
..initialPosition = Vector2(-32.52, -9.1); children: [
final upperBumper = _AlienBumper.b() ScoringBehaviour(points: 20),
..initialPosition = Vector2(-22.89, -17.35); ],
)..initialPosition = Vector2(-22.89, -17.35),
await addAll([ ],
lowerBumper, );
upperBumper,
]);
}
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _AlienBumper extends AlienBumper with ScorePoints {
_AlienBumper.a() : super.a();
_AlienBumper.b() : super.b();
@override
int get points => 20;
} }
/// Listens when a [Ball] bounces against an [AlienBumper]. /// Listens when a [Ball] bounces against an [AlienBumper].
@visibleForTesting // TODO(alestiago): Add Bumper animation behaviour.
class AlienBumperBallContactCallback // @visibleForTesting
extends ContactCallback<AlienBumper, Ball> { // class AlienBumperBallContactCallback
@override // extends ContactCallback<AlienBumper, Ball> {
void begin( // @override
AlienBumper alienBumper, // void begin(
Ball _, // AlienBumper alienBumper,
Contact __, // Ball _,
) { // Contact __,
alienBumper.animate(); // ) {
} // alienBumper.animate();
} // }
// }

@ -8,6 +8,6 @@ export 'flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_flow_controller.dart';
export 'google_word.dart'; export 'google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'score_points.dart'; export 'scoring_behaviour.dart';
export 'sparky_fire_zone.dart'; export 'sparky_fire_zone.dart';
export 'wall.dart'; export 'wall.dart';

@ -49,10 +49,6 @@ class BallController extends ComponentController<Ball>
/// {@macro ball_controller} /// {@macro ball_controller}
BallController(Ball ball) : super(ball); BallController(Ball ball) : super(ball);
/// Removes the [Ball] from a [PinballGame].
///
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
void lost() { void lost() {
component.shouldRemove = true; component.shouldRemove = true;
} }

@ -1,7 +1,6 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -16,33 +15,34 @@ import 'package:pinball_flame/pinball_flame.dart';
class FlutterForest extends Component class FlutterForest extends Component
with Controls<_FlutterForestController>, HasGameRef<PinballGame> { with Controls<_FlutterForestController>, HasGameRef<PinballGame> {
/// {@macro flutter_forest} /// {@macro flutter_forest}
FlutterForest() { FlutterForest()
: super(
children: [
Signpost(
children: [
ScoringBehaviour(points: 20),
],
)..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main(
children: [
ScoringBehaviour(points: 20),
],
)..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a(
children: [
ScoringBehaviour(points: 20),
],
)..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b(
children: [
ScoringBehaviour(points: 20),
],
)..initialPosition = Vector2(23.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66),
],
) {
controller = _FlutterForestController(this); controller = _FlutterForestController(this);
} }
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_DashNestBumperBallContactCallback());
final signpost = Signpost()..initialPosition = Vector2(8.35, -58.3);
final bigNest = _DashNestBumper.main()
..initialPosition = Vector2(18.55, -59.35);
final smallLeftNest = _DashNestBumper.a()
..initialPosition = Vector2(8.95, -51.95);
final smallRightNest = _DashNestBumper.b()
..initialPosition = Vector2(23.3, -46.75);
final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66);
await addAll([
signpost,
smallLeftNest,
smallRightNest,
bigNest,
dashAnimatronic,
]);
}
} }
class _FlutterForestController extends ComponentController<FlutterForest> class _FlutterForestController extends ComponentController<FlutterForest>
@ -76,27 +76,3 @@ class _FlutterForestController extends ComponentController<FlutterForest>
); );
} }
} }
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _DashNestBumper extends DashNestBumper with ScorePoints {
_DashNestBumper.main() : super.main();
_DashNestBumper.a() : super.a();
_DashNestBumper.b() : super.b();
@override
int get points => 20;
}
class _DashNestBumperBallContactCallback
extends ContactCallback<DashNestBumper, Ball> {
@override
void begin(DashNestBumper dashNestBumper, _, __) {
final parent = dashNestBumper.parent;
if (parent is FlutterForest) {
parent.controller.activateBumper(dashNestBumper);
}
}
}

@ -3,7 +3,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -25,7 +24,6 @@ class GoogleWord extends Component
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
gameRef.addContactCallback(_GoogleLetterBallContactCallback());
final offsets = [ final offsets = [
Vector2(-12.92, 1.82), Vector2(-12.92, 1.82),
@ -71,13 +69,14 @@ class _GoogleWordController extends ComponentController<GoogleWord>
} }
/// Activates a [GoogleLetter] when it contacts with a [Ball]. /// Activates a [GoogleLetter] when it contacts with a [Ball].
class _GoogleLetterBallContactCallback // TODO(alestiago): Add animation behaviour.
extends ContactCallback<GoogleLetter, Ball> { // class _GoogleLetterBallContactCallback
@override // extends ContactCallback<GoogleLetter, Ball> {
void begin(GoogleLetter googleLetter, _, __) { // @override
final parent = googleLetter.parent; // void begin(GoogleLetter googleLetter, _, __) {
if (parent is GoogleWord) { // final parent = googleLetter.parent;
parent.controller.activate(googleLetter); // if (parent is GoogleWord) {
} // parent.controller.activate(googleLetter);
} // }
} // }
// }

@ -1,47 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template score_points}
/// Specifies the amount of points received on [Ball] collision.
/// {@endtemplate}
mixin ScorePoints<T extends Forge2DGame> on BodyComponent<T> {
/// {@macro score_points}
int get points;
@override
Future<void> onLoad() async {
await super.onLoad();
body.userData = this;
}
}
/// {@template ball_score_points_callbacks}
/// Adds points to the score when a [Ball] collides with a [BodyComponent] that
/// implements [ScorePoints].
/// {@endtemplate}
class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
/// {@macro ball_score_points_callbacks}
BallScorePointsCallback(PinballGame game) : _gameRef = game;
final PinballGame _gameRef;
@override
void begin(
Ball ball,
ScorePoints scorePoints,
Contact _,
) {
_gameRef.read<GameBloc>().add(Scored(points: scorePoints.points));
_gameRef.audio.score();
_gameRef.add(
ScoreText(
text: scorePoints.points.toString(),
position: ball.body.position,
),
);
}
}

@ -0,0 +1,53 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template scoring_behaviour}
///
/// {@endtemplate}
class ScoringBehaviour extends Component
with ContactCallbacks, HasGameRef<PinballGame> {
/// {@macro scoring_behaviour}
ScoringBehaviour({
required int points,
}) : _points = points;
final int _points;
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Refactor once the following is merged:
// https://github.com/flame-engine/flame/pull/1566
final parent = this.parent;
if (parent is BodyComponent) {
final userData = parent.body.userData;
if (userData is ContactCallbacks) {
userData.add(this);
} else {
parent.body.userData = this;
}
}
}
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
gameRef.read<GameBloc>().add(Scored(points: _points));
gameRef.audio.score();
gameRef.add(
ScoreText(
text: _points.toString(),
position: other.body.position,
),
);
}
}

@ -1,7 +1,6 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -17,9 +16,21 @@ class SparkyFireZone extends Blueprint {
SparkyFireZone() SparkyFireZone()
: super( : super(
components: [ components: [
_SparkyBumper.a()..initialPosition = Vector2(-22.9, -41.65), SparkyBumper.a(
_SparkyBumper.b()..initialPosition = Vector2(-21.25, -57.9), children: [
_SparkyBumper.c()..initialPosition = Vector2(-3.3, -52.55), ScoringBehaviour(points: 20),
],
)..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b(
children: [
ScoringBehaviour(points: 20),
],
)..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c(
children: [
ScoringBehaviour(points: 20),
],
)..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8), SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8),
SparkyAnimatronic()..position = Vector2(-13.8, -58.2), SparkyAnimatronic()..position = Vector2(-13.8, -58.2),
], ],
@ -29,48 +40,27 @@ class SparkyFireZone extends Blueprint {
); );
} }
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _SparkyBumper extends SparkyBumper with ScorePoints {
_SparkyBumper.a() : super.a();
_SparkyBumper.b() : super.b();
_SparkyBumper.c() : super.c();
@override
int get points => 20;
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Revisit once this has been merged:
// https://github.com/flame-engine/flame/pull/1547
gameRef.addContactCallback(SparkyBumperBallContactCallback());
}
}
/// Listens when a [Ball] bounces bounces against a [SparkyBumper]. /// Listens when a [Ball] bounces bounces against a [SparkyBumper].
@visibleForTesting // TODO(alestiago): Add animation behaviour.
class SparkyBumperBallContactCallback // @visibleForTesting
extends ContactCallback<SparkyBumper, Ball> { // class SparkyBumperBallContactCallback
@override // extends ContactCallback<SparkyBumper, Ball> {
void begin( // @override
SparkyBumper sparkyBumper, // void begin(
Ball _, // SparkyBumper sparkyBumper,
Contact __, // Ball _,
) { // Contact __,
sparkyBumper.animate(); // ) {
} // sparkyBumper.animate();
} // }
// }
/// {@template sparky_computer_sensor} /// {@template sparky_computer_sensor}
/// Small sensor body used to detect when a ball has entered the /// Small sensor body used to detect when a ball has entered the
/// [SparkyComputer]. /// [SparkyComputer].
/// {@endtemplate} /// {@endtemplate}
// TODO(alestiago): Revisit once this has been merged: class SparkyComputerSensor extends BodyComponent
// https://github.com/flame-engine/flame/pull/1547 with InitialPosition, ContactCallbacks {
class SparkyComputerSensor extends BodyComponent with InitialPosition {
/// {@macro sparky_computer_sensor} /// {@macro sparky_computer_sensor}
SparkyComputerSensor() { SparkyComputerSensor() {
renderBody = false; renderBody = false;
@ -88,23 +78,11 @@ class SparkyComputerSensor extends BodyComponent with InitialPosition {
} }
@override @override
Future<void> onLoad() async { void beginContact(Object other, Contact contact) {
await super.onLoad(); super.beginContact(other, contact);
// TODO(alestiago): Revisit once this has been merged: if (other is! ControlledBall) return;
// https://github.com/flame-engine/flame/pull/1547
gameRef.addContactCallback(SparkyComputerSensorBallContactCallback());
}
}
@visibleForTesting other.controller.turboCharge();
// TODO(alestiago): Revisit once this has been merged: gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
// https://github.com/flame-engine/flame/pull/1547
// ignore: public_member_api_docs
class SparkyComputerSensorBallContactCallback
extends ContactCallback<SparkyComputerSensor, ControlledBall> {
@override
void begin(_, ControlledBall controlledBall, __) {
controlledBall.controller.turboCharge();
controlledBall.gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
} }
} }

@ -42,25 +42,19 @@ class Wall extends BodyComponent {
/// {@template bottom_wall} /// {@template bottom_wall}
/// [Wall] located at the bottom of the board. /// [Wall] located at the bottom of the board.
/// ///
/// Collisions with [BottomWall] are listened by
/// [BottomWallBallContactCallback].
/// {@endtemplate} /// {@endtemplate}
class BottomWall extends Wall { class BottomWall extends Wall with ContactCallbacks {
/// {@macro bottom_wall} /// {@macro bottom_wall}
BottomWall() BottomWall()
: super( : super(
start: BoardDimensions.bounds.bottomLeft.toVector2(), start: BoardDimensions.bounds.bottomLeft.toVector2(),
end: BoardDimensions.bounds.bottomRight.toVector2(), end: BoardDimensions.bounds.bottomRight.toVector2(),
); );
}
/// {@template bottom_wall_ball_contact_callback}
/// Listens when a [ControlledBall] falls into a [BottomWall].
/// {@endtemplate}
class BottomWallBallContactCallback
extends ContactCallback<ControlledBall, BottomWall> {
@override @override
void begin(ControlledBall ball, BottomWall wall, Contact contact) { void beginContact(Object other, Contact contact) {
ball.controller.lost(); super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.lost();
} }
} }

@ -41,8 +41,6 @@ class PinballGame extends Forge2DGame
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
_addContactCallbacks();
unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this))); unawaited(add(CameraController(this)));
unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); unawaited(add(Backboard.waiting(position: Vector2(0, -88))));
@ -74,11 +72,6 @@ class PinballGame extends Forge2DGame
await super.onLoad(); await super.onLoad();
} }
void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback(this));
addContactCallback(BottomWallBallContactCallback());
}
Future<void> _addBonusWord() async { Future<void> _addBonusWord() async {
await add( await add(
GoogleWord( GoogleWord(

@ -15,6 +15,7 @@ class AlienBumper extends BodyComponent with InitialPosition {
required double minorRadius, required double minorRadius,
required String onAssetPath, required String onAssetPath,
required String offAssetPath, required String offAssetPath,
Iterable<Component>? children,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
_minorRadius = minorRadius, _minorRadius = minorRadius,
super( super(
@ -24,27 +25,32 @@ class AlienBumper extends BodyComponent with InitialPosition {
onAssetPath: onAssetPath, onAssetPath: onAssetPath,
offAssetPath: offAssetPath, offAssetPath: offAssetPath,
), ),
if (children != null) ...children,
], ],
) { ) {
renderBody = false; renderBody = false;
} }
/// {@macro alien_bumper} /// {@macro alien_bumper}
AlienBumper.a() AlienBumper.a({
: this._( Iterable<Component>? children,
}) : this._(
majorRadius: 3.52, majorRadius: 3.52,
minorRadius: 2.97, minorRadius: 2.97,
onAssetPath: Assets.images.alienBumper.a.active.keyName, onAssetPath: Assets.images.alienBumper.a.active.keyName,
offAssetPath: Assets.images.alienBumper.a.inactive.keyName, offAssetPath: Assets.images.alienBumper.a.inactive.keyName,
children: children,
); );
/// {@macro alien_bumper} /// {@macro alien_bumper}
AlienBumper.b() AlienBumper.b({
: this._( Iterable<Component>? children,
}) : this._(
majorRadius: 3.19, majorRadius: 3.19,
minorRadius: 2.79, minorRadius: 2.79,
onAssetPath: Assets.images.alienBumper.b.active.keyName, onAssetPath: Assets.images.alienBumper.b.active.keyName,
offAssetPath: Assets.images.alienBumper.b.inactive.keyName, offAssetPath: Assets.images.alienBumper.b.inactive.keyName,
children: children,
); );
final double _majorRadius; final double _majorRadius;

@ -16,6 +16,7 @@ class DashNestBumper extends BodyComponent with InitialPosition {
required String activeAssetPath, required String activeAssetPath,
required String inactiveAssetPath, required String inactiveAssetPath,
required Vector2 spritePosition, required Vector2 spritePosition,
Iterable<Component>? children,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
_minorRadius = minorRadius, _minorRadius = minorRadius,
super( super(
@ -26,39 +27,46 @@ class DashNestBumper extends BodyComponent with InitialPosition {
inactiveAssetPath: inactiveAssetPath, inactiveAssetPath: inactiveAssetPath,
position: spritePosition, position: spritePosition,
), ),
if (children != null) ...children,
], ],
) { ) {
renderBody = false; renderBody = false;
} }
/// {@macro dash_nest_bumper} /// {@macro dash_nest_bumper}
DashNestBumper.main() DashNestBumper.main({
: this._( Iterable<Component>? children,
}) : this._(
majorRadius: 5.1, majorRadius: 5.1,
minorRadius: 3.75, minorRadius: 3.75,
activeAssetPath: Assets.images.dash.bumper.main.active.keyName, activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spritePosition: Vector2(0, -0.3), spritePosition: Vector2(0, -0.3),
children: children,
); );
/// {@macro dash_nest_bumper} /// {@macro dash_nest_bumper}
DashNestBumper.a() DashNestBumper.a({
: this._( Iterable<Component>? children,
}) : this._(
majorRadius: 3, majorRadius: 3,
minorRadius: 2.5, minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.a.active.keyName, activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spritePosition: Vector2(0.35, -1.2), spritePosition: Vector2(0.35, -1.2),
children: children,
); );
/// {@macro dash_nest_bumper} /// {@macro dash_nest_bumper}
DashNestBumper.b() DashNestBumper.b({
: this._( Iterable<Component>? children,
}) : this._(
majorRadius: 3, majorRadius: 3,
minorRadius: 2.5, minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.b.active.keyName, activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spritePosition: Vector2(0.35, -1.2), spritePosition: Vector2(0.35, -1.2),
children: children,
); );
final double _majorRadius; final double _majorRadius;

@ -107,13 +107,6 @@ class _LaunchRampBase extends BodyComponent with Layered {
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef
.addContactCallback(LayerSensorBallContactCallback<_LaunchRampExit>());
}
} }
class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {

@ -23,7 +23,8 @@ enum LayerEntranceOrientation {
/// By default the base [layer] is set to [Layer.board] and the /// By default the base [layer] is set to [Layer.board] and the
/// [outsidePriority] is set to the lowest possible [Layer]. /// [outsidePriority] is set to the lowest possible [Layer].
/// {@endtemplate} /// {@endtemplate}
abstract class LayerSensor extends BodyComponent with InitialPosition, Layered { abstract class LayerSensor extends BodyComponent
with InitialPosition, Layered, ContactCallbacks {
/// {@macro layer_sensor} /// {@macro layer_sensor}
LayerSensor({ LayerSensor({
required Layer insideLayer, required Layer insideLayer,
@ -75,35 +76,29 @@ abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
return world.createBody(bodyDef)..createFixture(fixtureDef); 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 @override
void begin(Ball ball, LayerEntrance layerEntrance, Contact _) { void beginContact(Object other, Contact contact) {
if (ball.layer != layerEntrance.insideLayer) { super.beginContact(other, contact);
if (other is! Ball) return;
if (other.layer != insideLayer) {
final isBallEnteringOpening = final isBallEnteringOpening =
(layerEntrance.orientation == LayerEntranceOrientation.down && (orientation == LayerEntranceOrientation.down &&
ball.body.linearVelocity.y < 0) || other.body.linearVelocity.y < 0) ||
(layerEntrance.orientation == LayerEntranceOrientation.up && (orientation == LayerEntranceOrientation.up &&
ball.body.linearVelocity.y > 0); other.body.linearVelocity.y > 0);
if (isBallEnteringOpening) { if (isBallEnteringOpening) {
ball other
..layer = layerEntrance.insideLayer ..layer = insideLayer
..priority = layerEntrance.insidePriority ..priority = insidePriority
..reorderChildren(); ..reorderChildren();
} }
} else { } else {
ball other
..layer = layerEntrance.outsideLayer ..layer = outsideLayer
..priority = layerEntrance.outsidePriority ..priority = outsidePriority
..reorderChildren(); ..reorderChildren();
} }
} }

@ -46,10 +46,14 @@ extension on SignpostSpriteState {
/// {@endtemplate} /// {@endtemplate}
class Signpost extends BodyComponent with InitialPosition { class Signpost extends BodyComponent with InitialPosition {
/// {@macro signpost} /// {@macro signpost}
Signpost() Signpost({
: super( Iterable<Component>? children,
}) : super(
priority: RenderPriority.signpost, priority: RenderPriority.signpost,
children: [_SignpostSpriteComponent()], children: [
_SignpostSpriteComponent(),
if (children != null) ...children,
],
) { ) {
renderBody = false; renderBody = false;
} }

@ -50,19 +50,6 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
renderBody = false; renderBody = false;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef
..addContactCallback(
LayerSensorBallContactCallback<_SpaceshipEntrance>(),
)
..addContactCallback(
LayerSensorBallContactCallback<_SpaceshipHole>(),
);
}
@override @override
Body createBody() { Body createBody() {
final shape = CircleShape()..radius = 3; final shape = CircleShape()..radius = 3;

@ -114,14 +114,6 @@ class _SpaceshipRailRamp extends BodyComponent with Layered {
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(
LayerSensorBallContactCallback<_SpaceshipRailExit>(),
);
}
} }
class _SpaceshipRailRampSpriteComponent extends SpriteComponent class _SpaceshipRailRampSpriteComponent extends SpriteComponent

@ -145,14 +145,6 @@ class _SpaceshipRampBackground extends BodyComponent
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(
LayerSensorBallContactCallback<_SpaceshipRampOpening>(),
);
}
} }
class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent

@ -16,6 +16,7 @@ class SparkyBumper extends BodyComponent with InitialPosition {
required String onAssetPath, required String onAssetPath,
required String offAssetPath, required String offAssetPath,
required Vector2 spritePosition, required Vector2 spritePosition,
Iterable<Component>? children,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
_minorRadius = minorRadius, _minorRadius = minorRadius,
super( super(
@ -26,39 +27,46 @@ class SparkyBumper extends BodyComponent with InitialPosition {
offAssetPath: offAssetPath, offAssetPath: offAssetPath,
position: spritePosition, position: spritePosition,
), ),
if (children != null) ...children,
], ],
) { ) {
renderBody = false; renderBody = false;
} }
/// {@macro sparky_bumper} /// {@macro sparky_bumper}
SparkyBumper.a() SparkyBumper.a({
: this._( Iterable<Component>? children,
}) : this._(
majorRadius: 2.9, majorRadius: 2.9,
minorRadius: 2.1, minorRadius: 2.1,
onAssetPath: Assets.images.sparky.bumper.a.active.keyName, onAssetPath: Assets.images.sparky.bumper.a.active.keyName,
offAssetPath: Assets.images.sparky.bumper.a.inactive.keyName, offAssetPath: Assets.images.sparky.bumper.a.inactive.keyName,
spritePosition: Vector2(0, -0.25), spritePosition: Vector2(0, -0.25),
children: children,
); );
/// {@macro sparky_bumper} /// {@macro sparky_bumper}
SparkyBumper.b() SparkyBumper.b({
: this._( Iterable<Component>? children,
}) : this._(
majorRadius: 2.85, majorRadius: 2.85,
minorRadius: 2, minorRadius: 2,
onAssetPath: Assets.images.sparky.bumper.b.active.keyName, onAssetPath: Assets.images.sparky.bumper.b.active.keyName,
offAssetPath: Assets.images.sparky.bumper.b.inactive.keyName, offAssetPath: Assets.images.sparky.bumper.b.inactive.keyName,
spritePosition: Vector2(0, -0.35), spritePosition: Vector2(0, -0.35),
children: children,
); );
/// {@macro sparky_bumper} /// {@macro sparky_bumper}
SparkyBumper.c() SparkyBumper.c({
: this._( Iterable<Component>? children,
}) : this._(
majorRadius: 3, majorRadius: 3,
minorRadius: 2.2, minorRadius: 2.2,
onAssetPath: Assets.images.sparky.bumper.c.active.keyName, onAssetPath: Assets.images.sparky.bumper.c.active.keyName,
offAssetPath: Assets.images.sparky.bumper.c.inactive.keyName, offAssetPath: Assets.images.sparky.bumper.c.inactive.keyName,
spritePosition: Vector2(0, -0.4), spritePosition: Vector2(0, -0.4),
children: children,
); );
final double _majorRadius; final double _majorRadius;

@ -8,7 +8,11 @@ environment:
dependencies: dependencies:
flame: ^1.1.1 flame: ^1.1.1
flame_forge2d: ^0.11.0 flame_forge2d:
git:
url: https://github.com/flame-engine/flame/
path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:
sdk: flutter sdk: flutter
geometry: geometry:

@ -17,7 +17,6 @@ class GoogleLetterGame extends BallGame {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
addContactCallback(_BallGoogleLetterContactCallback());
camera.followVector2(Vector2.zero()); camera.followVector2(Vector2.zero());
await add(GoogleLetter(0)); await add(GoogleLetter(0));
@ -25,12 +24,3 @@ class GoogleLetterGame extends BallGame {
await traceAllBodies(); await traceAllBodies();
} }
} }
class _BallGoogleLetterContactCallback
extends ContactCallback<Ball, GoogleLetter> {
@override
void begin(Ball<Forge2DGame> a, GoogleLetter b, Contact contact) {
super.begin(a, b, contact);
b.activate();
}
}

@ -102,9 +102,11 @@ packages:
flame_forge2d: flame_forge2d:
dependency: "direct main" dependency: "direct main"
description: description:
name: flame_forge2d path: "packages/flame_forge2d"
url: "https://pub.dartlang.org" ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
source: hosted resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
url: "https://github.com/flame-engine/flame/"
source: git
version: "0.11.0" version: "0.11.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
@ -169,7 +171,7 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.4"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@ -197,7 +199,7 @@ packages:
name: material_color_utilities name: material_color_utilities
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.3" version: "0.1.4"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -218,7 +220,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -349,7 +351,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -384,7 +386,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.8" version: "0.4.9"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -454,7 +456,7 @@ packages:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
very_good_analysis: very_good_analysis:
dependency: "direct dev" dependency: "direct dev"
description: description:

@ -9,7 +9,11 @@ environment:
dependencies: dependencies:
dashbook: ^0.1.7 dashbook: ^0.1.7
flame: ^1.1.1 flame: ^1.1.1
flame_forge2d: ^0.11.0 flame_forge2d:
git:
url: https://github.com/flame-engine/flame/
path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:
sdk: flutter sdk: flutter
pinball_components: pinball_components:

@ -15,7 +15,4 @@ class MockGame extends Mock implements Forge2DGame {}
class MockContact extends Mock implements Contact {} class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock
implements ContactCallback<Object, Object> {}
class MockComponent extends Mock implements Component {} class MockComponent extends Mock implements Component {}

@ -22,11 +22,6 @@ class TestLayerSensor extends LayerSensor {
Shape get shape => PolygonShape()..setAsBoxXY(1, 1); Shape get shape => PolygonShape()..setAsBoxXY(1, 1);
} }
class TestLayerSensorBallContactCallback
extends LayerSensorBallContactCallback<TestLayerSensor> {
TestLayerSensorBallContactCallback() : super();
}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
@ -135,18 +130,15 @@ void main() {
insidePriority: insidePriority, insidePriority: insidePriority,
insideLayer: Layer.spaceshipEntranceRamp, insideLayer: Layer.spaceshipEntranceRamp,
)..initialPosition = Vector2(0, 10); )..initialPosition = Vector2(0, 10);
final callback = TestLayerSensorBallContactCallback();
when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = sensor.insideLayer).called(1); verify(() => ball.layer = sensor.insideLayer).called(1);
verify(() => ball.priority = sensor.insidePriority).called(1); verify(() => ball.priority = sensor.insidePriority).called(1);
verify(ball.reorderChildren).called(1); verify(ball.reorderChildren).called(1);
when(() => ball.layer).thenReturn(sensor.insideLayer); when(() => ball.layer).thenReturn(sensor.insideLayer);
callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = Layer.board); verify(() => ball.layer = Layer.board);
verify(() => ball.priority = RenderPriority.ballOnBoard).called(1); verify(() => ball.priority = RenderPriority.ballOnBoard).called(1);
verify(ball.reorderChildren).called(1); verify(ball.reorderChildren).called(1);
@ -161,18 +153,15 @@ void main() {
insidePriority: insidePriority, insidePriority: insidePriority,
insideLayer: Layer.spaceshipEntranceRamp, insideLayer: Layer.spaceshipEntranceRamp,
)..initialPosition = Vector2(0, 10); )..initialPosition = Vector2(0, 10);
final callback = TestLayerSensorBallContactCallback();
when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = sensor.insideLayer).called(1); verify(() => ball.layer = sensor.insideLayer).called(1);
verify(() => ball.priority = sensor.insidePriority).called(1); verify(() => ball.priority = sensor.insidePriority).called(1);
verify(ball.reorderChildren).called(1); verify(ball.reorderChildren).called(1);
when(() => ball.layer).thenReturn(sensor.insideLayer); when(() => ball.layer).thenReturn(sensor.insideLayer);
callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = Layer.board); verify(() => ball.layer = Layer.board);
verify(() => ball.priority = RenderPriority.ballOnBoard).called(1); verify(() => ball.priority = RenderPriority.ballOnBoard).called(1);
verify(ball.reorderChildren).called(1); verify(ball.reorderChildren).called(1);

@ -2,5 +2,6 @@ library pinball_flame;
export 'src/blueprint.dart'; export 'src/blueprint.dart';
export 'src/component_controller.dart'; export 'src/component_controller.dart';
export 'src/contacts_callbacks_adder.dart';
export 'src/keyboard_input_controller.dart'; export 'src/keyboard_input_controller.dart';
export 'src/sprite_animation.dart'; export 'src/sprite_animation.dart';

@ -0,0 +1,38 @@
import 'package:flame_forge2d/flame_forge2d.dart';
/// {@template contact_callbacks_adder}
///
/// {@endtemplate}
// TODO(alestiago): Consider adding streams to [ContactCallbacks].
extension ContactCallbacksAdder on ContactCallbacks {
/// {@macro contact_callbacks_adder}
void add(ContactCallbacks contactCallbacks) {
if (contactCallbacks.onBeginContact != null) {
onBeginContact = (other, contact) {
onBeginContact?.call(other, contact);
contactCallbacks.beginContact(other, contact);
};
}
if (contactCallbacks.onEndContact != null) {
onEndContact = (other, contact) {
onEndContact?.call(other, contact);
contactCallbacks.endContact(other, contact);
};
}
if (contactCallbacks.onPreSolve != null) {
onPreSolve = (other, contact, oldManifold) {
onPreSolve?.call(other, contact, oldManifold);
contactCallbacks.preSolve(other, contact, oldManifold);
};
}
if (contactCallbacks.onPostSolve != null) {
onPostSolve = (other, contact, impulse) {
onPostSolve?.call(other, contact, impulse);
contactCallbacks.postSolve(other, contact, impulse);
};
}
}
}

@ -8,7 +8,11 @@ environment:
dependencies: dependencies:
flame: ^1.1.1 flame: ^1.1.1
flame_forge2d: ^0.11.0 flame_forge2d:
git:
url: https://github.com/flame-engine/flame/
path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:
sdk: flutter sdk: flutter

@ -4,7 +4,4 @@ import 'package:mocktail/mocktail.dart';
class MockForge2DGame extends Mock implements Forge2DGame {} class MockForge2DGame extends Mock implements Forge2DGame {}
class MockContactCallback extends Mock
implements ContactCallback<dynamic, dynamic> {}
class MockComponent extends Mock implements Component {} class MockComponent extends Mock implements Component {}

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared name: _fe_analyzer_shared
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "31.0.0" version: "39.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.0" version: "4.0.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -71,13 +71,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -214,9 +207,11 @@ packages:
flame_forge2d: flame_forge2d:
dependency: "direct main" dependency: "direct main"
description: description:
name: flame_forge2d path: "packages/flame_forge2d"
url: "https://pub.dartlang.org" ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
source: hosted resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
url: "https://github.com/flame-engine/flame/"
source: git
version: "0.11.0" version: "0.11.0"
flame_test: flame_test:
dependency: "direct dev" dependency: "direct dev"
@ -321,7 +316,7 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.4"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@ -356,7 +351,7 @@ packages:
name: material_color_utilities name: material_color_utilities
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.3" version: "0.1.4"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -419,7 +414,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.1"
path_provider: path_provider:
dependency: transitive dependency: transitive
description: description:
@ -592,7 +587,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -634,21 +629,21 @@ packages:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.19.5" version: "1.21.1"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.8" version: "0.4.9"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.9" version: "0.4.13"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -669,7 +664,7 @@ packages:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
very_good_analysis: very_good_analysis:
dependency: "direct dev" dependency: "direct dev"
description: description:

@ -12,7 +12,11 @@ dependencies:
equatable: ^2.0.3 equatable: ^2.0.3
flame: ^1.1.1 flame: ^1.1.1
flame_bloc: ^1.2.0 flame_bloc: ^1.2.0
flame_forge2d: ^0.11.0 flame_forge2d:
git:
url: https://github.com/flame-engine/flame/
path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: ^8.0.1 flutter_bloc: ^8.0.1

@ -1,11 +1,8 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -19,7 +16,9 @@ void main() {
Assets.images.alienBumper.b.active.keyName, Assets.images.alienBumper.b.active.keyName,
Assets.images.alienBumper.b.inactive.keyName, Assets.images.alienBumper.b.inactive.keyName,
]; ];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('AlienZone', () { group('AlienZone', () {
flameTester.test( flameTester.test(
@ -64,41 +63,6 @@ void main() {
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
assets: assets, assets: assets,
); );
flameTester.test('call animate on contact', (game) async {
final contactCallback = AlienBumperBallContactCallback();
final bumper = MockAlienBumper();
final ball = MockBall();
when(bumper.animate).thenAnswer((_) async {});
contactCallback.begin(bumper, ball, MockContact());
verify(bumper.animate).called(1);
});
flameBlocTester.testGameWidget(
'add Scored event',
setUp: (game, tester) async {
final ball = Ball(baseColor: const Color(0xFF00FFFF));
final alienZone = AlienZone();
await game.ensureAdd(alienZone);
await game.ensureAdd(ball);
game.addContactCallback(BallScorePointsCallback(game));
final bumpers = alienZone.descendants().whereType<ScorePoints>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
verify(
() => gameBloc.add(
Scored(points: bumper.points),
),
).called(1);
}
},
);
}); });
}); });
} }

@ -26,7 +26,9 @@ void main() {
Assets.images.flipper.left.keyName, Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName, Assets.images.flipper.right.keyName,
]; ];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('Board', () { group('Board', () {
flameTester.test( flameTester.test(

@ -15,7 +15,9 @@ void main() {
Assets.images.flipper.left.keyName, Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName, Assets.images.flipper.right.keyName,
]; ];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>( final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new, gameBuilder: EmptyPinballTestGame.new,

@ -25,7 +25,9 @@ void main() {
Assets.images.signpost.active2.keyName, Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName, Assets.images.signpost.active3.keyName,
]; ];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('FlutterForest', () { group('FlutterForest', () {
flameTester.test( flameTester.test(
@ -88,7 +90,7 @@ void main() {
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: () => EmptyPinballTestGame(assets), gameBuilder: () => EmptyPinballTestGame(assets: assets),
blocBuilder: () { blocBuilder: () {
gameBloc = MockGameBloc(); gameBloc = MockGameBloc();
const state = GameState.initial(); const state = GameState.initial();
@ -98,29 +100,6 @@ void main() {
assets: assets, assets: assets,
); );
flameBlocTester.testGameWidget(
'add Scored event',
setUp: (game, tester) async {
final flutterForest = FlutterForest();
await game.ensureAddAll([
flutterForest,
ball,
]);
game.addContactCallback(BallScorePointsCallback(game));
final bumpers = flutterForest.descendants().whereType<ScorePoints>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
verify(
() => gameBloc.add(
Scored(points: bumper.points),
),
).called(1);
}
},
);
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'adds GameBonus.dashNest to the game when 3 bumpers are activated', 'adds GameBonus.dashNest to the game when 3 bumpers are activated',
setUp: (game, _) async { setUp: (game, _) async {

@ -1,105 +0,0 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
class FakeScorePoints extends BodyComponent with ScorePoints {
@override
Body createBody() {
throw UnimplementedError();
}
@override
int get points => 2;
}
void main() {
group('BallScorePointsCallback', () {
late PinballGame game;
late GameBloc bloc;
late PinballAudio audio;
late Ball ball;
late FakeScorePoints fakeScorePoints;
setUp(() {
game = MockPinballGame();
bloc = MockGameBloc();
audio = MockPinballAudio();
fakeScorePoints = FakeScorePoints();
ball = MockBall();
final ballBody = MockBody();
when(() => ball.body).thenReturn(ballBody);
when(() => ballBody.position).thenReturn(Vector2.all(4));
});
setUpAll(() {
registerFallbackValue(FakeGameEvent());
});
group('begin', () {
test(
'emits Scored event with points',
() {
when(game.read<GameBloc>).thenReturn(bloc);
when(() => game.audio).thenReturn(audio);
BallScorePointsCallback(game).begin(
ball,
fakeScorePoints,
FakeContact(),
);
verify(
() => bloc.add(
Scored(points: fakeScorePoints.points),
),
).called(1);
},
);
test(
'plays a Score sound',
() {
when(game.read<GameBloc>).thenReturn(bloc);
when(() => game.audio).thenReturn(audio);
BallScorePointsCallback(game).begin(
ball,
fakeScorePoints,
FakeContact(),
);
verify(audio.score).called(1);
},
);
test(
"adds a ScoreText component at Ball's position",
() {
when(game.read<GameBloc>).thenReturn(bloc);
when(() => game.audio).thenReturn(audio);
BallScorePointsCallback(game).begin(
ball,
fakeScorePoints,
FakeContact(),
);
verify(
() => game.add(
ScoreText(
text: fakeScorePoints.points.toString(),
position: ball.body.position,
),
),
).called(1);
},
);
});
});
}

@ -0,0 +1,100 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
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/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('ScoringBehaviour', () {
group('beginContact', () {
late GameBloc bloc;
late PinballAudio audio;
late Ball ball;
setUp(() {
audio = MockPinballAudio();
ball = MockBall();
final ballBody = MockBody();
when(() => ball.body).thenReturn(ballBody);
when(() => ballBody.position).thenReturn(Vector2.all(4));
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: () => EmptyPinballTestGame(
audio: audio,
),
blocBuilder: () {
bloc = MockGameBloc();
const state = GameState(
score: 0,
balls: 0,
bonusHistory: [],
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
);
flameBlocTester.testGameWidget(
'emits Scored event with points',
setUp: (game, tester) async {
const points = 20;
final scoringBehaviour = ScoringBehaviour(points: points);
await game.ensureAdd(scoringBehaviour);
scoringBehaviour.beginContact(ball, MockContact());
verify(
() => bloc.add(
const Scored(points: points),
),
).called(1);
},
);
flameBlocTester.testGameWidget(
'plays score sound',
setUp: (game, tester) async {
const points = 20;
final scoringBehaviour = ScoringBehaviour(points: points);
await game.ensureAdd(scoringBehaviour);
scoringBehaviour.beginContact(ball, MockContact());
verify(audio.score).called(1);
},
);
flameBlocTester.testGameWidget(
"adds a ScoreText component at Ball's position with points",
setUp: (game, tester) async {
const points = 20;
final scoringBehaviour = ScoringBehaviour(points: points);
await game.ensureAdd(scoringBehaviour);
scoringBehaviour.beginContact(ball, MockContact());
await game.ready();
final scoreText = game.descendants().whereType<ScoreText>();
expect(scoreText.length, equals(1));
expect(
scoreText.first.text,
equals(points.toString()),
);
expect(
scoreText.first.position,
equals(ball.body.position),
);
},
);
});
});
}

@ -1,7 +1,5 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -23,7 +21,10 @@ void main() {
Assets.images.sparky.bumper.c.inactive.keyName, Assets.images.sparky.bumper.c.inactive.keyName,
Assets.images.sparky.animatronic.keyName, Assets.images.sparky.animatronic.keyName,
]; ];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets));
final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('SparkyFireZone', () { group('SparkyFireZone', () {
flameTester.test('loads correctly', (game) async { flameTester.test('loads correctly', (game) async {
@ -88,59 +89,21 @@ void main() {
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
assets: assets, assets: assets,
); );
flameTester.test('call animate on contact', (game) async {
final contactCallback = SparkyBumperBallContactCallback();
final bumper = MockSparkyBumper();
final ball = MockBall();
when(bumper.animate).thenAnswer((_) async {});
contactCallback.begin(bumper, ball, MockContact());
verify(bumper.animate).called(1);
});
flameBlocTester.testGameWidget(
'add Scored event',
setUp: (game, tester) async {
final ball = Ball(baseColor: const Color(0xFF00FFFF));
final sparkyFireZone = SparkyFireZone();
await game.addFromBlueprint(sparkyFireZone);
await game.ensureAdd(ball);
game.addContactCallback(BallScorePointsCallback(game));
final bumpers = sparkyFireZone.components.whereType<ScorePoints>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
verify(
() => gameBloc.add(
Scored(points: bumper.points),
),
).called(1);
}
},
);
}); });
}); });
group('SparkyTurboChargeSensorBallContactCallback', () { group('SparkyTurboChargeSensorBallContactCallback', () {
flameTester.test('calls turboCharge', (game) async { flameTester.test('calls turboCharge', (game) async {
final callback = SparkyComputerSensorBallContactCallback();
final ball = MockControlledBall(); final ball = MockControlledBall();
final controller = MockBallController(); final controller = MockBallController();
when(() => ball.controller).thenReturn(controller); when(() => ball.controller).thenReturn(controller);
when(() => ball.gameRef).thenReturn(game); when(() => ball.gameRef).thenReturn(game);
when(controller.turboCharge).thenAnswer((_) async {}); when(controller.turboCharge).thenAnswer((_) async {});
callback.begin(MockSparkyComputerSensor(), ball, MockContact());
verify(() => ball.controller.turboCharge()).called(1); verify(() => ball.controller.turboCharge()).called(1);
}); });
flameTester.test('plays SparkyAnimatronic', (game) async { flameTester.test('plays SparkyAnimatronic', (game) async {
final callback = SparkyComputerSensorBallContactCallback();
final ball = MockControlledBall(); final ball = MockControlledBall();
final controller = MockBallController(); final controller = MockBallController();
when(() => ball.controller).thenReturn(controller); when(() => ball.controller).thenReturn(controller);
@ -155,7 +118,6 @@ void main() {
sparkyFireZone.components.whereType<SparkyAnimatronic>().single; sparkyFireZone.components.whereType<SparkyAnimatronic>().single;
expect(sparkyAnimatronic.playing, isFalse); expect(sparkyAnimatronic.playing, isFalse);
callback.begin(MockSparkyComputerSensor(), ball, MockContact());
expect(sparkyAnimatronic.playing, isTrue); expect(sparkyAnimatronic.playing, isTrue);
}); });

@ -122,7 +122,6 @@ void main() {
); );
final wall = BottomWall(); final wall = BottomWall();
await game.ensureAddAll([ball, wall]); await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall); beginContact(game, ball, wall);
await game.ready(); await game.ready();
@ -139,7 +138,6 @@ void main() {
); );
final wall = BottomWall(); final wall = BottomWall();
await game.ensureAddAll([ball, wall]); await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall); beginContact(game, ball, wall);
await game.ready(); await game.ready();
@ -154,7 +152,6 @@ void main() {
final ball = ControlledBall.debug(); final ball = ControlledBall.debug();
final wall = BottomWall(); final wall = BottomWall();
await game.ensureAddAll([ball, wall]); await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall); beginContact(game, ball, wall);
await game.ready(); await game.ready();

@ -57,13 +57,14 @@ void main() {
Assets.images.dino.dinoLandTop.keyName, Assets.images.dino.dinoLandTop.keyName,
Assets.images.dino.dinoLandBottom.keyName, Assets.images.dino.dinoLandBottom.keyName,
]; ];
final flameTester = FlameTester(() => PinballTestGame(assets)); final flameTester = FlameTester(
final debugModeFlameTester = FlameTester(() => DebugPinballTestGame(assets)); () => PinballTestGame(assets: assets),
);
final debugModeFlameTester = FlameTester(
() => DebugPinballTestGame(assets: assets),
);
group('PinballGame', () { group('PinballGame', () {
// TODO(alestiago): test if [PinballGame] registers
// [BallScorePointsCallback] once the following issue is resolved:
// https://github.com/flame-engine/flame/issues/1416
group('components', () { group('components', () {
flameTester.test( flameTester.test(
'has only one BottomWall', 'has only one BottomWall',

@ -29,9 +29,6 @@ class MockBallController extends Mock implements BallController {}
class MockContact extends Mock implements Contact {} class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock
implements ContactCallback<Object, Object> {}
class MockGameBloc extends Mock implements GameBloc {} class MockGameBloc extends Mock implements GameBloc {}
class MockStartGameBloc extends Mock implements StartGameBloc {} class MockStartGameBloc extends Mock implements StartGameBloc {}

@ -5,6 +5,7 @@ import 'dart:async';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import 'helpers.dart'; import 'helpers.dart';
@ -16,11 +17,14 @@ class TestGame extends Forge2DGame with FlameBloc {
} }
class PinballTestGame extends PinballGame { class PinballTestGame extends PinballGame {
PinballTestGame([List<String>? assets]) PinballTestGame({
: _assets = assets, List<String>? assets,
PinballAudio? audio,
CharacterTheme? theme,
}) : _assets = assets,
super( super(
audio: MockPinballAudio(), audio: audio ?? MockPinballAudio(),
characterTheme: const DashTheme(), characterTheme: theme ?? const DashTheme(),
); );
final List<String>? _assets; final List<String>? _assets;
@ -34,11 +38,14 @@ class PinballTestGame extends PinballGame {
} }
class DebugPinballTestGame extends DebugPinballGame { class DebugPinballTestGame extends DebugPinballGame {
DebugPinballTestGame([List<String>? assets]) DebugPinballTestGame({
: _assets = assets, List<String>? assets,
PinballAudio? audio,
CharacterTheme? theme,
}) : _assets = assets,
super( super(
audio: MockPinballAudio(), audio: audio ?? MockPinballAudio(),
characterTheme: const DashTheme(), characterTheme: theme ?? const DashTheme(),
); );
final List<String>? _assets; final List<String>? _assets;
@ -53,7 +60,15 @@ class DebugPinballTestGame extends DebugPinballGame {
} }
class EmptyPinballTestGame extends PinballTestGame { class EmptyPinballTestGame extends PinballTestGame {
EmptyPinballTestGame([List<String>? assets]) : super(assets); EmptyPinballTestGame({
List<String>? assets,
PinballAudio? audio,
CharacterTheme? theme,
}) : super(
assets: assets,
audio: audio,
theme: theme,
);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {

Loading…
Cancel
Save