feat: migration to new `ContactCallbacks` (#234)

* feat: migrated to new ContactCallbacks

* refactor: moved renderBody to super call

* feat: defined AlienBumper behaviours

* feat: included ParentIsA mixin

* refactor: modified ContactCallbacksGroup

* feat: resolved missing ContactCallbacks with Behaviors

* refactor: removed unused ParentIsA

* refactor: moved tests

* fix: invalid export

* refactor: renamed behaviours

* test: tested pinball_components

* refactor: removed "Behaviour" typo

* docs: included TODO comment for generics

* docs: included flame_bloc TODO comments

* refactor: renamed ContacCallbacksGroup variable

* docs: included doc comments where possible

* docs: rephrased ContactBehaviour doc comment

* test: included ContactBehavior tests

* feat: implemented FlutterForestBonusBehavior

* refactor: fixed analyser warnings

* test: tested DashNestBumper

* refactor: moved children to last arguement

* test: included closing test

* refactor: used barrel files as imports

* docs: included flutter_bloc TODO

* test: correctly tested GoogleWordBonusBehavior

* refactor: moved flutter_forest_test.dart

* test: fixed AlienZone typo

* feat: removed FlutterForestCubit

* test: closed streams

* refactor: removed optional bloc parameter

* refactor: added flame_bloc TODO comment

* docs: included .test constructor docs

* feat: included GoogleLetter.test

* test: made blink test pass

* fix: renamed theme to CharacterTheme

* refactor: moved timer.stop();

* refactor: renamed hasBonus to achievedBonus

* refactor: ignore public_member_api_docs for cubits

* test: removed beginContact group

* refactor: typos correction

* docs: used correct AlienBumper reference

* docs: removed TODO comment from ContactBehavior subclasses

* docs: includes ScoringBehavior doc

* feat: adjusted FlutterForest priorities

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
pull/246/head
Alejandro Santiago 3 years ago committed by GitHub
parent a71afb7623
commit 2673c419b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,60 +1,29 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
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]
/// can bounce off [AlienBumper]s. /// can bounce off [AlienBumper]s.
///
/// 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: [
ScoringBehavior(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() ScoringBehavior(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].
@visibleForTesting
class AlienBumperBallContactCallback
extends ContactCallback<AlienBumper, Ball> {
@override
void begin(
AlienBumper alienBumper,
Ball _,
Contact __,
) {
alienBumper.animate();
}
} }

@ -4,10 +4,10 @@ export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
export 'controlled_plunger.dart'; export 'controlled_plunger.dart';
export 'flutter_forest.dart'; export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_flow_controller.dart';
export 'google_word.dart'; export 'google_word/google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'score_points.dart'; export 'scoring_behavior.dart';
export 'sparky_fire_zone.dart'; export 'sparky_fire_zone.dart';
export 'wall.dart'; export 'wall.dart';

@ -49,10 +49,8 @@ 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]. /// Event triggered when the ball is lost.
/// // TODO(alestiago): Refactor using behaviors.
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
void lost() { void lost() {
component.shouldRemove = true; component.shouldRemove = true;
} }

@ -1,102 +0,0 @@
// 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 flutter_forest}
/// Area positioned at the top right of the [Board] where the [Ball]
/// can bounce off [DashNestBumper]s.
///
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
/// {@endtemplate}
class FlutterForest extends Component
with Controls<_FlutterForestController>, HasGameRef<PinballGame> {
/// {@macro flutter_forest}
FlutterForest() {
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>
with HasGameRef<PinballGame> {
_FlutterForestController(FlutterForest flutterForest) : super(flutterForest);
final _activatedBumpers = <DashNestBumper>{};
void activateBumper(DashNestBumper dashNestBumper) {
if (!_activatedBumpers.add(dashNestBumper)) return;
dashNestBumper.activate();
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 {
await gameRef.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme)
..initialPosition = Vector2(17.2, -52.7),
);
}
}
// 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);
}
}
}

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

@ -0,0 +1,41 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component
with ParentIsA<FlutterForest>, HasGameRef<PinballGame> {
@override
void onMount() {
super.onMount();
final bumpers = parent.children.whereType<DashNestBumper>();
for (final bumper in bumpers) {
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
bumper.bloc.stream.listen((state) {
final achievedBonus = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active,
);
if (achievedBonus) {
gameRef
.read<GameBloc>()
.add(const BonusActivated(GameBonus.dashNest));
gameRef.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme)
..initialPosition = Vector2(17.2, -52.7),
);
parent.firstChild<DashAnimatronic>()?.playing = true;
for (final bumper in bumpers) {
bumper.bloc.onReset();
}
}
});
}
}
}

@ -0,0 +1,49 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template flutter_forest}
/// Area positioned at the top right of the [Board] where the [Ball] can bounce
/// off [DashNestBumper]s.
/// {@endtemplate}
class FlutterForest extends Component {
/// {@macro flutter_forest}
FlutterForest()
: super(
priority: RenderPriority.flutterForest,
children: [
Signpost(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(23.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66),
FlutterForestBonusBehavior(),
],
);
/// Creates a [FlutterForest] without any children.
///
/// This can be used for testing [FlutterForest]'s behaviors in isolation.
@visibleForTesting
FlutterForest.test();
}

@ -1,83 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'dart:async';
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 google_word}
/// Loads all [GoogleLetter]s to compose a [GoogleWord].
/// {@endtemplate}
class GoogleWord extends Component
with HasGameRef<PinballGame>, Controls<_GoogleWordController> {
/// {@macro google_word}
GoogleWord({
required Vector2 position,
}) : _position = position {
controller = _GoogleWordController(this);
}
final Vector2 _position;
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_GoogleLetterBallContactCallback());
final offsets = [
Vector2(-12.92, 1.82),
Vector2(-8.33, -0.65),
Vector2(-2.88, -1.75),
Vector2(2.88, -1.75),
Vector2(8.33, -0.65),
Vector2(12.92, 1.82),
];
final letters = <GoogleLetter>[];
for (var index = 0; index < offsets.length; index++) {
letters.add(
GoogleLetter(index)..initialPosition = _position + offsets[index],
);
}
await addAll(letters);
}
}
class _GoogleWordController extends ComponentController<GoogleWord>
with HasGameRef<PinballGame> {
_GoogleWordController(GoogleWord googleWord) : super(googleWord);
final _activatedLetters = <GoogleLetter>{};
void activate(GoogleLetter googleLetter) {
if (!_activatedLetters.add(googleLetter)) return;
googleLetter.activate();
final activatedBonus = _activatedLetters.length == 6;
if (activatedBonus) {
gameRef.audio.googleBonus();
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.googleWord));
component.children.whereType<GoogleLetter>().forEach(
(letter) => letter.deactivate(),
);
_activatedLetters.clear();
}
}
}
/// Activates a [GoogleLetter] when it contacts with a [Ball].
class _GoogleLetterBallContactCallback
extends ContactCallback<GoogleLetter, Ball> {
@override
void begin(GoogleLetter googleLetter, _, __) {
final parent = googleLetter.parent;
if (parent is GoogleWord) {
parent.controller.activate(googleLetter);
}
}
}

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

@ -0,0 +1,34 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated.
class GoogleWordBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<GoogleWord> {
@override
void onMount() {
super.onMount();
final googleLetters = parent.children.whereType<GoogleLetter>();
for (final letter in googleLetters) {
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
letter.bloc.stream.listen((_) {
final achievedBonus = googleLetters
.every((letter) => letter.bloc.state == GoogleLetterState.active);
if (achievedBonus) {
gameRef.audio.googleBonus();
gameRef
.read<GameBloc>()
.add(const BonusActivated(GameBonus.googleWord));
for (final letter in googleLetters) {
letter.bloc.onReset();
}
}
});
}
}
}

@ -0,0 +1,30 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/components/google_word/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template google_word}
/// Loads all [GoogleLetter]s to compose a [GoogleWord].
/// {@endtemplate}
class GoogleWord extends Component {
/// {@macro google_word}
GoogleWord({
required Vector2 position,
}) : super(
children: [
GoogleLetter(0)..initialPosition = position + Vector2(-12.92, 1.82),
GoogleLetter(1)..initialPosition = position + Vector2(-8.33, -0.65),
GoogleLetter(2)..initialPosition = position + Vector2(-2.88, -1.75),
GoogleLetter(3)..initialPosition = position + Vector2(2.88, -1.75),
GoogleLetter(4)..initialPosition = position + Vector2(8.33, -0.65),
GoogleLetter(5)..initialPosition = position + Vector2(12.92, 1.82),
GoogleWordBonusBehavior(),
],
);
/// Creates a [GoogleWord] without any children.
///
/// This can be used for testing [GoogleWord]'s behaviors in isolation.
@visibleForTesting
GoogleWord.test();
}

@ -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,34 @@
// 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_behavior}
/// Adds points to the score when the ball contacts the [parent].
/// {@endtemplate}
class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> {
/// {@macro scoring_behavior}
ScoringBehavior({
required int points,
}) : _points = points;
final int _points;
@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), ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b(
children: [
ScoringBehavior(points: 20),
],
)..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c(
children: [
ScoringBehavior(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,52 +40,14 @@ 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].
@visibleForTesting
class SparkyBumperBallContactCallback
extends ContactCallback<SparkyBumper, Ball> {
@override
void begin(
SparkyBumper sparkyBumper,
Ball _,
Contact __,
) {
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() : super(renderBody: false);
renderBody = false;
}
@override @override
Body createBody() { Body createBody() {
@ -88,23 +61,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))));
@ -55,11 +53,11 @@ class PinballGame extends Forge2DGame
final launcher = Launcher(); final launcher = Launcher();
unawaited(addFromBlueprint(launcher)); unawaited(addFromBlueprint(launcher));
unawaited(add(Board())); unawaited(add(Board()));
unawaited(add(AlienZone())); await addFromBlueprint(AlienZone());
await addFromBlueprint(SparkyFireZone()); await addFromBlueprint(SparkyFireZone());
unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls())); unawaited(addFromBlueprint(DinoWalls()));
unawaited(_addBonusWord());
unawaited(addFromBlueprint(SpaceshipRamp())); unawaited(addFromBlueprint(SpaceshipRamp()));
unawaited( unawaited(
addFromBlueprint( addFromBlueprint(
@ -69,17 +67,6 @@ class PinballGame extends Forge2DGame
), ),
); );
unawaited(addFromBlueprint(SpaceshipRail())); unawaited(addFromBlueprint(SpaceshipRail()));
controller.attachTo(launcher.components.whereType<Plunger>().first);
await super.onLoad();
}
void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback(this));
addContactCallback(BottomWallBallContactCallback());
}
Future<void> _addBonusWord() async {
await add( await add(
GoogleWord( GoogleWord(
position: Vector2( position: Vector2(
@ -88,6 +75,9 @@ class PinballGame extends Forge2DGame
), ),
), ),
); );
controller.attachTo(launcher.components.whereType<Plunger>().first);
await super.onLoad();
} }
} }

@ -4,6 +4,10 @@ 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:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/alien_bumper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/alien_bumper_cubit.dart';
/// {@template alien_bumper} /// {@template alien_bumper}
/// Bumper for area under the [Spaceship]. /// Bumper for area under the [Spaceship].
@ -15,41 +19,75 @@ 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,
required this.bloc,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
_minorRadius = minorRadius, _minorRadius = minorRadius,
super( super(
priority: RenderPriority.alienBumper, priority: RenderPriority.alienBumper,
renderBody: false,
children: [ children: [
AlienBumperBallContactBehavior(),
AlienBumperBlinkingBehavior(),
_AlienBumperSpriteGroupComponent( _AlienBumperSpriteGroupComponent(
onAssetPath: onAssetPath,
offAssetPath: offAssetPath, offAssetPath: offAssetPath,
onAssetPath: onAssetPath,
state: bloc.state,
), ),
...?children,
], ],
) { );
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,
bloc: AlienBumperCubit(),
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,
bloc: AlienBumperCubit(),
children: children,
); );
/// Creates an [AlienBumper] without any children.
///
/// This can be used for testing [AlienBumper]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
AlienBumper.test({
required this.bloc,
}) : _majorRadius = 3.52,
_minorRadius = 2.97;
final double _majorRadius; final double _majorRadius;
final double _minorRadius; final double _minorRadius;
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final AlienBumperCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override @override
Body createBody() { Body createBody() {
final shape = EllipseShape( final shape = EllipseShape(
@ -63,41 +101,25 @@ class AlienBumper extends BodyComponent with InitialPosition {
); );
final bodyDef = BodyDef( final bodyDef = BodyDef(
position: initialPosition, position: initialPosition,
userData: this,
); );
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
/// Animates the [AlienBumper].
Future<void> animate() async {
final spriteGroupComponent = firstChild<_AlienBumperSpriteGroupComponent>()
?..current = AlienBumperSpriteState.inactive;
await Future<void>.delayed(const Duration(milliseconds: 50));
spriteGroupComponent?.current = AlienBumperSpriteState.active;
}
}
/// Indicates the [AlienBumper]'s current sprite state.
@visibleForTesting
enum AlienBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
} }
class _AlienBumperSpriteGroupComponent class _AlienBumperSpriteGroupComponent
extends SpriteGroupComponent<AlienBumperSpriteState> with HasGameRef { extends SpriteGroupComponent<AlienBumperState>
with HasGameRef, ParentIsA<AlienBumper> {
_AlienBumperSpriteGroupComponent({ _AlienBumperSpriteGroupComponent({
required String onAssetPath, required String onAssetPath,
required String offAssetPath, required String offAssetPath,
required AlienBumperState state,
}) : _onAssetPath = onAssetPath, }) : _onAssetPath = onAssetPath,
_offAssetPath = offAssetPath, _offAssetPath = offAssetPath,
super( super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -0.1), position: Vector2(0, -0.1),
current: state,
); );
final String _onAssetPath; final String _onAssetPath;
@ -106,16 +128,16 @@ class _AlienBumperSpriteGroupComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
parent.bloc.stream.listen((state) => current = state);
final sprites = { final sprites = {
AlienBumperSpriteState.active: AlienBumperState.active: Sprite(
Sprite(gameRef.images.fromCache(_onAssetPath)), gameRef.images.fromCache(_onAssetPath),
AlienBumperSpriteState.inactive: ),
AlienBumperState.inactive:
Sprite(gameRef.images.fromCache(_offAssetPath)), Sprite(gameRef.images.fromCache(_offAssetPath)),
}; };
this.sprites = sprites; this.sprites = sprites;
current = AlienBumperSpriteState.active;
size = sprites[current]!.originalSize / 10; size = sprites[current]!.originalSize / 10;
} }
} }

@ -0,0 +1,14 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class AlienBumperBallContactBehavior extends ContactBehavior<AlienBumper> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
}
}

@ -0,0 +1,39 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template alien_bumper_blinking_behavior}
/// Makes a [AlienBumper] blink back to [AlienBumperState.active] when
/// [AlienBumperState.inactive].
/// {@endtemplate}
class AlienBumperBlinkingBehavior extends TimerComponent
with ParentIsA<AlienBumper> {
/// {@macro alien_bumper_blinking_behavior}
AlienBumperBlinkingBehavior() : super(period: 0.05);
void _onNewState(AlienBumperState state) {
switch (state) {
case AlienBumperState.active:
break;
case AlienBumperState.inactive:
timer
..reset()
..start();
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
timer.stop();
parent.bloc.onBlinked();
}
}

@ -0,0 +1,2 @@
export 'alien_bumper_ball_contact_behavior.dart';
export 'alien_bumper_blinking_behavior.dart';

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'alien_bumper_state.dart';
class AlienBumperCubit extends Cubit<AlienBumperState> {
AlienBumperCubit() : super(AlienBumperState.active);
void onBallContacted() {
emit(AlienBumperState.inactive);
}
void onBlinked() {
emit(AlienBumperState.active);
}
}

@ -0,0 +1,10 @@
part of 'alien_bumper_cubit.dart';
/// Indicates the [AlienBumperCubit]'s current state.
enum AlienBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}

@ -16,6 +16,7 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
Ball({ Ball({
required this.baseColor, required this.baseColor,
}) : super( }) : super(
renderBody: false,
children: [ children: [
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)),
], ],
@ -26,7 +27,6 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
// We need to see what happens if Ball appears from other place like nest // 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. // bumper, it will need to explicit change layer to Layer.board then.
layer = Layer.board; layer = Layer.board;
renderBody = false;
} }
/// The size of the [Ball]. /// The size of the [Ball].

@ -13,10 +13,9 @@ class Baseboard extends BodyComponent with InitialPosition {
required BoardSide side, required BoardSide side,
}) : _side = side, }) : _side = side,
super( super(
renderBody: false,
children: [_BaseboardSpriteComponent(side: side)], children: [_BaseboardSpriteComponent(side: side)],
) { );
renderBody = false;
}
/// Whether the [Baseboard] is on the left or right side of the board. /// Whether the [Baseboard] is on the left or right side of the board.
final BoardSide _side; final BoardSide _side;

@ -26,11 +26,10 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
/// {@macro bottom_boundary} /// {@macro bottom_boundary}
_BottomBoundary() _BottomBoundary()
: super( : super(
renderBody: false,
priority: RenderPriority.bottomBoundary, priority: RenderPriority.bottomBoundary,
children: [_BottomBoundarySpriteComponent()], children: [_BottomBoundarySpriteComponent()],
) { );
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final bottomLeftCurve = BezierCurveShape( final bottomLeftCurve = BezierCurveShape(
@ -92,13 +91,10 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
/// {@macro outer_boundary} /// {@macro outer_boundary}
_OuterBoundary() _OuterBoundary()
: super( : super(
renderBody: false,
priority: RenderPriority.outerBoundary, priority: RenderPriority.outerBoundary,
children: [ children: [_OuterBoundarySpriteComponent()],
_OuterBoundarySpriteComponent(), );
],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topWall = EdgeShape() final topWall = EdgeShape()

@ -1,4 +1,4 @@
export 'alien_bumper.dart'; export 'alien_bumper/alien_bumper.dart';
export 'backboard/backboard.dart'; export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
@ -8,11 +8,11 @@ export 'boundaries.dart';
export 'camera_zoom.dart'; export 'camera_zoom.dart';
export 'chrome_dino.dart'; export 'chrome_dino.dart';
export 'dash_animatronic.dart'; export 'dash_animatronic.dart';
export 'dash_nest_bumper.dart'; export 'dash_nest_bumper/dash_nest_bumper.dart';
export 'dino_walls.dart'; export 'dino_walls.dart';
export 'fire_effect.dart'; export 'fire_effect.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'google_letter.dart'; export 'google_letter/google_letter.dart';
export 'initial_position.dart'; export 'initial_position.dart';
export 'joint_anchor.dart'; export 'joint_anchor.dart';
export 'kicker.dart'; export 'kicker.dart';
@ -30,5 +30,5 @@ export 'spaceship.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export 'spaceship_ramp.dart'; export 'spaceship_ramp.dart';
export 'sparky_animatronic.dart'; export 'sparky_animatronic.dart';
export 'sparky_bumper.dart'; export 'sparky_bumper/sparky_bumper.dart';
export 'sparky_computer.dart'; export 'sparky_computer.dart';

@ -10,7 +10,6 @@ class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
playing: false, playing: false,
priority: RenderPriority.dashAnimatronic,
); );
@override @override

@ -0,0 +1,15 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class DashNestBumperBallContactBehavior
extends ContactBehavior<DashNestBumper> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
}
}

@ -0,0 +1,19 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'dash_nest_bumper_state.dart';
class DashNestBumperCubit extends Cubit<DashNestBumperState> {
DashNestBumperCubit() : super(DashNestBumperState.inactive);
/// Event added when the bumper contacts with a ball.
void onBallContacted() {
emit(DashNestBumperState.active);
}
/// Event added when the bumper should return to its initial configuration.
void onReset() {
emit(DashNestBumperState.inactive);
}
}

@ -0,0 +1,10 @@
part of 'dash_nest_bumper_cubit.dart';
/// Indicates the [DashNestBumperCubit]'s current state.
enum DashNestBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}

@ -4,6 +4,10 @@ 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:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/dash_nest_bumper_cubit.dart';
/// {@template dash_nest_bumper} /// {@template dash_nest_bumper}
/// Bumper with a nest appearance. /// Bumper with a nest appearance.
@ -16,54 +20,87 @@ 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,
required this.bloc,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
_minorRadius = minorRadius, _minorRadius = minorRadius,
super( super(
priority: RenderPriority.dashBumper, renderBody: false,
children: [ children: [
_DashNestBumperSpriteGroupComponent( _DashNestBumperSpriteGroupComponent(
activeAssetPath: activeAssetPath, activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath, inactiveAssetPath: inactiveAssetPath,
position: spritePosition, position: spritePosition,
current: bloc.state,
), ),
DashNestBumperBallContactBehavior(),
...?children,
], ],
) { );
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,
bloc: DashNestBumperCubit(),
); );
/// {@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,
bloc: DashNestBumperCubit(),
); );
/// {@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,
bloc: DashNestBumperCubit(),
); );
/// Creates an [DashNestBumper] without any children.
///
/// This can be used for testing [DashNestBumper]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
DashNestBumper.test({required this.bloc})
: _majorRadius = 3,
_minorRadius = 2.5;
final double _majorRadius; final double _majorRadius;
final double _minorRadius; final double _minorRadius;
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final DashNestBumperCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override @override
Body createBody() { Body createBody() {
final shape = EllipseShape( final shape = EllipseShape(
@ -79,41 +116,22 @@ class DashNestBumper extends BodyComponent with InitialPosition {
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
/// Activates the [DashNestBumper].
void activate() {
firstChild<_DashNestBumperSpriteGroupComponent>()?.current =
DashNestBumperSpriteState.active;
}
/// Deactivates the [DashNestBumper].
void deactivate() {
firstChild<_DashNestBumperSpriteGroupComponent>()?.current =
DashNestBumperSpriteState.inactive;
}
}
/// Indicates the [DashNestBumper]'s current sprite state.
@visibleForTesting
enum DashNestBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
} }
class _DashNestBumperSpriteGroupComponent class _DashNestBumperSpriteGroupComponent
extends SpriteGroupComponent<DashNestBumperSpriteState> with HasGameRef { extends SpriteGroupComponent<DashNestBumperState>
with HasGameRef, ParentIsA<DashNestBumper> {
_DashNestBumperSpriteGroupComponent({ _DashNestBumperSpriteGroupComponent({
required String activeAssetPath, required String activeAssetPath,
required String inactiveAssetPath, required String inactiveAssetPath,
required Vector2 position, required Vector2 position,
required DashNestBumperState current,
}) : _activeAssetPath = activeAssetPath, }) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath, _inactiveAssetPath = inactiveAssetPath,
super( super(
anchor: Anchor.center, anchor: Anchor.center,
position: position, position: position,
current: current,
); );
final String _activeAssetPath; final String _activeAssetPath;
@ -122,15 +140,15 @@ class _DashNestBumperSpriteGroupComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
parent.bloc.stream.listen((state) => current = state);
final sprites = { final sprites = {
DashNestBumperSpriteState.active: DashNestBumperState.active:
Sprite(gameRef.images.fromCache(_activeAssetPath)), Sprite(gameRef.images.fromCache(_activeAssetPath)),
DashNestBumperSpriteState.inactive: DashNestBumperState.inactive:
Sprite(gameRef.images.fromCache(_inactiveAssetPath)), Sprite(gameRef.images.fromCache(_inactiveAssetPath)),
}; };
this.sprites = sprites; this.sprites = sprites;
current = DashNestBumperSpriteState.inactive;
size = sprites[current]!.originalSize / 10; size = sprites[current]!.originalSize / 10;
} }
} }

@ -29,9 +29,8 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
: super( : super(
priority: RenderPriority.dinoTopWall, priority: RenderPriority.dinoTopWall,
children: [_DinoTopWallSpriteComponent()], children: [_DinoTopWallSpriteComponent()],
) { renderBody: false,
renderBody = false; );
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topStraightShape = EdgeShape() final topStraightShape = EdgeShape()
@ -128,9 +127,8 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
: super( : super(
priority: RenderPriority.dinoBottomWall, priority: RenderPriority.dinoBottomWall,
children: [_DinoBottomWallSpriteComponent()], children: [_DinoBottomWallSpriteComponent()],
) { renderBody: false,
renderBody = false; );
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
const restitution = 1.0; const restitution = 1.0;

@ -14,10 +14,9 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
Flipper({ Flipper({
required this.side, required this.side,
}) : super( }) : super(
renderBody: false,
children: [_FlipperSpriteComponent(side: side)], children: [_FlipperSpriteComponent(side: side)],
) { );
renderBody = false;
}
/// The size of the [Flipper]. /// The size of the [Flipper].
static final size = Vector2(13.5, 4.3); static final size = Vector2(13.5, 4.3);

@ -0,0 +1,14 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class GoogleLetterBallContactBehavior extends ContactBehavior<GoogleLetter> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
}
}

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'google_letter_state.dart';
class GoogleLetterCubit extends Cubit<GoogleLetterState> {
GoogleLetterCubit() : super(GoogleLetterState.inactive);
void onBallContacted() {
emit(GoogleLetterState.active);
}
void onReset() {
emit(GoogleLetterState.inactive);
}
}

@ -0,0 +1,10 @@
part of 'google_letter_cubit.dart';
/// Indicates the [GoogleLetterCubit]'s current state.
enum GoogleLetterState {
/// A lit up letter.
active,
/// A dimmed letter.
inactive,
}

@ -1,33 +1,46 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/google_letter/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/google_letter_cubit.dart';
/// {@template google_letter} /// {@template google_letter}
/// Circular sensor that represents a letter in "GOOGLE" for a given index. /// Circular sensor that represents a letter in "GOOGLE" for a given index.
/// {@endtemplate} /// {@endtemplate}
class GoogleLetter extends BodyComponent with InitialPosition { class GoogleLetter extends BodyComponent with InitialPosition {
/// {@macro google_letter} /// {@macro google_letter}
GoogleLetter(int index) GoogleLetter(
: _sprite = _GoogleLetterSprite( int index,
_GoogleLetterSprite.spritePaths[index], ) : bloc = GoogleLetterCubit(),
super(
children: [
GoogleLetterBallContactBehavior(),
_GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index])
],
); );
final _GoogleLetterSprite _sprite; /// Creates a [GoogleLetter] without any children.
///
/// Activates this [GoogleLetter]. /// This can be used for testing [GoogleLetter]'s behaviors in isolation.
// TODO(alestiago): Improve doc comment once activate and deactivate // TODO(alestiago): Refactor injecting bloc once the following is merged:
// are implemented with the actual assets. // https://github.com/flame-engine/flame/pull/1538
Future<void> activate() => _sprite.activate(); @visibleForTesting
GoogleLetter.test({
required this.bloc,
});
/// Deactivates this [GoogleLetter]. // TODO(alestiago): Consider refactoring once the following is merged:
Future<void> deactivate() => _sprite.deactivate(); // https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final GoogleLetterCubit bloc;
@override @override
Future<void> onLoad() async { void onRemove() {
await super.onLoad(); bloc.close();
await add(_sprite); super.onRemove();
} }
@override @override
@ -46,8 +59,11 @@ class GoogleLetter extends BodyComponent with InitialPosition {
} }
} }
class _GoogleLetterSprite extends SpriteComponent with HasGameRef { class _GoogleLetterSprite extends SpriteComponent
_GoogleLetterSprite(String path) : _path = path; with HasGameRef, ParentIsA<GoogleLetter> {
_GoogleLetterSprite(String path)
: _path = path,
super(anchor: Anchor.center);
static final spritePaths = [ static final spritePaths = [
Assets.images.googleWord.letter1.keyName, Assets.images.googleWord.letter1.keyName,
@ -60,39 +76,16 @@ class _GoogleLetterSprite extends SpriteComponent with HasGameRef {
final String _path; final String _path;
// TODO(alestiago): Correctly implement activate and deactivate once the
// assets are provided.
Future<void> activate() async {
await add(
_GoogleLetterColorEffect(color: Colors.green),
);
}
Future<void> deactivate() async {
await add(
_GoogleLetterColorEffect(color: Colors.red),
);
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
// TODO(alisonryan2002): Make SpriteGroupComponent.
// parent.bloc.stream.listen();
// TODO(alestiago): Used cached assets. // TODO(alestiago): Used cached assets.
final sprite = await gameRef.loadSprite(_path); final sprite = await gameRef.loadSprite(_path);
this.sprite = sprite; this.sprite = sprite;
// TODO(alestiago): Size correctly once the assets are provided. // TODO(alestiago): Size correctly once the assets are provided.
size = sprite.originalSize / 5; size = sprite.originalSize / 5;
anchor = Anchor.center;
} }
} }
class _GoogleLetterColorEffect extends ColorEffect {
_GoogleLetterColorEffect({
required Color color,
}) : super(
color,
const Offset(0, 1),
EffectController(duration: 0.25),
);
}

@ -19,9 +19,8 @@ class Kicker extends BodyComponent with InitialPosition {
}) : _side = side, }) : _side = side,
super( super(
children: [_KickerSpriteComponent(side: side)], children: [_KickerSpriteComponent(side: side)],
) { renderBody: false,
renderBody = false; );
}
/// The size of the [Kicker] body. /// The size of the [Kicker] body.
static final Vector2 size = Vector2(4.4, 15); static final Vector2 size = Vector2(4.4, 15);

@ -32,13 +32,13 @@ class _LaunchRampBase extends BodyComponent with Layered {
_LaunchRampBase() _LaunchRampBase()
: super( : super(
priority: RenderPriority.launchRamp, priority: RenderPriority.launchRamp,
renderBody: false,
children: [ children: [
_LaunchRampBackgroundRailingSpriteComponent(), _LaunchRampBackgroundRailingSpriteComponent(),
_LaunchRampBaseSpriteComponent(), _LaunchRampBaseSpriteComponent(),
], ],
) { ) {
layer = Layer.launcher; layer = Layer.launcher;
renderBody = false;
} }
// TODO(ruimiguel): final asset differs slightly from the current shape. We // TODO(ruimiguel): final asset differs slightly from the current shape. We
@ -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 {
@ -157,9 +150,8 @@ class _LaunchRampForegroundRailing extends BodyComponent {
: super( : super(
priority: RenderPriority.launchRampForegroundRailing, priority: RenderPriority.launchRampForegroundRailing,
children: [_LaunchRampForegroundRailingSpriteComponent()], children: [_LaunchRampForegroundRailingSpriteComponent()],
) { renderBody: false,
renderBody = false; );
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
@ -218,9 +210,8 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent
} }
class _LaunchRampCloseWall extends BodyComponent with InitialPosition, Layered { class _LaunchRampCloseWall extends BodyComponent with InitialPosition, Layered {
_LaunchRampCloseWall() { _LaunchRampCloseWall() : super(renderBody: false) {
layer = Layer.board; layer = Layer.board;
renderBody = false;
} }
@override @override
@ -252,7 +243,6 @@ class _LaunchRampExit extends LayerSensor {
outsidePriority: RenderPriority.ballOnBoard, outsidePriority: RenderPriority.ballOnBoard,
) { ) {
layer = Layer.launcher; layer = Layer.launcher;
renderBody = false;
} }
static final Vector2 _size = Vector2(1.6, 0.1); static final Vector2 _size = Vector2(1.6, 0.1);

@ -17,13 +17,11 @@ enum LayerEntranceOrientation {
/// {@template layer_sensor} /// {@template layer_sensor}
/// [BodyComponent] located at the entrance and exit of a [Layer]. /// [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 /// 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,
@ -34,7 +32,8 @@ abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
}) : _insideLayer = insideLayer, }) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board, _outsideLayer = outsideLayer ?? Layer.board,
_insidePriority = insidePriority, _insidePriority = insidePriority,
_outsidePriority = outsidePriority ?? RenderPriority.ballOnBoard { _outsidePriority = outsidePriority ?? RenderPriority.ballOnBoard,
super(renderBody: false) {
layer = Layer.opening; layer = Layer.opening;
} }
final Layer _insideLayer; final Layer _insideLayer;
@ -75,35 +74,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();
} }
} }

@ -14,9 +14,11 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
required this.compressionDistance, required this.compressionDistance,
// TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities // TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities
// are fixed. // are fixed.
}) : super(priority: RenderPriority.plunger) { }) : super(
priority: RenderPriority.plunger,
renderBody: false,
) {
layer = Layer.launcher; layer = Layer.launcher;
renderBody = false;
} }
/// Distance the plunger can lower. /// Distance the plunger can lower.

@ -69,11 +69,7 @@ abstract class RenderPriority {
// Flutter Forest // Flutter Forest
static const int signpost = _above + launchRampForegroundRailing; static const int flutterForest = _above + launchRampForegroundRailing;
static const int dashBumper = _above + ballOnBoard;
static const int dashAnimatronic = 2 * _above + launchRamp;
// Sparky Fire Zone // Sparky Fire Zone

@ -46,13 +46,15 @@ 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,
priority: RenderPriority.signpost, }) : super(
children: [_SignpostSpriteComponent()], renderBody: false,
) { children: [
renderBody = false; _SignpostSpriteComponent(),
} ...?children,
],
);
/// Forwards the sprite to the next [SignpostSpriteState]. /// Forwards the sprite to the next [SignpostSpriteState].
/// ///

@ -40,9 +40,8 @@ class Slingshot extends BodyComponent with InitialPosition {
super( super(
priority: RenderPriority.slingshot, priority: RenderPriority.slingshot,
children: [_SlinghsotSpriteComponent(spritePath, angle: angle)], children: [_SlinghsotSpriteComponent(spritePath, angle: angle)],
) { renderBody: false,
renderBody = false; );
}
final double _length; final double _length;

@ -42,25 +42,12 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
SpaceshipSaucer() SpaceshipSaucer()
: super( : super(
priority: RenderPriority.spaceshipSaucer, priority: RenderPriority.spaceshipSaucer,
renderBody: false,
children: [ children: [
_SpaceshipSaucerSpriteComponent(), _SpaceshipSaucerSpriteComponent(),
], ],
) { ) {
layer = Layer.spaceship; layer = Layer.spaceship;
renderBody = false;
}
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef
..addContactCallback(
LayerSensorBallContactCallback<_SpaceshipEntrance>(),
)
..addContactCallback(
LayerSensorBallContactCallback<_SpaceshipHole>(),
);
} }
@override @override
@ -108,8 +95,8 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered {
: super( : super(
priority: RenderPriority.androidHead, priority: RenderPriority.androidHead,
children: [_AndroidHeadSpriteAnimation()], children: [_AndroidHeadSpriteAnimation()],
renderBody: false,
) { ) {
renderBody = false;
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@ -164,7 +151,6 @@ class _SpaceshipEntrance extends LayerSensor {
@override @override
Shape get shape { Shape get shape {
renderBody = false;
final radius = Spaceship.size.y / 2; final radius = Spaceship.size.y / 2;
return PolygonShape() return PolygonShape()
..setAsEdge( ..setAsEdge(
@ -189,7 +175,6 @@ class _SpaceshipHole extends LayerSensor {
insidePriority: RenderPriority.ballOnSpaceship, insidePriority: RenderPriority.ballOnSpaceship,
outsidePriority: outsidePriority, outsidePriority: outsidePriority,
) { ) {
renderBody = false;
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@ -237,14 +222,16 @@ class _SpaceshipWallShape extends ChainShape {
/// {@endtemplate} /// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered { class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_wall} /// {@macro spaceship_wall}
SpaceshipWall() : super(priority: RenderPriority.spaceshipSaucerWall) { SpaceshipWall()
: super(
priority: RenderPriority.spaceshipSaucerWall,
renderBody: false,
) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@override @override
Body createBody() { Body createBody() {
renderBody = false;
final shape = _SpaceshipWallShape(); final shape = _SpaceshipWallShape();
final fixtureDef = FixtureDef(shape); final fixtureDef = FixtureDef(shape);

@ -29,10 +29,10 @@ class _SpaceshipRailRamp extends BodyComponent with Layered {
_SpaceshipRailRamp() _SpaceshipRailRamp()
: super( : super(
priority: RenderPriority.spaceshipRail, priority: RenderPriority.spaceshipRail,
renderBody: false,
children: [_SpaceshipRailRampSpriteComponent()], children: [_SpaceshipRailRampSpriteComponent()],
) { ) {
layer = Layer.spaceshipExitRail; layer = Layer.spaceshipExitRail;
renderBody = false;
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
@ -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
@ -160,9 +152,7 @@ class _SpaceshipRailForeground extends SpriteComponent with HasGameRef {
/// Represents the ground bases of the [_SpaceshipRailRamp]. /// Represents the ground bases of the [_SpaceshipRailRamp].
class _SpaceshipRailBase extends BodyComponent with InitialPosition { class _SpaceshipRailBase extends BodyComponent with InitialPosition {
_SpaceshipRailBase({required this.radius}) { _SpaceshipRailBase({required this.radius}) : super(renderBody: false);
renderBody = false;
}
final double radius; final double radius;
@ -185,7 +175,6 @@ class _SpaceshipRailExit extends LayerSensor {
insideLayer: Layer.spaceshipExitRail, insideLayer: Layer.spaceshipExitRail,
insidePriority: RenderPriority.ballOnSpaceshipRail, insidePriority: RenderPriority.ballOnSpaceshipRail,
) { ) {
renderBody = false;
layer = Layer.spaceshipExitRail; layer = Layer.spaceshipExitRail;
} }

@ -98,12 +98,12 @@ class _SpaceshipRampBackground extends BodyComponent
_SpaceshipRampBackground() _SpaceshipRampBackground()
: super( : super(
priority: RenderPriority.spaceshipRamp, priority: RenderPriority.spaceshipRamp,
renderBody: false,
children: [ children: [
_SpaceshipRampBackgroundRampSpriteComponent(), _SpaceshipRampBackgroundRampSpriteComponent(),
], ],
) { ) {
layer = Layer.spaceshipEntranceRamp; layer = Layer.spaceshipEntranceRamp;
renderBody = false;
} }
/// Width between walls of the ramp. /// Width between walls of the ramp.
@ -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
@ -255,10 +247,10 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
_SpaceshipRampForegroundRailing() _SpaceshipRampForegroundRailing()
: super( : super(
priority: RenderPriority.spaceshipRampForegroundRailing, priority: RenderPriority.spaceshipRampForegroundRailing,
renderBody: false,
children: [_SpaceshipRampForegroundRailingSpriteComponent()], children: [_SpaceshipRampForegroundRailingSpriteComponent()],
) { ) {
layer = Layer.spaceshipEntranceRamp; layer = Layer.spaceshipEntranceRamp;
renderBody = false;
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
@ -321,8 +313,7 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
} }
class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered { class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered {
_SpaceshipRampBase() { _SpaceshipRampBase() : super(renderBody: false) {
renderBody = false;
layer = Layer.board; layer = Layer.board;
} }
@ -363,9 +354,7 @@ class _SpaceshipRampOpening extends LayerSensor {
orientation: LayerEntranceOrientation.down, orientation: LayerEntranceOrientation.down,
insidePriority: RenderPriority.ballOnSpaceshipRamp, insidePriority: RenderPriority.ballOnSpaceshipRamp,
outsidePriority: outsidePriority, outsidePriority: outsidePriority,
) { );
renderBody = false;
}
final double _rotation; final double _rotation;

@ -0,0 +1,2 @@
export 'sparky_bumper_ball_contact_behavior.dart';
export 'sparky_bumper_blinking_behavior.dart';

@ -0,0 +1,14 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class SparkyBumperBallContactBehavior extends ContactBehavior<SparkyBumper> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
}
}

@ -0,0 +1,39 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_bumper_blinking_behavior}
/// Makes a [SparkyBumper] blink back to [SparkyBumperState.active] when
/// [SparkyBumperState.inactive].
/// {@endtemplate}
class SparkyBumperBlinkingBehavior extends TimerComponent
with ParentIsA<SparkyBumper> {
/// {@macro sparky_bumper_sprite_behavior}
SparkyBumperBlinkingBehavior() : super(period: 0.05);
void _onNewState(SparkyBumperState state) {
switch (state) {
case SparkyBumperState.active:
break;
case SparkyBumperState.inactive:
timer
..reset()
..start();
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
timer.stop();
parent.bloc.onBlinked();
}
}

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'sparky_bumper_state.dart';
class SparkyBumperCubit extends Cubit<SparkyBumperState> {
SparkyBumperCubit() : super(SparkyBumperState.active);
void onBallContacted() {
emit(SparkyBumperState.inactive);
}
void onBlinked() {
emit(SparkyBumperState.active);
}
}

@ -0,0 +1,10 @@
part of 'sparky_bumper_cubit.dart';
/// Indicates the [SparkyBumperCubit]'s current state.
enum SparkyBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}

@ -4,6 +4,10 @@ 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:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/sparky_bumper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/sparky_bumper_cubit.dart';
/// {@template sparky_bumper} /// {@template sparky_bumper}
/// Bumper for Sparky area. /// Bumper for Sparky area.
@ -16,58 +20,92 @@ class SparkyBumper extends BodyComponent with InitialPosition {
required String onAssetPath, required String onAssetPath,
required String offAssetPath, required String offAssetPath,
required Vector2 spritePosition, required Vector2 spritePosition,
required this.bloc,
Iterable<Component>? children,
}) : _majorRadius = majorRadius, }) : _majorRadius = majorRadius,
_minorRadius = minorRadius, _minorRadius = minorRadius,
super( super(
priority: RenderPriority.sparkyBumper, priority: RenderPriority.sparkyBumper,
renderBody: false,
children: [ children: [
SparkyBumperBallContactBehavior(),
SparkyBumperBlinkingBehavior(),
_SparkyBumperSpriteGroupComponent( _SparkyBumperSpriteGroupComponent(
onAssetPath: onAssetPath, onAssetPath: onAssetPath,
offAssetPath: offAssetPath, offAssetPath: offAssetPath,
position: spritePosition, position: spritePosition,
state: bloc.state,
), ),
...?children,
], ],
) { );
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),
bloc: SparkyBumperCubit(),
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),
bloc: SparkyBumperCubit(),
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),
bloc: SparkyBumperCubit(),
children: children,
); );
/// Creates an [SparkyBumper] without any children.
///
/// This can be used for testing [SparkyBumper]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
SparkyBumper.test({
required this.bloc,
}) : _majorRadius = 3,
_minorRadius = 2.2;
final double _majorRadius; final double _majorRadius;
final double _minorRadius; final double _minorRadius;
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final SparkyBumperCubit bloc;
@override @override
Body createBody() { void onRemove() {
renderBody = false; bloc.close();
super.onRemove();
}
@override
Body createBody() {
final shape = EllipseShape( final shape = EllipseShape(
center: Vector2.zero(), center: Vector2.zero(),
majorRadius: _majorRadius, majorRadius: _majorRadius,
@ -83,37 +121,22 @@ class SparkyBumper extends BodyComponent with InitialPosition {
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
/// Animates the [DashNestBumper].
Future<void> animate() async {
final spriteGroupComponent = firstChild<_SparkyBumperSpriteGroupComponent>()
?..current = SparkyBumperSpriteState.inactive;
await Future<void>.delayed(const Duration(milliseconds: 50));
spriteGroupComponent?.current = SparkyBumperSpriteState.active;
}
}
/// Indicates the [SparkyBumper]'s current sprite state.
@visibleForTesting
enum SparkyBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
} }
class _SparkyBumperSpriteGroupComponent class _SparkyBumperSpriteGroupComponent
extends SpriteGroupComponent<SparkyBumperSpriteState> with HasGameRef { extends SpriteGroupComponent<SparkyBumperState>
with HasGameRef, ParentIsA<SparkyBumper> {
_SparkyBumperSpriteGroupComponent({ _SparkyBumperSpriteGroupComponent({
required String onAssetPath, required String onAssetPath,
required String offAssetPath, required String offAssetPath,
required Vector2 position, required Vector2 position,
required SparkyBumperState state,
}) : _onAssetPath = onAssetPath, }) : _onAssetPath = onAssetPath,
_offAssetPath = offAssetPath, _offAssetPath = offAssetPath,
super( super(
anchor: Anchor.center, anchor: Anchor.center,
position: position, position: position,
current: state,
); );
final String _onAssetPath; final String _onAssetPath;
@ -122,15 +145,20 @@ class _SparkyBumperSpriteGroupComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
parent.bloc.stream.listen((state) => current = state);
final sprites = { final sprites = {
SparkyBumperSpriteState.active: SparkyBumperState.active: Sprite(
Sprite(gameRef.images.fromCache(_onAssetPath)), gameRef.images.fromCache(_onAssetPath),
SparkyBumperSpriteState.inactive: ),
Sprite(gameRef.images.fromCache(_offAssetPath)), SparkyBumperState.inactive: Sprite(
gameRef.images.fromCache(_offAssetPath),
),
}; };
this.sprites = sprites; this.sprites = sprites;
current = SparkyBumperSpriteState.active;
size = sprites[current]!.originalSize / 10; size = sprites[current]!.originalSize / 10;
} }
} }

@ -23,10 +23,9 @@ class _ComputerBase extends BodyComponent with InitialPosition {
_ComputerBase() _ComputerBase()
: super( : super(
priority: RenderPriority.computerBase, priority: RenderPriority.computerBase,
renderBody: false,
children: [_ComputerBaseSpriteComponent()], children: [_ComputerBaseSpriteComponent()],
) { );
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final leftEdge = EdgeShape() final leftEdge = EdgeShape()

@ -7,8 +7,13 @@ environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.16.0 <3.0.0"
dependencies: dependencies:
bloc: ^8.0.3
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:
@ -19,8 +24,8 @@ dependencies:
pinball_theme: pinball_theme:
path: ../pinball_theme path: ../pinball_theme
dev_dependencies: dev_dependencies:
bloc_test: ^9.0.3
flame_test: ^1.3.0 flame_test: ^1.3.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter

@ -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,12 @@ 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 {}
class MockAlienBumperCubit extends Mock implements AlienBumperCubit {}
class MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {}
class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {}
class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {}

@ -0,0 +1,78 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.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_components/src/components/alien_bumper/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.alienBumper.a.active.keyName,
Assets.images.alienBumper.a.inactive.keyName,
Assets.images.alienBumper.b.active.keyName,
Assets.images.alienBumper.b.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('AlienBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final alienBumper = AlienBumper.a();
await game.ensureAdd(alienBumper);
expect(game.contains(alienBumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final alienBumper = AlienBumper.b();
await game.ensureAdd(alienBumper);
expect(game.contains(alienBumper), isTrue);
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = MockAlienBumperCubit();
whenListen(
bloc,
const Stream<AlienBumperState>.empty(),
initialState: AlienBumperState.active,
);
when(bloc.close).thenAnswer((_) async {});
final alienBumper = AlienBumper.test(bloc: bloc);
await game.ensureAdd(alienBumper);
game.remove(alienBumper);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final alienBumper = AlienBumper.a(
children: [component],
);
await game.ensureAdd(alienBumper);
expect(alienBumper.children, contains(component));
});
flameTester.test('an AlienBumperBallContactBehavior', (game) async {
final alienBumper = AlienBumper.a();
await game.ensureAdd(alienBumper);
expect(
alienBumper.children
.whereType<AlienBumperBallContactBehavior>()
.single,
isNotNull,
);
});
});
});
}

@ -0,0 +1,48 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.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_components/src/components/alien_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'AlienBumperBallContactBehavior',
() {
test('can be instantiated', () {
expect(
AlienBumperBallContactBehavior(),
isA<AlienBumperBallContactBehavior>(),
);
});
flameTester.test(
'beginContact emits onBallContacted when contacts with a ball',
(game) async {
final behavior = AlienBumperBallContactBehavior();
final bloc = MockAlienBumperCubit();
whenListen(
bloc,
const Stream<AlienBumperState>.empty(),
initialState: AlienBumperState.active,
);
final alienBumper = AlienBumper.test(bloc: bloc);
await alienBumper.add(behavior);
await game.ensureAdd(alienBumper);
behavior.beginContact(MockBall(), MockContact());
verify(alienBumper.bloc.onBallContacted).called(1);
},
);
},
);
}

@ -0,0 +1,45 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.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_components/src/components/alien_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'AlienBumperBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls onBlinked after 0.05 seconds when inactive',
setUp: (game, tester) async {
final behavior = AlienBumperBlinkingBehavior();
final bloc = MockAlienBumperCubit();
final streamController = StreamController<AlienBumperState>();
whenListen(
bloc,
streamController.stream,
initialState: AlienBumperState.active,
);
final alienBumper = AlienBumper.test(bloc: bloc);
await alienBumper.add(behavior);
await game.ensureAdd(alienBumper);
streamController.add(AlienBumperState.inactive);
await tester.pump();
game.update(0.05);
await streamController.close();
verify(bloc.onBlinked).called(1);
},
);
},
);
}

@ -0,0 +1,24 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'AlienBumperCubit',
() {
blocTest<AlienBumperCubit, AlienBumperState>(
'onBallContacted emits inactive',
build: AlienBumperCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [AlienBumperState.inactive],
);
blocTest<AlienBumperCubit, AlienBumperState>(
'onBlinked emits active',
build: AlienBumperCubit.new,
act: (bloc) => bloc.onBlinked(),
expect: () => [AlienBumperState.active],
);
},
);
}

@ -1,61 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.alienBumper.a.active.keyName,
Assets.images.alienBumper.a.inactive.keyName,
Assets.images.alienBumper.b.active.keyName,
Assets.images.alienBumper.b.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('AlienBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = AlienBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('animate switches between on and off sprites',
(game) async {
final bumper = AlienBumper.a();
await game.ensureAdd(bumper);
final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!;
expect(
spriteGroupComponent.current,
equals(AlienBumperSpriteState.active),
);
final future = bumper.animate();
expect(
spriteGroupComponent.current,
equals(AlienBumperSpriteState.inactive),
);
await future;
expect(
spriteGroupComponent.current,
equals(AlienBumperSpriteState.active),
);
});
});
}

@ -0,0 +1,48 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.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_components/src/components/dash_nest_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'DashNestBumperBallContactBehavior',
() {
test('can be instantiated', () {
expect(
DashNestBumperBallContactBehavior(),
isA<DashNestBumperBallContactBehavior>(),
);
});
flameTester.test(
'beginContact emits onBallContacted when contacts with a ball',
(game) async {
final behavior = DashNestBumperBallContactBehavior();
final bloc = MockDashNestBumperCubit();
whenListen(
bloc,
const Stream<DashNestBumperState>.empty(),
initialState: DashNestBumperState.active,
);
final dashNestBumper = DashNestBumper.test(bloc: bloc);
await dashNestBumper.add(behavior);
await game.ensureAdd(dashNestBumper);
behavior.beginContact(MockBall(), MockContact());
verify(dashNestBumper.bloc.onBallContacted).called(1);
},
);
},
);
}

@ -0,0 +1,24 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'DashNestBumperCubit',
() {
blocTest<DashNestBumperCubit, DashNestBumperState>(
'onBallContacted emits active',
build: DashNestBumperCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [DashNestBumperState.active],
);
blocTest<DashNestBumperCubit, DashNestBumperState>(
'onReset emits inactive',
build: DashNestBumperCubit.new,
act: (bloc) => bloc.onReset(),
expect: () => [DashNestBumperState.inactive],
);
},
);
}

@ -0,0 +1,88 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.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_components/src/components/dash_nest_bumper/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('DashNestBumper', () {
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('"main" loads correctly', (game) async {
final bumper = DashNestBumper.main();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"a" loads correctly', (game) async {
final bumper = DashNestBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = DashNestBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = MockDashNestBumperCubit();
whenListen(
bloc,
const Stream<DashNestBumperState>.empty(),
initialState: DashNestBumperState.inactive,
);
when(bloc.close).thenAnswer((_) async {});
final dashNestBumper = DashNestBumper.test(bloc: bloc);
await game.ensureAdd(dashNestBumper);
game.remove(dashNestBumper);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('adds new children', (game) async {
final component = Component();
final dashNestBumper = DashNestBumper.a(
children: [component],
);
await game.ensureAdd(dashNestBumper);
expect(dashNestBumper.children, contains(component));
});
flameTester.test('a DashNestBumperBallContactBehavior', (game) async {
final dashNestBumper = DashNestBumper.a();
await game.ensureAdd(dashNestBumper);
expect(
dashNestBumper.children
.whereType<DashNestBumperBallContactBehavior>()
.single,
isNotNull,
);
});
});
});
}

@ -1,77 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('DashNestBumper', () {
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('"main" loads correctly', (game) async {
final bumper = DashNestBumper.main();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"a" loads correctly', (game) async {
final bumper = DashNestBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = DashNestBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate switches to active sprite', (game) async {
final bumper = DashNestBumper.main();
await game.ensureAdd(bumper);
final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!;
expect(
spriteGroupComponent.current,
equals(DashNestBumperSpriteState.inactive),
);
bumper.activate();
expect(
spriteGroupComponent.current,
equals(DashNestBumperSpriteState.active),
);
});
flameTester.test('deactivate switches to inactive sprite', (game) async {
final bumper = DashNestBumper.main();
await game.ensureAdd(bumper);
final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!
..current = DashNestBumperSpriteState.active;
bumper.deactivate();
expect(
spriteGroupComponent.current,
equals(DashNestBumperSpriteState.inactive),
);
});
});
}

@ -0,0 +1,48 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.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_components/src/components/google_letter/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'GoogleLetterBallContactBehavior',
() {
test('can be instantiated', () {
expect(
GoogleLetterBallContactBehavior(),
isA<GoogleLetterBallContactBehavior>(),
);
});
flameTester.test(
'beginContact emits onBallContacted when contacts with a ball',
(game) async {
final behavior = GoogleLetterBallContactBehavior();
final bloc = MockGoogleLetterCubit();
whenListen(
bloc,
const Stream<GoogleLetterState>.empty(),
initialState: GoogleLetterState.active,
);
final googleLetter = GoogleLetter.test(bloc: bloc);
await googleLetter.add(behavior);
await game.ensureAdd(googleLetter);
behavior.beginContact(MockBall(), MockContact());
verify(googleLetter.bloc.onBallContacted).called(1);
},
);
},
);
}

@ -0,0 +1,24 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'GoogleLetterCubit',
() {
blocTest<GoogleLetterCubit, GoogleLetterState>(
'onBallContacted emits active',
build: GoogleLetterCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [GoogleLetterState.active],
);
blocTest<GoogleLetterCubit, GoogleLetterState>(
'onReset emits inactive',
build: GoogleLetterCubit.new,
act: (bloc) => bloc.onReset(),
expect: () => [GoogleLetterState.inactive],
);
},
);
}

@ -1,11 +1,13 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame/effects.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_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/google_letter/behaviors/behaviors.dart';
import '../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -83,44 +85,35 @@ void main() {
expect(() => GoogleLetter(6), throwsA(isA<RangeError>())); expect(() => GoogleLetter(6), throwsA(isA<RangeError>()));
}); });
group('activate', () { // TODO(alestiago): Consider refactoring once the following is merged:
flameTester.test('returns normally', (game) async { // https://github.com/flame-engine/flame/pull/1538
final googleLetter = GoogleLetter(0); // ignore: public_member_api_docs
await game.ensureAdd(googleLetter); flameTester.test('closes bloc when removed', (game) async {
await expectLater(googleLetter.activate, returnsNormally); final bloc = MockGoogleLetterCubit();
}); whenListen(
bloc,
flameTester.test('adds an Effect', (game) async { const Stream<GoogleLetterState>.empty(),
final googleLetter = GoogleLetter(0); initialState: GoogleLetterState.active,
await game.ensureAdd(googleLetter); );
await googleLetter.activate(); when(bloc.close).thenAnswer((_) async {});
await game.ready(); final googleLetter = GoogleLetter.test(bloc: bloc);
expect( await game.ensureAdd(googleLetter);
googleLetter.descendants().whereType<Effect>().length, game.remove(googleLetter);
equals(1), await game.ready();
);
}); verify(bloc.close).called(1);
}); });
group('deactivate', () { flameTester.test('adds a GoogleLetterBallContactBehavior', (game) async {
flameTester.test('returns normally', (game) async { final googleLetter = GoogleLetter(0);
final googleLetter = GoogleLetter(0); await game.ensureAdd(googleLetter);
await game.ensureAdd(googleLetter); expect(
await expectLater(googleLetter.deactivate, returnsNormally); googleLetter.children
}); .whereType<GoogleLetterBallContactBehavior>()
.single,
flameTester.test('adds an Effect', (game) async { isNotNull,
final googleLetter = GoogleLetter(0); );
await game.ensureAdd(googleLetter);
await googleLetter.deactivate();
await game.ready();
expect(
googleLetter.descendants().whereType<Effect>().length,
equals(1),
);
});
}); });
}); });
} }

@ -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);
@ -113,7 +108,7 @@ void main() {
}); });
}); });
group('LayerSensorBallContactCallback', () { group('beginContact', () {
late Ball ball; late Ball ball;
late Body body; late Body body;
@ -135,18 +130,17 @@ 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()); sensor.beginContact(ball, 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()); sensor.beginContact(ball, 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 +155,17 @@ 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()); sensor.beginContact(ball, 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()); sensor.beginContact(ball, 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);

@ -151,5 +151,14 @@ void main() {
expect(spriteComponent.current, SignpostSpriteState.inactive); expect(spriteComponent.current, SignpostSpriteState.inactive);
}, },
); );
flameTester.test('adds new children', (game) async {
final component = Component();
final signpost = Signpost(
children: [component],
);
await game.ensureAdd(signpost);
expect(signpost.children, contains(component));
});
}); });
} }

@ -0,0 +1,48 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.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_components/src/components/sparky_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SparkyBumperBallContactBehavior',
() {
test('can be instantiated', () {
expect(
SparkyBumperBallContactBehavior(),
isA<SparkyBumperBallContactBehavior>(),
);
});
flameTester.test(
'beginContact emits onBallContacted when contacts with a ball',
(game) async {
final behavior = SparkyBumperBallContactBehavior();
final bloc = MockSparkyBumperCubit();
whenListen(
bloc,
const Stream<SparkyBumperState>.empty(),
initialState: SparkyBumperState.active,
);
final sparkyBumper = SparkyBumper.test(bloc: bloc);
await sparkyBumper.add(behavior);
await game.ensureAdd(sparkyBumper);
behavior.beginContact(MockBall(), MockContact());
verify(sparkyBumper.bloc.onBallContacted).called(1);
},
);
},
);
}

@ -0,0 +1,45 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.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_components/src/components/sparky_bumper/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SparkyBumperBlinkingBehavior',
() {
flameTester.testGameWidget(
'calls onBlinked after 0.05 seconds when inactive',
setUp: (game, tester) async {
final behavior = SparkyBumperBlinkingBehavior();
final bloc = MockSparkyBumperCubit();
final streamController = StreamController<SparkyBumperState>();
whenListen(
bloc,
streamController.stream,
initialState: SparkyBumperState.active,
);
final sparkyBumper = SparkyBumper.test(bloc: bloc);
await sparkyBumper.add(behavior);
await game.ensureAdd(sparkyBumper);
streamController.add(SparkyBumperState.inactive);
await tester.pump();
game.update(0.05);
await streamController.close();
verify(bloc.onBlinked).called(1);
},
);
},
);
}

@ -0,0 +1,24 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'SparkyBumperCubit',
() {
blocTest<SparkyBumperCubit, SparkyBumperState>(
'onBallContacted emits inactive',
build: SparkyBumperCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [SparkyBumperState.inactive],
);
blocTest<SparkyBumperCubit, SparkyBumperState>(
'onBlinked emits active',
build: SparkyBumperCubit.new,
act: (bloc) => bloc.onBlinked(),
expect: () => [SparkyBumperState.active],
);
},
);
}

@ -0,0 +1,86 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.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_components/src/components/sparky_bumper/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('SparkyBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final sparkyBumper = SparkyBumper.a();
await game.ensureAdd(sparkyBumper);
expect(game.contains(sparkyBumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final sparkyBumper = SparkyBumper.b();
await game.ensureAdd(sparkyBumper);
expect(game.contains(sparkyBumper), isTrue);
});
flameTester.test('"c" loads correctly', (game) async {
final sparkyBumper = SparkyBumper.c();
await game.ensureAdd(sparkyBumper);
expect(game.contains(sparkyBumper), isTrue);
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = MockSparkyBumperCubit();
whenListen(
bloc,
const Stream<SparkyBumperState>.empty(),
initialState: SparkyBumperState.active,
);
when(bloc.close).thenAnswer((_) async {});
final sparkyBumper = SparkyBumper.test(bloc: bloc);
await game.ensureAdd(sparkyBumper);
game.remove(sparkyBumper);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final sparkyBumper = SparkyBumper.a(
children: [component],
);
await game.ensureAdd(sparkyBumper);
expect(sparkyBumper.children, contains(component));
});
flameTester.test('a SparkyBumperBallContactBehavior', (game) async {
final sparkyBumper = SparkyBumper.a();
await game.ensureAdd(sparkyBumper);
expect(
sparkyBumper.children
.whereType<SparkyBumperBallContactBehavior>()
.single,
isNotNull,
);
});
});
});
}

@ -1,69 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('SparkyBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = SparkyBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"c" loads correctly', (game) async {
final bumper = SparkyBumper.c();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('animate switches between on and off sprites',
(game) async {
final bumper = SparkyBumper.a();
await game.ensureAdd(bumper);
final spriteGroupComponent = bumper.firstChild<SpriteGroupComponent>()!;
expect(
spriteGroupComponent.current,
equals(SparkyBumperSpriteState.active),
);
final future = bumper.animate();
expect(
spriteGroupComponent.current,
equals(SparkyBumperSpriteState.inactive),
);
await future;
expect(
spriteGroupComponent.current,
equals(SparkyBumperSpriteState.active),
);
});
});
}

@ -2,5 +2,7 @@ library pinball_flame;
export 'src/blueprint.dart'; export 'src/blueprint.dart';
export 'src/component_controller.dart'; export 'src/component_controller.dart';
export 'src/contact_behavior.dart';
export 'src/keyboard_input_controller.dart'; export 'src/keyboard_input_controller.dart';
export 'src/parent_is_a.dart';
export 'src/sprite_animation.dart'; export 'src/sprite_animation.dart';

@ -0,0 +1,95 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Appends a new [ContactCallbacks] to the parent.
///
/// This is a convenience class for adding a [ContactCallbacks] to the parent.
/// In constract with just adding a [ContactCallbacks] to the parent's body
/// userData, this class respects the previous [ContactCallbacks] in the
/// parent's body userData, if any. Hence, it avoids overriding any previous
/// [ContactCallbacks] in the parent.
///
/// It does so by grouping the [ContactCallbacks] in a [_ContactCallbacksGroup],
/// and resetting the parent's userData accordingly.
// TODO(alestiago): Make use of generics to infer the type of the contact.
// https://github.com/VGVentures/pinball/pull/234#discussion_r859182267
// TODO(alestiago): Consider if there is a need to support adjusting a fixture's
// userData.
class ContactBehavior<T extends BodyComponent> extends Component
with ContactCallbacks, ParentIsA<T> {
@override
@mustCallSuper
Future<void> onLoad() async {
final userData = parent.body.userData;
if (userData is _ContactCallbacksGroup) {
userData.addContactCallbacks(this);
} else if (userData is ContactCallbacks) {
final contactCallbacksGroup = _ContactCallbacksGroup()
..addContactCallbacks(userData)
..addContactCallbacks(this);
parent.body.userData = contactCallbacksGroup;
} else {
parent.body.userData = this;
}
}
}
class _ContactCallbacksGroup implements ContactCallbacks {
final List<ContactCallbacks> _contactCallbacks = [];
@override
@mustCallSuper
void beginContact(Object other, Contact contact) {
onBeginContact?.call(other, contact);
for (final callback in _contactCallbacks) {
callback.beginContact(other, contact);
}
}
@override
@mustCallSuper
void endContact(Object other, Contact contact) {
onEndContact?.call(other, contact);
for (final callback in _contactCallbacks) {
callback.endContact(other, contact);
}
}
@override
@mustCallSuper
void preSolve(Object other, Contact contact, Manifold oldManifold) {
onPreSolve?.call(other, contact, oldManifold);
for (final callback in _contactCallbacks) {
callback.preSolve(other, contact, oldManifold);
}
}
@override
@mustCallSuper
void postSolve(Object other, Contact contact, ContactImpulse impulse) {
onPostSolve?.call(other, contact, impulse);
for (final callback in _contactCallbacks) {
callback.postSolve(other, contact, impulse);
}
}
void addContactCallbacks(ContactCallbacks callback) {
_contactCallbacks.add(callback);
}
@override
void Function(Object other, Contact contact)? onBeginContact;
@override
void Function(Object other, Contact contact)? onEndContact;
@override
void Function(Object other, Contact contact, ContactImpulse impulse)?
onPostSolve;
@override
void Function(Object other, Contact contact, Manifold oldManifold)?
onPreSolve;
}

@ -0,0 +1,15 @@
import 'package:flame/components.dart';
// TODO(alestiago): Remove once the following is merged:
// https://github.com/flame-engine/flame/pull/1566
/// A mixin that ensures a parent is of the given type [T].
mixin ParentIsA<T extends Component> on Component {
@override
T get parent => super.parent! as T;
@override
Future<void>? addToParent(covariant T parent) {
return super.addToParent(parent);
}
}

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

@ -0,0 +1,153 @@
// 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';
class _TestBodyComponent extends BodyComponent {
@override
Body createBody() => world.createBody(BodyDef());
}
class _TestContactBehavior extends ContactBehavior {
int beginContactCallsCount = 0;
@override
void beginContact(Object other, Contact contact) {
beginContactCallsCount++;
super.beginContact(other, contact);
}
int endContactCallsCount = 0;
@override
void endContact(Object other, Contact contact) {
endContactCallsCount++;
super.endContact(other, contact);
}
int preSolveContactCallsCount = 0;
@override
void preSolve(Object other, Contact contact, Manifold oldManifold) {
preSolveContactCallsCount++;
super.preSolve(other, contact, oldManifold);
}
int postSolveContactCallsCount = 0;
@override
void postSolve(Object other, Contact contact, ContactImpulse impulse) {
postSolveContactCallsCount++;
super.postSolve(other, contact, impulse);
}
}
class _MockContactCallbacks extends Mock implements ContactCallbacks {}
class _MockContact extends Mock implements Contact {}
class _MockManifold extends Mock implements Manifold {}
class _MockContactImpulse extends Mock implements ContactImpulse {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
group('ContactBehavior', () {
late Object other;
late Contact contact;
late Manifold manifold;
late ContactImpulse contactImpulse;
setUp(() {
other = Object();
contact = _MockContact();
manifold = _MockManifold();
contactImpulse = _MockContactImpulse();
});
flameTester.test(
'should add a new ContactCallbacks to the parent',
(game) async {
final parent = _TestBodyComponent();
final contactBehavior = ContactBehavior();
await parent.add(contactBehavior);
await game.ensureAdd(parent);
expect(parent.body.userData, contactBehavior);
},
);
flameTester.test(
"should respect the previous ContactCallbacks in the parent's userData",
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final contactCallbacks1 = _MockContactCallbacks();
parent.body.userData = contactCallbacks1;
final contactBehavior = ContactBehavior();
await parent.ensureAdd(contactBehavior);
final contactCallbacks = parent.body.userData! as ContactCallbacks;
contactCallbacks.beginContact(other, contact);
verify(
() => contactCallbacks1.beginContact(other, contact),
).called(1);
contactCallbacks.endContact(other, contact);
verify(
() => contactCallbacks1.endContact(other, contact),
).called(1);
contactCallbacks.preSolve(other, contact, manifold);
verify(
() => contactCallbacks1.preSolve(other, contact, manifold),
).called(1);
contactCallbacks.postSolve(other, contact, contactImpulse);
verify(
() => contactCallbacks1.postSolve(other, contact, contactImpulse),
).called(1);
},
);
flameTester.test('can group multiple ContactBehaviors and keep listening',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final contactBehavior1 = _TestContactBehavior();
final contactBehavior2 = _TestContactBehavior();
final contactBehavior3 = _TestContactBehavior();
await parent.ensureAddAll([
contactBehavior1,
contactBehavior2,
contactBehavior3,
]);
final contactCallbacks = parent.body.userData! as ContactCallbacks;
contactCallbacks.beginContact(other, contact);
expect(contactBehavior1.beginContactCallsCount, equals(1));
expect(contactBehavior2.beginContactCallsCount, equals(1));
expect(contactBehavior3.beginContactCallsCount, equals(1));
contactCallbacks.endContact(other, contact);
expect(contactBehavior1.endContactCallsCount, equals(1));
expect(contactBehavior2.endContactCallsCount, equals(1));
expect(contactBehavior3.endContactCallsCount, equals(1));
contactCallbacks.preSolve(other, contact, manifold);
expect(contactBehavior1.preSolveContactCallsCount, equals(1));
expect(contactBehavior2.preSolveContactCallsCount, equals(1));
expect(contactBehavior3.preSolveContactCallsCount, equals(1));
contactCallbacks.postSolve(other, contact, contactImpulse);
expect(contactBehavior1.postSolveContactCallsCount, equals(1));
expect(contactBehavior2.postSolveContactCallsCount, equals(1));
expect(contactBehavior3.postSolveContactCallsCount, equals(1));
});
});
}

@ -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,13 +1,10 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:ui';
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';
import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -19,16 +16,16 @@ 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(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final alienZone = AlienZone(); await game.addFromBlueprint(AlienZone());
await game.ensureAdd(alienZone); await game.ready();
expect(game.contains(alienZone), isTrue);
}, },
); );
@ -37,68 +34,15 @@ void main() {
'two AlienBumper', 'two AlienBumper',
(game) async { (game) async {
final alienZone = AlienZone(); final alienZone = AlienZone();
await game.ensureAdd(alienZone); await game.addFromBlueprint(alienZone);
await game.ready();
expect( expect(
alienZone.descendants().whereType<AlienBumper>().length, game.descendants().whereType<AlienBumper>().length,
equals(2), equals(2),
); );
}, },
); );
}); });
group('bumpers', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
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,

@ -0,0 +1,84 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
void main() {
group('FlutterForestBonusBehavior', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'adds GameBonus.dashNest to the game when all bumpers are active',
setUp: (game, tester) async {
final behavior = FlutterForestBonusBehavior();
final parent = FlutterForest.test();
final bumpers = [
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
];
await parent.addAll(bumpers);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
for (final bumper in bumpers) {
bumper.bloc.onBallContacted();
}
await tester.pump();
verify(
() => gameBloc.add(const BonusActivated(GameBonus.dashNest)),
).called(1);
},
);
flameBlocTester.testGameWidget(
'adds a new ball to the game when all bumpers are active',
setUp: (game, tester) async {
final behavior = FlutterForestBonusBehavior();
final parent = FlutterForest.test();
final bumpers = [
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
DashNestBumper.test(bloc: DashNestBumperCubit()),
];
await parent.addAll(bumpers);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
for (final bumper in bumpers) {
bumper.bloc.onBallContacted();
}
await game.ready();
expect(
game.descendants().whereType<Ball>().single,
isNotNull,
);
},
);
});
}

@ -0,0 +1,80 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
Assets.images.dash.animatronic.keyName,
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(assets: assets),
);
group('FlutterForest', () {
flameTester.test(
'loads correctly',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(game.contains(flutterForest), isTrue);
},
);
group('loads', () {
flameTester.test(
'a Signpost',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.descendants().whereType<Signpost>().length,
equals(1),
);
},
);
flameTester.test(
'a DashAnimatronic',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.firstChild<DashAnimatronic>(),
isNotNull,
);
},
);
flameTester.test(
'three DashNestBumper',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.descendants().whereType<DashNestBumper>().length,
equals(3),
);
},
);
});
});
}

@ -1,177 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
Assets.images.dash.animatronic.keyName,
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets));
group('FlutterForest', () {
flameTester.test(
'loads correctly',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(game.contains(flutterForest), isTrue);
},
);
group('loads', () {
flameTester.test(
'a Signpost',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.descendants().whereType<Signpost>().length,
equals(1),
);
},
);
flameTester.test(
'a DashAnimatronic',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.firstChild<DashAnimatronic>(),
isNotNull,
);
},
);
flameTester.test(
'three DashNestBumper',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.descendants().whereType<DashNestBumper>().length,
equals(3),
);
},
);
});
group('bumpers', () {
late Ball ball;
late GameBloc gameBloc;
setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: () => EmptyPinballTestGame(assets),
blocBuilder: () {
gameBloc = MockGameBloc();
const state = GameState.initial();
whenListen(gameBloc, Stream.value(state), initialState: state);
return gameBloc;
},
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(
'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]);
final bumpers = flutterForest.children.whereType<DashNestBumper>();
expect(bumpers.length, equals(3));
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
await game.ready();
if (bumper == bumpers.last) {
verify(
() => gameBloc.add(const BonusActivated(GameBonus.dashNest)),
).called(1);
} else {
verifyNever(
() => gameBloc.add(const BonusActivated(GameBonus.dashNest)),
);
}
}
},
);
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]);
final bumpers = [
MockDashNestBumper(),
MockDashNestBumper(),
MockDashNestBumper(),
];
for (final bumper in bumpers) {
flutterForest.controller.activateBumper(bumper);
await game.ready();
if (bumper == bumpers.last) {
for (final bumper in bumpers) {
verify(bumper.deactivate).called(1);
}
}
}
},
);
});
});
}

@ -0,0 +1,61 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/components/google_word/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('GoogleWordBonusBehaviors', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'adds GameBonus.googleWord to the game when all letters are activated',
setUp: (game, tester) async {
final behavior = GoogleWordBonusBehavior();
final parent = GoogleWord.test();
final letters = [
GoogleLetter(0),
GoogleLetter(1),
GoogleLetter(2),
GoogleLetter(3),
GoogleLetter(4),
GoogleLetter(5),
];
await parent.addAll(letters);
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);
for (final letter in letters) {
letter.bloc.onBallContacted();
}
await tester.pump();
verify(
() => gameBloc.add(const BonusActivated(GameBonus.googleWord)),
).called(1);
},
);
});
}

@ -0,0 +1,26 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
group('GoogleWord', () {
flameTester.test(
'loads the letters correctly',
(game) async {
const word = 'Google';
final googleWord = GoogleWord(position: Vector2.zero());
await game.ensureAdd(googleWord);
final letters = googleWord.children.whereType<GoogleLetter>();
expect(letters.length, equals(word.length));
},
);
});
}

@ -1,73 +0,0 @@
// 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('GoogleWord', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameTester = FlameTester(EmptyPinballTestGame.new);
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);
flameTester.test(
'loads the letters correctly',
(game) async {
const word = 'Google';
final googleWord = GoogleWord(position: Vector2.zero());
await game.ensureAdd(googleWord);
final letters = googleWord.children.whereType<GoogleLetter>();
expect(letters.length, equals(word.length));
},
);
flameBlocTester.testGameWidget(
'adds GameBonus.googleWord to the game when all letters are activated',
setUp: (game, _) async {
final ball = Ball(baseColor: const Color(0xFFFF0000));
final googleWord = GoogleWord(position: Vector2.zero());
await game.ensureAddAll([googleWord, ball]);
final letters = googleWord.children.whereType<GoogleLetter>();
expect(letters, isNotEmpty);
for (final letter in letters) {
beginContact(game, letter, ball);
await game.ready();
if (letter == letters.last) {
verify(
() => gameBloc.add(const BonusActivated(GameBonus.googleWord)),
).called(1);
} else {
verifyNever(
() => gameBloc.add(const BonusActivated(GameBonus.googleWord)),
);
}
}
},
);
});
}

@ -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,111 @@
// 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';
class _TestBodyComponent extends BodyComponent {
@override
Body createBody() => world.createBody(BodyDef());
}
void main() {
group('ScoringBehavior', () {
group('beginContact', () {
late GameBloc bloc;
late PinballAudio audio;
late Ball ball;
late BodyComponent parent;
setUp(() {
audio = MockPinballAudio();
ball = MockBall();
final ballBody = MockBody();
when(() => ball.body).thenReturn(ballBody);
when(() => ballBody.position).thenReturn(Vector2.all(4));
parent = _TestBodyComponent();
});
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 scoringBehavior = ScoringBehavior(points: points);
await parent.add(scoringBehavior);
await game.ensureAdd(parent);
scoringBehavior.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 scoringBehavior = ScoringBehavior(points: points);
await parent.add(scoringBehavior);
await game.ensureAdd(parent);
scoringBehavior.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 scoringBehavior = ScoringBehavior(points: points);
await parent.add(scoringBehavior);
await game.ensureAdd(parent);
scoringBehavior.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,8 +1,5 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:ui';
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:mocktail/mocktail.dart';
@ -23,7 +20,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 {
@ -70,93 +70,40 @@ void main() {
}, },
); );
}); });
group('bumpers', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
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('SparkyComputerSensor', () {
flameTester.test('calls turboCharge', (game) async { flameTester.test('calls turboCharge', (game) async {
final callback = SparkyComputerSensorBallContactCallback(); final sensor = SparkyComputerSensor();
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(controller.turboCharge).thenAnswer((_) async {}); when(controller.turboCharge).thenAnswer((_) async {});
callback.begin(MockSparkyComputerSensor(), ball, MockContact()); await game.ensureAddAll([
sensor,
SparkyAnimatronic(),
]);
sensor.beginContact(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 sensor = SparkyComputerSensor();
final sparkyAnimatronic = SparkyAnimatronic();
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(controller.turboCharge).thenAnswer((_) async {}); when(controller.turboCharge).thenAnswer((_) async {});
await game.ensureAddAll([
final sparkyFireZone = SparkyFireZone(); sensor,
await game.addFromBlueprint(sparkyFireZone); sparkyAnimatronic,
await game.ready(); ]);
final sparkyAnimatronic =
sparkyFireZone.components.whereType<SparkyAnimatronic>().single;
expect(sparkyAnimatronic.playing, isFalse); expect(sparkyAnimatronic.playing, isFalse);
callback.begin(MockSparkyComputerSensor(), ball, MockContact()); sensor.beginContact(ball, MockContact());
expect(sparkyAnimatronic.playing, isTrue); expect(sparkyAnimatronic.playing, isTrue);
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save