Merge branch 'main' into chore/update-theme-style-files

pull/193/head
arturplaczek 3 years ago committed by GitHub
commit d76df61ae7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,20 @@
name: pinball_flame
on:
push:
paths:
- "packages/pinball_flame/**"
- ".github/workflows/pinball_flame.yaml"
pull_request:
paths:
- "packages/pinball_flame/**"
- ".github/workflows/pinball_flame.yaml"
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
working_directory: packages/pinball_flame
coverage_excludes: "lib/gen/*.dart"
test_optimization: false

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

@ -11,14 +11,10 @@ class GameBloc extends Bloc<GameEvent, GameState> {
GameBloc() : super(const GameState.initial()) { GameBloc() : super(const GameState.initial()) {
on<BallLost>(_onBallLost); on<BallLost>(_onBallLost);
on<Scored>(_onScored); on<Scored>(_onScored);
on<BonusLetterActivated>(_onBonusLetterActivated); on<BonusActivated>(_onBonusActivated);
on<DashNestActivated>(_onDashNestActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated); on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
} }
static const bonusWord = 'GOOGLE';
static const bonusWordScore = 10000;
void _onBallLost(BallLost event, Emitter emit) { void _onBallLost(BallLost event, Emitter emit) {
emit(state.copyWith(balls: state.balls - 1)); emit(state.copyWith(balls: state.balls - 1));
} }
@ -29,54 +25,12 @@ class GameBloc extends Bloc<GameEvent, GameState> {
} }
} }
void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) { void _onBonusActivated(BonusActivated event, Emitter emit) {
final newBonusLetters = [ emit(
...state.activatedBonusLetters, state.copyWith(
event.letterIndex, bonusHistory: [...state.bonusHistory, event.bonus],
]; ),
);
final achievedBonus = newBonusLetters.length == bonusWord.length;
if (achievedBonus) {
emit(
state.copyWith(
activatedBonusLetters: [],
bonusHistory: [
...state.bonusHistory,
GameBonus.word,
],
),
);
add(const Scored(points: bonusWordScore));
} else {
emit(
state.copyWith(activatedBonusLetters: newBonusLetters),
);
}
}
void _onDashNestActivated(DashNestActivated event, Emitter emit) {
final newNests = {
...state.activatedDashNests,
event.nestId,
};
final achievedBonus = newNests.length == 3;
if (achievedBonus) {
emit(
state.copyWith(
balls: state.balls + 1,
activatedDashNests: {},
bonusHistory: [
...state.bonusHistory,
GameBonus.dashNest,
],
),
);
} else {
emit(
state.copyWith(activatedDashNests: newNests),
);
}
} }
Future<void> _onSparkyTurboChargeActivated( Future<void> _onSparkyTurboChargeActivated(

@ -33,26 +33,13 @@ class Scored extends GameEvent {
List<Object?> get props => [points]; List<Object?> get props => [points];
} }
class BonusLetterActivated extends GameEvent { class BonusActivated extends GameEvent {
const BonusLetterActivated(this.letterIndex) const BonusActivated(this.bonus);
: assert(
letterIndex < GameBloc.bonusWord.length,
'Index must be smaller than the length of the word',
);
final int letterIndex; final GameBonus bonus;
@override @override
List<Object?> get props => [letterIndex]; List<Object?> get props => [bonus];
}
class DashNestActivated extends GameEvent {
const DashNestActivated(this.nestId);
final String nestId;
@override
List<Object?> get props => [nestId];
} }
class SparkyTurboChargeActivated extends GameEvent { class SparkyTurboChargeActivated extends GameEvent {

@ -4,9 +4,8 @@ part of 'game_bloc.dart';
/// Defines bonuses that a player can gain during a PinballGame. /// Defines bonuses that a player can gain during a PinballGame.
enum GameBonus { enum GameBonus {
/// Bonus achieved when the user activate all of the bonus /// Bonus achieved when the ball activates all Google letters.
/// letters on the board, forming the bonus word. googleWord,
word,
/// Bonus achieved when the user activates all dash nest bumpers. /// Bonus achieved when the user activates all dash nest bumpers.
dashNest, dashNest,
@ -23,17 +22,13 @@ class GameState extends Equatable {
const GameState({ const GameState({
required this.score, required this.score,
required this.balls, required this.balls,
required this.activatedBonusLetters,
required this.bonusHistory, required this.bonusHistory,
required this.activatedDashNests,
}) : assert(score >= 0, "Score can't be negative"), }) : assert(score >= 0, "Score can't be negative"),
assert(balls >= 0, "Number of balls can't be negative"); assert(balls >= 0, "Number of balls can't be negative");
const GameState.initial() const GameState.initial()
: score = 0, : score = 0,
balls = 3, balls = 3,
activatedBonusLetters = const [],
activatedDashNests = const {},
bonusHistory = const []; bonusHistory = const [];
/// The current score of the game. /// The current score of the game.
@ -44,12 +39,6 @@ class GameState extends Equatable {
/// When the number of balls is 0, the game is over. /// When the number of balls is 0, the game is over.
final int balls; final int balls;
/// Active bonus letters.
final List<int> activatedBonusLetters;
/// Active dash nests.
final Set<String> activatedDashNests;
/// Holds the history of all the [GameBonus]es earned by the player during a /// Holds the history of all the [GameBonus]es earned by the player during a
/// PinballGame. /// PinballGame.
final List<GameBonus> bonusHistory; final List<GameBonus> bonusHistory;
@ -57,15 +46,9 @@ class GameState extends Equatable {
/// Determines when the game is over. /// Determines when the game is over.
bool get isGameOver => balls == 0; bool get isGameOver => balls == 0;
/// Shortcut method to check if the given [i]
/// is activated.
bool isLetterActivated(int i) => activatedBonusLetters.contains(i);
GameState copyWith({ GameState copyWith({
int? score, int? score,
int? balls, int? balls,
List<int>? activatedBonusLetters,
Set<String>? activatedDashNests,
List<GameBonus>? bonusHistory, List<GameBonus>? bonusHistory,
}) { }) {
assert( assert(
@ -76,9 +59,6 @@ class GameState extends Equatable {
return GameState( return GameState(
score: score ?? this.score, score: score ?? this.score,
balls: balls ?? this.balls, balls: balls ?? this.balls,
activatedBonusLetters:
activatedBonusLetters ?? this.activatedBonusLetters,
activatedDashNests: activatedDashNests ?? this.activatedDashNests,
bonusHistory: bonusHistory ?? this.bonusHistory, bonusHistory: bonusHistory ?? this.bonusHistory,
); );
} }
@ -87,8 +67,6 @@ class GameState extends Equatable {
List<Object?> get props => [ List<Object?> get props => [
score, score,
balls, balls,
activatedBonusLetters,
activatedDashNests,
bonusHistory, bonusHistory,
]; ];
} }

@ -3,9 +3,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.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]

@ -1,208 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template bonus_word}
/// Loads all [BonusLetter]s to compose a [BonusWord].
/// {@endtemplate}
class BonusWord extends Component
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
/// {@macro bonus_word}
BonusWord({required Vector2 position}) : _position = position;
final Vector2 _position;
@override
bool listenWhen(GameState? previousState, GameState newState) {
return (previousState?.bonusHistory.length ?? 0) <
newState.bonusHistory.length &&
newState.bonusHistory.last == GameBonus.word;
}
@override
void onNewState(GameState state) {
if (state.bonusHistory.last == GameBonus.word) {
gameRef.audio.googleBonus();
final letters = children.whereType<BonusLetter>().toList();
for (var i = 0; i < letters.length; i++) {
final letter = letters[i];
letter
..isEnabled = false
..add(
SequenceEffect(
[
ColorEffect(
i.isOdd
? BonusLetter._activeColor
: BonusLetter._disableColor,
const Offset(0, 1),
EffectController(duration: 0.25),
),
ColorEffect(
i.isOdd
? BonusLetter._disableColor
: BonusLetter._activeColor,
const Offset(0, 1),
EffectController(duration: 0.25),
),
],
repeatCount: 4,
)..onFinishCallback = () {
letter
..isEnabled = true
..add(
ColorEffect(
BonusLetter._disableColor,
const Offset(0, 1),
EffectController(duration: 0.25),
),
);
},
);
}
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
final offsets = [
Vector2(-12.92, 1.82),
Vector2(-8.33, -0.65),
Vector2(-2.88, -1.75),
];
offsets.addAll(
offsets.reversed
.map(
(offset) => Vector2(-offset.x, offset.y),
)
.toList(),
);
assert(offsets.length == GameBloc.bonusWord.length, 'Invalid positions');
final letters = <BonusLetter>[];
for (var i = 0; i < GameBloc.bonusWord.length; i++) {
letters.add(
BonusLetter(
letter: GameBloc.bonusWord[i],
index: i,
)..initialPosition = _position + offsets[i],
);
}
await addAll(letters);
}
}
/// {@template bonus_letter}
/// [BodyType.static] sensor component, part of a word bonus,
/// which will activate its letter after contact with a [Ball].
/// {@endtemplate}
class BonusLetter extends BodyComponent<PinballGame>
with BlocComponent<GameBloc, GameState>, InitialPosition {
/// {@macro bonus_letter}
BonusLetter({
required String letter,
required int index,
}) : _letter = letter,
_index = index {
paint = Paint()..color = _disableColor;
}
/// The size of the [BonusLetter].
static final size = Vector2.all(3.7);
static const _activeColor = Colors.green;
static const _disableColor = Colors.red;
final String _letter;
final int _index;
/// Indicates if a [BonusLetter] can be activated on [Ball] contact.
///
/// It is disabled whilst animating and enabled again once the animation
/// completes. The animation is triggered when [GameBonus.word] is
/// awarded.
bool isEnabled = true;
@override
Future<void> onLoad() async {
await super.onLoad();
await add(
TextComponent(
position: Vector2(-1, -1),
text: _letter,
textRenderer: TextPaint(
style: const TextStyle(fontSize: 2, color: Colors.white),
),
),
);
}
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(shape)..isSensor = true;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this
..type = BodyType.static;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
bool listenWhen(GameState? previousState, GameState newState) {
final wasActive = previousState?.isLetterActivated(_index) ?? false;
final isActive = newState.isLetterActivated(_index);
return wasActive != isActive;
}
@override
void onNewState(GameState state) {
final isActive = state.isLetterActivated(_index);
add(
ColorEffect(
isActive ? _activeColor : _disableColor,
const Offset(0, 1),
EffectController(duration: 0.25),
),
);
}
/// Activates this [BonusLetter], if it's not already activated.
void activate() {
final isActive = state?.isLetterActivated(_index) ?? false;
if (!isActive) {
gameRef.read<GameBloc>().add(BonusLetterActivated(_index));
}
}
}
/// Triggers [BonusLetter.activate] method when a [BonusLetter] and a [Ball]
/// come in contact.
class BonusLetterBallContactCallback
extends ContactCallback<Ball, BonusLetter> {
@override
void begin(Ball ball, BonusLetter bonusLetter, Contact contact) {
if (bonusLetter.isEnabled) {
bonusLetter.activate();
}
}
}

@ -1,7 +1,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds helpers methods to Flame's [Camera] /// Adds helpers methods to Flame's [Camera]
extension CameraX on Camera { extension CameraX on Camera {

@ -1,6 +1,5 @@
export 'alien_zone.dart'; export 'alien_zone.dart';
export 'board.dart'; export 'board.dart';
export 'bonus_word.dart';
export 'camera_controller.dart'; export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
@ -8,6 +7,7 @@ export 'controlled_plunger.dart';
export 'controlled_sparky_computer.dart'; export 'controlled_sparky_computer.dart';
export 'flutter_forest.dart'; export 'flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_flow_controller.dart';
export 'google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'score_effect_controller.dart'; export 'score_effect_controller.dart';
export 'score_points.dart'; export 'score_points.dart';

@ -1,9 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.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_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
/// {@template controlled_ball} /// {@template controlled_ball}

@ -1,9 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/flame/flame.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 controlled_flipper} /// {@template controlled_flipper}
/// A [Flipper] with a [FlipperController] attached. /// A [Flipper] with a [FlipperController] attached.

@ -1,9 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/flame/flame.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 controlled_plunger} /// {@template controlled_plunger}
/// A [Plunger] with a [PlungerController] attached. /// A [Plunger] with a [PlungerController] attached.

@ -3,9 +3,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.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 controlled_sparky_computer} /// {@template controlled_sparky_computer}
/// [SparkyComputer] with a [SparkyComputerController] attached. /// [SparkyComputer] with a [SparkyComputerController] attached.

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

@ -1,8 +1,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/flame/flame.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 game_flow_controller} /// {@template game_flow_controller}
/// A [Component] that controls the game over and game restart logic /// A [Component] that controls the game over and game restart logic

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

@ -1,6 +1,7 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/components/components.dart'; import 'package:pinball/game/components/components.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
/// {@template launcher} /// {@template launcher}
/// A [Blueprint] which creates the [Plunger], [RocketSpriteComponent] and /// A [Blueprint] which creates the [Plunger], [RocketSpriteComponent] and

@ -2,9 +2,9 @@ import 'dart:math';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/flame/flame.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 score_effect_controller} /// {@template score_effect_controller}
/// A [ComponentController] responsible for adding [ScoreText]s /// A [ComponentController] responsible for adding [ScoreText]s

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

@ -3,9 +3,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.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 sparky_fire_zone} /// {@template sparky_fire_zone}
/// Area positioned at the top left of the [Board] where the [Ball] /// Area positioned at the top left of the [Board] where the [Ball]

@ -58,6 +58,12 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.sparky.bumper.c.inactive.keyName), images.load(components.Assets.images.sparky.bumper.c.inactive.keyName),
images.load(components.Assets.images.backboard.backboardScores.keyName), images.load(components.Assets.images.backboard.backboardScores.keyName),
images.load(components.Assets.images.backboard.backboardGameOver.keyName), images.load(components.Assets.images.backboard.backboardGameOver.keyName),
images.load(components.Assets.images.googleWord.letter1.keyName),
images.load(components.Assets.images.googleWord.letter2.keyName),
images.load(components.Assets.images.googleWord.letter3.keyName),
images.load(components.Assets.images.googleWord.letter4.keyName),
images.load(components.Assets.images.googleWord.letter5.keyName),
images.load(components.Assets.images.googleWord.letter6.keyName),
images.load(components.Assets.images.backboard.display.keyName), images.load(components.Assets.images.backboard.display.keyName),
images.load(Assets.images.components.background.path), images.load(Assets.images.components.background.path),
]; ];

@ -5,11 +5,11 @@ import 'package:flame/components.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class PinballGame extends Forge2DGame class PinballGame extends Forge2DGame
@ -72,7 +72,6 @@ class PinballGame extends Forge2DGame
void _addContactCallbacks() { void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback(this)); addContactCallback(BallScorePointsCallback(this));
addContactCallback(BottomWallBallContactCallback()); addContactCallback(BottomWallBallContactCallback());
addContactCallback(BonusLetterBallContactCallback());
} }
Future<void> _addGameBoundaries() async { Future<void> _addGameBoundaries() async {
@ -82,7 +81,7 @@ class PinballGame extends Forge2DGame
Future<void> _addBonusWord() async { Future<void> _addBonusWord() async {
await add( await add(
BonusWord( GoogleWord(
position: Vector2( position: Vector2(
BoardDimensions.bounds.center.dx - 4.1, BoardDimensions.bounds.center.dx - 4.1,
BoardDimensions.bounds.center.dy + 1.8, BoardDimensions.bounds.center.dy + 1.8,

@ -72,13 +72,14 @@ class AlienBumper extends BodyComponent with InitialPosition {
majorRadius: _majorRadius, majorRadius: _majorRadius,
minorRadius: _minorRadius, minorRadius: _minorRadius,
)..rotate(1.29); )..rotate(1.29);
final fixtureDef = FixtureDef(shape) final fixtureDef = FixtureDef(
..friction = 0 shape,
..restitution = 4; restitution: 4,
);
final bodyDef = BodyDef() final bodyDef = BodyDef(
..position = initialPosition position: initialPosition,
..userData = this; userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Signature for the callback called when the used has /// Signature for the callback called when the used has
/// submettied their initials on the [BackboardGameOver] /// submettied their initials on the [BackboardGameOver]

@ -5,6 +5,7 @@ import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template backboard_letter_prompt} /// {@template backboard_letter_prompt}
/// A [PositionComponent] that renders a letter prompt used /// A [PositionComponent] that renders a letter prompt used

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

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

@ -3,6 +3,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template boundaries} /// {@template boundaries}
/// A [Blueprint] which creates the [_BottomBoundary] and [_OuterBoundary]. /// A [Blueprint] which creates the [_BottomBoundary] and [_OuterBoundary].
@ -23,7 +24,13 @@ class Boundaries extends Forge2DBlueprint {
/// {@endtemplate bottom_boundary} /// {@endtemplate bottom_boundary}
class _BottomBoundary extends BodyComponent with InitialPosition { class _BottomBoundary extends BodyComponent with InitialPosition {
/// {@macro bottom_boundary} /// {@macro bottom_boundary}
_BottomBoundary() : super(priority: 1); _BottomBoundary()
: super(
priority: 1,
children: [_BottomBoundarySpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[]; final fixturesDefs = <FixtureDef>[];
@ -59,13 +66,6 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_BottomBoundarySpriteComponent());
}
} }
class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
@ -88,7 +88,13 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
/// {@endtemplate outer_boundary} /// {@endtemplate outer_boundary}
class _OuterBoundary extends BodyComponent with InitialPosition { class _OuterBoundary extends BodyComponent with InitialPosition {
/// {@macro outer_boundary} /// {@macro outer_boundary}
_OuterBoundary() : super(priority: Ball.launchRampPriority - 1); _OuterBoundary()
: super(
priority: Ball.launchRampPriority - 1,
children: [_OuterBoundarySpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[]; final fixturesDefs = <FixtureDef>[];
@ -130,13 +136,6 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_OuterBoundarySpriteComponent());
}
} }
class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef { class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef {

@ -54,12 +54,14 @@ class ChromeDino extends BodyComponent with InitialPosition {
// TODO(alestiago): Subject to change when sprites are added. // TODO(alestiago): Subject to change when sprites are added.
final box = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2); final box = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2);
final fixtureDef = FixtureDef(box) final fixtureDef = FixtureDef(
..shape = box box,
..density = 999 density: 999,
..friction = 0.3 friction: 0.3,
..restitution = 0.1 restitution: 0.1,
..isSensor = true; isSensor: true,
);
fixtureDefs.add(fixtureDef); fixtureDefs.add(fixtureDef);
// FIXME(alestiago): Investigate why adding these fixtures is considered as // FIXME(alestiago): Investigate why adding these fixtures is considered as
@ -93,10 +95,11 @@ class ChromeDino extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..gravityScale = Vector2.zero() position: initialPosition,
..position = initialPosition type: BodyType.dynamic,
..type = BodyType.dynamic; gravityScale: Vector2.zero(),
);
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
@ -111,10 +114,7 @@ class ChromeDino extends BodyComponent with InitialPosition {
class _ChromeDinoAnchor extends JointAnchor { class _ChromeDinoAnchor extends JointAnchor {
/// {@macro flipper_anchor} /// {@macro flipper_anchor}
_ChromeDinoAnchor() { _ChromeDinoAnchor() {
initialPosition = Vector2( initialPosition = Vector2(ChromeDino.size.x / 2, 0);
ChromeDino.size.x / 2,
0,
);
} }
} }

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

@ -79,10 +79,10 @@ class BigDashNestBumper extends DashNestBumper {
minorRadius: 3.75, minorRadius: 3.75,
)..rotate(math.pi / 1.9); )..rotate(math.pi / 1.9);
final fixtureDef = FixtureDef(shape); final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
final bodyDef = BodyDef() position: initialPosition,
..position = initialPosition userData: this,
..userData = this; );
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
@ -130,13 +130,14 @@ class SmallDashNestBumper extends DashNestBumper {
majorRadius: 3, majorRadius: 3,
minorRadius: 2.25, minorRadius: 2.25,
)..rotate(math.pi / 2); )..rotate(math.pi / 2);
final fixtureDef = FixtureDef(shape) final fixtureDef = FixtureDef(
..friction = 0 shape,
..restitution = 4; restitution: 4,
);
final bodyDef = BodyDef() final bodyDef = BodyDef(
..position = initialPosition position: initialPosition,
..userData = this; userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }

@ -6,6 +6,7 @@ import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
/// {@template dinowalls} /// {@template dinowalls}
/// A [Blueprint] which creates walls for the [ChromeDino]. /// A [Blueprint] which creates walls for the [ChromeDino].
@ -28,7 +29,13 @@ class DinoWalls extends Forge2DBlueprint {
/// {@endtemplate} /// {@endtemplate}
class _DinoTopWall extends BodyComponent with InitialPosition { class _DinoTopWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall} ///{@macro dino_top_wall}
_DinoTopWall() : super(priority: 1); _DinoTopWall()
: super(
priority: 1,
children: [_DinoTopWallSpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
@ -81,10 +88,11 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition userData: this,
..type = BodyType.static; );
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach( _createFixtureDefs().forEach(
(fixture) => body.createFixture( (fixture) => body.createFixture(
@ -96,14 +104,6 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_DinoTopWallSpriteComponent());
}
} }
class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef {
@ -124,10 +124,16 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef {
/// {@endtemplate} /// {@endtemplate}
class _DinoBottomWall extends BodyComponent with InitialPosition { class _DinoBottomWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall} ///{@macro dino_top_wall}
_DinoBottomWall(); _DinoBottomWall()
: super(
children: [_DinoBottomWallSpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
const restitution = 1.0;
final topStraightControlPoints = [ final topStraightControlPoints = [
Vector2(32.4, -8.3), Vector2(32.4, -8.3),
@ -138,7 +144,10 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
topStraightControlPoints.first, topStraightControlPoints.first,
topStraightControlPoints.last, topStraightControlPoints.last,
); );
final topStraightFixtureDef = FixtureDef(topStraightShape); final topStraightFixtureDef = FixtureDef(
topStraightShape,
restitution: restitution,
);
fixturesDef.add(topStraightFixtureDef); fixturesDef.add(topStraightFixtureDef);
final topLeftCurveControlPoints = [ final topLeftCurveControlPoints = [
@ -149,7 +158,11 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
final topLeftCurveShape = BezierCurveShape( final topLeftCurveShape = BezierCurveShape(
controlPoints: topLeftCurveControlPoints, controlPoints: topLeftCurveControlPoints,
); );
fixturesDef.add(FixtureDef(topLeftCurveShape)); final topLeftCurveFixtureDef = FixtureDef(
topLeftCurveShape,
restitution: restitution,
);
fixturesDef.add(topLeftCurveFixtureDef);
final bottomLeftStraightControlPoints = [ final bottomLeftStraightControlPoints = [
topLeftCurveControlPoints.last, topLeftCurveControlPoints.last,
@ -160,7 +173,10 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
bottomLeftStraightControlPoints.first, bottomLeftStraightControlPoints.first,
bottomLeftStraightControlPoints.last, bottomLeftStraightControlPoints.last,
); );
final bottomLeftStraightFixtureDef = FixtureDef(bottomLeftStraightShape); final bottomLeftStraightFixtureDef = FixtureDef(
bottomLeftStraightShape,
restitution: restitution,
);
fixturesDef.add(bottomLeftStraightFixtureDef); fixturesDef.add(bottomLeftStraightFixtureDef);
final bottomStraightControlPoints = [ final bottomStraightControlPoints = [
@ -172,7 +188,10 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
bottomStraightControlPoints.first, bottomStraightControlPoints.first,
bottomStraightControlPoints.last, bottomStraightControlPoints.last,
); );
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); final bottomStraightFixtureDef = FixtureDef(
bottomStraightShape,
restitution: restitution,
);
fixturesDef.add(bottomStraightFixtureDef); fixturesDef.add(bottomStraightFixtureDef);
return fixturesDef; return fixturesDef;
@ -180,30 +199,16 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition userData: this,
..type = BodyType.static; );
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach( _createFixtureDefs().forEach(body.createFixture);
(fixture) => body.createFixture(
fixture
..restitution = 0.1
..friction = 0,
),
);
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_DinoBottomWallSpriteComponent());
}
} }
class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef {

@ -13,7 +13,11 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
/// {@macro flipper} /// {@macro flipper}
Flipper({ Flipper({
required this.side, required this.side,
}); }) : super(
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);
@ -99,9 +103,11 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
Vector2(smallCircleShape.position.x, -smallCircleShape.radius), Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
]; ];
final trapezium = PolygonShape()..set(trapeziumVertices); final trapezium = PolygonShape()..set(trapeziumVertices);
final trapeziumFixtureDef = FixtureDef(trapezium) final trapeziumFixtureDef = FixtureDef(
..density = 50.0 // TODO(alestiago): Use a proper density. trapezium,
..friction = .1; // TODO(alestiago): Use a proper friction. density: 50, // TODO(alestiago): Use a proper density.
friction: .1, // TODO(alestiago): Use a proper friction.
);
fixturesDef.add(trapeziumFixtureDef); fixturesDef.add(trapeziumFixtureDef);
return fixturesDef; return fixturesDef;
@ -110,18 +116,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
renderBody = false;
await _anchorToJoint(); await _anchorToJoint();
await add(_FlipperSpriteComponent(side: side));
} }
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..position = initialPosition position: initialPosition,
..gravityScale = Vector2.zero() gravityScale: Vector2.zero(),
..type = BodyType.dynamic; type: BodyType.dynamic,
);
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);

@ -6,19 +6,21 @@ import 'package:pinball_components/pinball_components.dart';
/// A sign, found in the Flutter Forest. /// A sign, found in the Flutter Forest.
/// {@endtemplate} /// {@endtemplate}
class FlutterSignPost extends BodyComponent with InitialPosition { class FlutterSignPost extends BodyComponent with InitialPosition {
@override /// {@macro flutter_sign_post}
Future<void> onLoad() async { FlutterSignPost()
await super.onLoad(); : super(
children: [_FlutterSignPostSpriteComponent()],
) {
renderBody = false; renderBody = false;
await add(_FlutterSignPostSpriteComponent());
} }
@override @override
Body createBody() { Body createBody() {
final shape = CircleShape()..radius = 0.25; final shape = CircleShape()..radius = 0.25;
final fixtureDef = FixtureDef(shape); final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()..position = initialPosition; final bodyDef = BodyDef(
position: initialPosition,
);
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }

@ -33,12 +33,14 @@ class GoogleLetter extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final shape = CircleShape()..radius = 1.85; final shape = CircleShape()..radius = 1.85;
final fixtureDef = FixtureDef(shape)..isSensor = true; final fixtureDef = FixtureDef(
shape,
final bodyDef = BodyDef() isSensor: true,
..position = initialPosition );
..userData = this final bodyDef = BodyDef(
..type = BodyType.static; position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }

@ -22,7 +22,9 @@ class JointAnchor extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef()..position = initialPosition; final bodyDef = BodyDef(
position: initialPosition,
);
return world.createBody(bodyDef); return world.createBody(bodyDef);
} }
} }

@ -16,7 +16,12 @@ class Kicker extends BodyComponent with InitialPosition {
/// {@macro kicker} /// {@macro kicker}
Kicker({ Kicker({
required BoardSide side, required BoardSide side,
}) : _side = side; }) : _side = side,
super(
children: [_KickerSpriteComponent(side: side)],
) {
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);
@ -35,7 +40,7 @@ class Kicker extends BodyComponent with InitialPosition {
final upperCircle = CircleShape()..radius = 1.6; final upperCircle = CircleShape()..radius = 1.6;
upperCircle.position.setValues(0, upperCircle.radius / 2); upperCircle.position.setValues(0, upperCircle.radius / 2);
final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0; final upperCircleFixtureDef = FixtureDef(upperCircle);
fixturesDefs.add(upperCircleFixtureDef); fixturesDefs.add(upperCircleFixtureDef);
final lowerCircle = CircleShape()..radius = 1.6; final lowerCircle = CircleShape()..radius = 1.6;
@ -43,7 +48,7 @@ class Kicker extends BodyComponent with InitialPosition {
size.x * -direction, size.x * -direction,
size.y + 0.8, size.y + 0.8,
); );
final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0; final lowerCircleFixtureDef = FixtureDef(lowerCircle);
fixturesDefs.add(lowerCircleFixtureDef); fixturesDefs.add(lowerCircleFixtureDef);
final wallFacingEdge = EdgeShape() final wallFacingEdge = EdgeShape()
@ -55,7 +60,7 @@ class Kicker extends BodyComponent with InitialPosition {
), ),
Vector2(2.5 * direction, size.y - 2), Vector2(2.5 * direction, size.y - 2),
); );
final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0; final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge);
fixturesDefs.add(wallFacingLineFixtureDef); fixturesDefs.add(wallFacingLineFixtureDef);
final bottomEdge = EdgeShape() final bottomEdge = EdgeShape()
@ -67,7 +72,7 @@ class Kicker extends BodyComponent with InitialPosition {
lowerCircle.radius * math.sin(quarterPi), lowerCircle.radius * math.sin(quarterPi),
), ),
); );
final bottomLineFixtureDef = FixtureDef(bottomEdge)..friction = 0; final bottomLineFixtureDef = FixtureDef(bottomEdge);
fixturesDefs.add(bottomLineFixtureDef); fixturesDefs.add(bottomLineFixtureDef);
final bouncyEdge = EdgeShape() final bouncyEdge = EdgeShape()
@ -84,10 +89,11 @@ class Kicker extends BodyComponent with InitialPosition {
), ),
); );
final bouncyFixtureDef = FixtureDef(bouncyEdge) final bouncyFixtureDef = FixtureDef(
bouncyEdge,
// TODO(alestiago): Play with restitution value once game is bundled. // TODO(alestiago): Play with restitution value once game is bundled.
..restitution = 10.0 restitution: 10,
..friction = 0; );
fixturesDefs.add(bouncyFixtureDef); fixturesDefs.add(bouncyFixtureDef);
// TODO(alestiago): Evaluate if there is value on centering the fixtures. // TODO(alestiago): Evaluate if there is value on centering the fixtures.
@ -111,19 +117,14 @@ class Kicker extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef()..position = initialPosition; final bodyDef = BodyDef(
position: initialPosition,
);
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_KickerSpriteComponent(side: _side));
}
} }
class _KickerSpriteComponent extends SpriteComponent with HasGameRef { class _KickerSpriteComponent extends SpriteComponent with HasGameRef {

@ -5,6 +5,7 @@ import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template launch_ramp} /// {@template launch_ramp}
/// A [Blueprint] which creates the [_LaunchRampBase] and /// A [Blueprint] which creates the [_LaunchRampBase] and
@ -14,7 +15,7 @@ class LaunchRamp extends Forge2DBlueprint {
@override @override
void build(_) { void build(_) {
addAllContactCallback([ addAllContactCallback([
RampOpeningBallContactCallback<_LaunchRampExit>(), LayerSensorBallContactCallback<_LaunchRampExit>(),
]); ]);
final launchRampBase = _LaunchRampBase(); final launchRampBase = _LaunchRampBase();
@ -103,9 +104,10 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition; userData: this,
);
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
@ -234,10 +236,10 @@ class _LaunchRampCloseWall extends BodyComponent with InitialPosition, Layered {
} }
/// {@template launch_ramp_exit} /// {@template launch_ramp_exit}
/// [RampOpening] with [Layer.launcher] to filter [Ball]s exiting the /// [LayerSensor] with [Layer.launcher] to filter [Ball]s exiting the
/// [LaunchRamp]. /// [LaunchRamp].
/// {@endtemplate} /// {@endtemplate}
class _LaunchRampExit extends RampOpening { class _LaunchRampExit extends LayerSensor {
/// {@macro launch_ramp_exit} /// {@macro launch_ramp_exit}
_LaunchRampExit({ _LaunchRampExit({
required double rotation, required double rotation,
@ -245,7 +247,7 @@ class _LaunchRampExit extends RampOpening {
super( super(
insideLayer: Layer.launcher, insideLayer: Layer.launcher,
outsideLayer: Layer.board, outsideLayer: Layer.board,
orientation: RampOrientation.down, orientation: LayerEntranceOrientation.down,
insidePriority: Ball.launchRampPriority, insidePriority: Ball.launchRampPriority,
outsidePriority: 0, outsidePriority: 0,
) { ) {

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

@ -33,11 +33,12 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
final fixtureDef = FixtureDef(shape)..density = 80; final fixtureDef = FixtureDef(shape)..density = 80;
final bodyDef = BodyDef() final bodyDef = BodyDef(
..position = initialPosition position: initialPosition,
..userData = this userData: this,
..type = BodyType.dynamic type: BodyType.dynamic,
..gravityScale = Vector2.zero(); gravityScale: Vector2.zero(),
);
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }

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

@ -3,6 +3,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template slingshots} /// {@template slingshots}
/// A [Blueprint] which creates the pair of [Slingshot]s on the right side of /// A [Blueprint] which creates the pair of [Slingshot]s on the right side of
@ -41,27 +42,29 @@ class Slingshot extends BodyComponent with InitialPosition {
required String spritePath, required String spritePath,
}) : _length = length, }) : _length = length,
_angle = angle, _angle = angle,
_spritePath = spritePath, super(
super(priority: 1); priority: 1,
children: [_SlinghsotSpriteComponent(spritePath, angle: angle)],
) {
renderBody = false;
}
final double _length; final double _length;
final double _angle; final double _angle;
final String _spritePath;
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
const circleRadius = 1.55; const circleRadius = 1.55;
final topCircleShape = CircleShape()..radius = circleRadius; final topCircleShape = CircleShape()..radius = circleRadius;
topCircleShape.position.setValues(0, -_length / 2); topCircleShape.position.setValues(0, -_length / 2);
final topCircleFixtureDef = FixtureDef(topCircleShape)..friction = 0; final topCircleFixtureDef = FixtureDef(topCircleShape);
fixturesDef.add(topCircleFixtureDef); fixturesDef.add(topCircleFixtureDef);
final bottomCircleShape = CircleShape()..radius = circleRadius; final bottomCircleShape = CircleShape()..radius = circleRadius;
bottomCircleShape.position.setValues(0, _length / 2); bottomCircleShape.position.setValues(0, _length / 2);
final bottomCircleFixtureDef = FixtureDef(bottomCircleShape)..friction = 0; final bottomCircleFixtureDef = FixtureDef(bottomCircleShape);
fixturesDef.add(bottomCircleFixtureDef); fixturesDef.add(bottomCircleFixtureDef);
final leftEdgeShape = EdgeShape() final leftEdgeShape = EdgeShape()
@ -69,9 +72,11 @@ class Slingshot extends BodyComponent with InitialPosition {
Vector2(circleRadius, _length / 2), Vector2(circleRadius, _length / 2),
Vector2(circleRadius, -_length / 2), Vector2(circleRadius, -_length / 2),
); );
final leftEdgeShapeFixtureDef = FixtureDef(leftEdgeShape) final leftEdgeShapeFixtureDef = FixtureDef(
..friction = 0 leftEdgeShape,
..restitution = 5; restitution: 5,
);
fixturesDef.add(leftEdgeShapeFixtureDef); fixturesDef.add(leftEdgeShapeFixtureDef);
final rightEdgeShape = EdgeShape() final rightEdgeShape = EdgeShape()
@ -79,9 +84,10 @@ class Slingshot extends BodyComponent with InitialPosition {
Vector2(-circleRadius, _length / 2), Vector2(-circleRadius, _length / 2),
Vector2(-circleRadius, -_length / 2), Vector2(-circleRadius, -_length / 2),
); );
final rightEdgeShapeFixtureDef = FixtureDef(rightEdgeShape) final rightEdgeShapeFixtureDef = FixtureDef(
..friction = 0 rightEdgeShape,
..restitution = 5; restitution: 5,
);
fixturesDef.add(rightEdgeShapeFixtureDef); fixturesDef.add(rightEdgeShapeFixtureDef);
return fixturesDef; return fixturesDef;
@ -89,34 +95,36 @@ class Slingshot extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition userData: this,
..angle = _angle; angle: _angle,
);
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
return body; return body;
} }
}
class _SlinghsotSpriteComponent extends SpriteComponent with HasGameRef {
_SlinghsotSpriteComponent(
String path, {
required double angle,
}) : _path = path,
super(
angle: -angle,
anchor: Anchor.center,
);
final String _path;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await _loadSprite(); final sprite = await gameRef.loadSprite(_path);
renderBody = false; this.sprite = sprite;
} size = sprite.originalSize / 10;
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(_spritePath);
await add(
SpriteComponent(
sprite: sprite,
size: sprite.originalSize / 10,
anchor: Anchor.center,
angle: -_angle,
),
);
} }
} }

@ -7,6 +7,7 @@ import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship} /// {@template spaceship}
/// A [Blueprint] which creates the spaceship feature. /// A [Blueprint] which creates the spaceship feature.
@ -24,19 +25,19 @@ class Spaceship extends Forge2DBlueprint {
@override @override
void build(_) { void build(_) {
addAllContactCallback([ addAllContactCallback([
SpaceshipHoleBallContactCallback(), LayerSensorBallContactCallback<_SpaceshipEntrance>(),
SpaceshipEntranceBallContactCallback(), LayerSensorBallContactCallback<_SpaceshipHole>(),
]); ]);
addAll([ addAll([
SpaceshipSaucer()..initialPosition = position, SpaceshipSaucer()..initialPosition = position,
SpaceshipEntrance()..initialPosition = position, _SpaceshipEntrance()..initialPosition = position,
AndroidHead()..initialPosition = position, AndroidHead()..initialPosition = position,
SpaceshipHole( _SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail, outsideLayer: Layer.spaceshipExitRail,
outsidePriority: Ball.spaceshipRailPriority, outsidePriority: Ball.spaceshipRailPriority,
)..initialPosition = position - Vector2(5.2, -4.8), )..initialPosition = position - Vector2(5.2, -4.8),
SpaceshipHole()..initialPosition = position - Vector2(-7.2, -0.8), _SpaceshipHole()..initialPosition = position - Vector2(-7.2, -0.8),
SpaceshipWall()..initialPosition = position, SpaceshipWall()..initialPosition = position,
]); ]);
} }
@ -71,17 +72,17 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
@override @override
Body createBody() { Body createBody() {
final circleShape = CircleShape()..radius = 3; final shape = CircleShape()..radius = 3;
final fixtureDef = FixtureDef(
final bodyDef = BodyDef() shape,
..userData = this isSensor: true,
..position = initialPosition );
..type = BodyType.static; final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef) return world.createBody(bodyDef)..createFixture(fixtureDef);
..createFixture(
FixtureDef(circleShape)..isSensor = true,
);
} }
} }
@ -91,26 +92,23 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@endtemplate} /// {@endtemplate}
class AndroidHead extends BodyComponent with InitialPosition, Layered { class AndroidHead extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_bridge} /// {@macro spaceship_bridge}
AndroidHead() : super(priority: Ball.spaceshipPriority + 1) { AndroidHead()
layer = Layer.spaceship; : super(
} priority: Ball.spaceshipPriority + 1,
children: [_AndroidHeadSpriteAnimation()],
@override ) {
Future<void> onLoad() async {
await super.onLoad();
renderBody = false; renderBody = false;
layer = Layer.spaceship;
await add(_AndroidHeadSpriteAnimation());
} }
@override @override
Body createBody() { Body createBody() {
final circleShape = CircleShape()..radius = 2; final circleShape = CircleShape()..radius = 2;
final bodyDef = BodyDef() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition userData: this,
..type = BodyType.static; );
return world.createBody(bodyDef) return world.createBody(bodyDef)
..createFixture( ..createFixture(
@ -142,17 +140,11 @@ class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent
} }
} }
/// {@template spaceship_entrance} class _SpaceshipEntrance extends LayerSensor {
/// A sensor [BodyComponent] used to detect when the ball enters the _SpaceshipEntrance()
/// the spaceship area in order to modify its filter data so the ball
/// can correctly collide only with the Spaceship
/// {@endtemplate}
class SpaceshipEntrance extends RampOpening {
/// {@macro spaceship_entrance}
SpaceshipEntrance()
: super( : super(
insideLayer: Layer.spaceship, insideLayer: Layer.spaceship,
orientation: RampOrientation.up, orientation: LayerEntranceOrientation.up,
insidePriority: Ball.spaceshipPriority, insidePriority: Ball.spaceshipPriority,
) { ) {
layer = Layer.spaceship; layer = Layer.spaceship;
@ -176,17 +168,12 @@ class SpaceshipEntrance extends RampOpening {
} }
} }
/// {@template spaceship_hole} class _SpaceshipHole extends LayerSensor {
/// A sensor [BodyComponent] responsible for sending the [Ball] _SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1})
/// out from the [Spaceship].
/// {@endtemplate}
class SpaceshipHole extends RampOpening {
/// {@macro spaceship_hole}
SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1})
: super( : super(
insideLayer: Layer.spaceship, insideLayer: Layer.spaceship,
outsideLayer: outsideLayer, outsideLayer: outsideLayer,
orientation: RampOrientation.up, orientation: LayerEntranceOrientation.down,
insidePriority: 4, insidePriority: 4,
outsidePriority: outsidePriority, outsidePriority: outsidePriority,
) { ) {
@ -246,47 +233,15 @@ class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
Body createBody() { Body createBody() {
renderBody = false; renderBody = false;
final wallShape = _SpaceshipWallShape(); final shape = _SpaceshipWallShape();
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..angle = -1.7
..type = BodyType.static;
return world.createBody(bodyDef)
..createFixture(
FixtureDef(wallShape)..restitution = 1,
);
}
}
/// [ContactCallback] that handles the contact between the [Ball] final bodyDef = BodyDef(
/// and the [SpaceshipEntrance]. position: initialPosition,
/// userData: this,
/// It modifies the [Ball] priority and filter data so it can appear on top of angle: -1.7,
/// the spaceship and also only collide with the spaceship. );
class SpaceshipEntranceBallContactCallback
extends ContactCallback<SpaceshipEntrance, Ball> {
@override
void begin(SpaceshipEntrance entrance, Ball ball, _) {
ball
..sendTo(entrance.insidePriority)
..layer = Layer.spaceship;
}
}
/// [ContactCallback] that handles the contact between the [Ball] return world.createBody(bodyDef)..createFixture(fixtureDef);
/// and a [SpaceshipHole].
///
/// It sets the [Ball] priority and filter data so it will outside of the
/// [Spaceship].
class SpaceshipHoleBallContactCallback
extends ContactCallback<SpaceshipHole, Ball> {
@override
void begin(SpaceshipHole hole, Ball ball, _) {
ball
..sendTo(hole.outsidePriority)
..layer = hole.outsideLayer;
} }
} }

@ -6,6 +6,7 @@ import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_rail} /// {@template spaceship_rail}
/// A [Blueprint] for the spaceship drop tube. /// A [Blueprint] for the spaceship drop tube.
@ -17,11 +18,11 @@ class SpaceshipRail extends Forge2DBlueprint {
@override @override
void build(_) { void build(_) {
addAllContactCallback([ addAllContactCallback([
SpaceshipRailExitBallContactCallback(), LayerSensorBallContactCallback<_SpaceshipRailExit>(),
]); ]);
final railRamp = _SpaceshipRailRamp(); final railRamp = _SpaceshipRailRamp();
final railEnd = SpaceshipRailExit(); final railEnd = _SpaceshipRailExit();
final topBase = _SpaceshipRailBase(radius: 0.55) final topBase = _SpaceshipRailBase(radius: 0.55)
..initialPosition = Vector2(-26.15, -18.65); ..initialPosition = Vector2(-26.15, -18.65);
final bottomBase = _SpaceshipRailBase(radius: 0.8) final bottomBase = _SpaceshipRailBase(radius: 0.8)
@ -122,9 +123,10 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered {
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition; userData: this,
);
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
@ -189,26 +191,20 @@ class _SpaceshipRailBase extends BodyComponent with InitialPosition, Layered {
@override @override
Body createBody() { Body createBody() {
final shape = CircleShape()..radius = radius; final shape = CircleShape()..radius = radius;
final fixtureDef = FixtureDef(shape); final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
final bodyDef = BodyDef() position: initialPosition,
..position = initialPosition userData: this,
..userData = this; );
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
} }
/// {@template spaceship_rail_exit} class _SpaceshipRailExit extends LayerSensor {
/// A sensor [BodyComponent] responsible for sending the [Ball] _SpaceshipRailExit()
/// back to the board.
/// {@endtemplate}
class SpaceshipRailExit extends RampOpening {
/// {@macro spaceship_rail_exit}
SpaceshipRailExit()
: super( : super(
orientation: RampOrientation.down, orientation: LayerEntranceOrientation.down,
insideLayer: Layer.spaceshipExitRail, insideLayer: Layer.spaceshipExitRail,
insidePriority: Ball.spaceshipRailPriority, insidePriority: Ball.spaceshipRailPriority,
) { ) {
@ -226,18 +222,3 @@ class SpaceshipRailExit extends RampOpening {
); );
} }
} }
/// [ContactCallback] that handles the contact between the [Ball]
/// and a [SpaceshipRailExit].
///
/// It resets the [Ball] priority and filter data so it will "be back" on the
/// board.
class SpaceshipRailExitBallContactCallback
extends ContactCallback<SpaceshipRailExit, Ball> {
@override
void begin(SpaceshipRailExit exitRail, Ball ball, _) {
ball
..sendTo(exitRail.outsidePriority)
..layer = exitRail.outsideLayer;
}
}

@ -6,6 +6,7 @@ import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_ramp} /// {@template spaceship_ramp}
/// A [Blueprint] which creates the ramp leading into the [Spaceship]. /// A [Blueprint] which creates the ramp leading into the [Spaceship].
@ -17,7 +18,7 @@ class SpaceshipRamp extends Forge2DBlueprint {
@override @override
void build(_) { void build(_) {
addAllContactCallback([ addAllContactCallback([
RampOpeningBallContactCallback<_SpaceshipRampOpening>(), LayerSensorBallContactCallback<_SpaceshipRampOpening>(),
]); ]);
final rightOpening = _SpaceshipRampOpening( final rightOpening = _SpaceshipRampOpening(
@ -75,7 +76,6 @@ class _SpaceshipRampBackground extends BodyComponent
Vector2(-14.2, -71.25), Vector2(-14.2, -71.25),
], ],
); );
final outerLeftCurveFixtureDef = FixtureDef(outerLeftCurveShape); final outerLeftCurveFixtureDef = FixtureDef(outerLeftCurveShape);
fixturesDef.add(outerLeftCurveFixtureDef); fixturesDef.add(outerLeftCurveFixtureDef);
@ -86,7 +86,6 @@ class _SpaceshipRampBackground extends BodyComponent
Vector2(6.1, -44.9), Vector2(6.1, -44.9),
], ],
); );
final outerRightCurveFixtureDef = FixtureDef(outerRightCurveShape); final outerRightCurveFixtureDef = FixtureDef(outerRightCurveShape);
fixturesDef.add(outerRightCurveFixtureDef); fixturesDef.add(outerRightCurveFixtureDef);
@ -103,9 +102,10 @@ class _SpaceshipRampBackground extends BodyComponent
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition; userData: this,
);
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
@ -170,8 +170,12 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
class _SpaceshipRampForegroundRailing extends BodyComponent class _SpaceshipRampForegroundRailing extends BodyComponent
with InitialPosition, Layered { with InitialPosition, Layered {
_SpaceshipRampForegroundRailing() _SpaceshipRampForegroundRailing()
: super(priority: Ball.spaceshipRampPriority + 1) { : super(
priority: Ball.spaceshipRampPriority + 1,
children: [_SpaceshipRampForegroundRailingSpriteComponent()],
) {
layer = Layer.spaceshipEntranceRamp; layer = Layer.spaceshipEntranceRamp;
renderBody = false;
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
@ -212,23 +216,16 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition; userData: this,
);
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_SpaceshipRampForegroundRailingSpriteComponent());
}
} }
class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
@ -266,20 +263,20 @@ class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered {
], ],
); );
final fixtureDef = FixtureDef(baseShape); final fixtureDef = FixtureDef(baseShape);
final bodyDef = BodyDef(
final bodyDef = BodyDef() position: initialPosition,
..userData = this userData: this,
..position = initialPosition; );
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
} }
/// {@template spaceship_ramp_opening} /// {@template spaceship_ramp_opening}
/// [RampOpening] with [Layer.spaceshipEntranceRamp] to filter [Ball] collisions /// [LayerSensor] with [Layer.spaceshipEntranceRamp] to filter [Ball] collisions
/// inside [_SpaceshipRampBackground]. /// inside [_SpaceshipRampBackground].
/// {@endtemplate} /// {@endtemplate}
class _SpaceshipRampOpening extends RampOpening { class _SpaceshipRampOpening extends LayerSensor {
/// {@macro spaceship_ramp_opening} /// {@macro spaceship_ramp_opening}
_SpaceshipRampOpening({ _SpaceshipRampOpening({
Layer? outsideLayer, Layer? outsideLayer,
@ -289,7 +286,7 @@ class _SpaceshipRampOpening extends RampOpening {
super( super(
insideLayer: Layer.spaceshipEntranceRamp, insideLayer: Layer.spaceshipEntranceRamp,
outsideLayer: outsideLayer, outsideLayer: outsideLayer,
orientation: RampOrientation.down, orientation: LayerEntranceOrientation.down,
insidePriority: Ball.spaceshipRampPriority, insidePriority: Ball.spaceshipRampPriority,
outsidePriority: outsidePriority, outsidePriority: outsidePriority,
) { ) {

@ -90,10 +90,10 @@ class SparkyBumper extends BodyComponent with InitialPosition {
majorRadius: _majorRadius, majorRadius: _majorRadius,
minorRadius: _minorRadius, minorRadius: _minorRadius,
)..rotate(math.pi / 2.1); )..rotate(math.pi / 2.1);
final fixtureDef = FixtureDef(shape) final fixtureDef = FixtureDef(
..friction = 0 shape,
..restitution = 4; restitution: 4,
);
final bodyDef = BodyDef() final bodyDef = BodyDef()
..position = initialPosition ..position = initialPosition
..userData = this; ..userData = this;

@ -3,6 +3,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_computer} /// {@template sparky_computer}
/// A [Blueprint] which creates the [_ComputerBase] and /// A [Blueprint] which creates the [_ComputerBase] and
@ -56,9 +57,10 @@ class _ComputerBase extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition; userData: this,
);
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);

@ -1,3 +0,0 @@
export 'blueprint.dart';
export 'keyboard_input_controller.dart';
export 'priority.dart';

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

@ -1,3 +1,2 @@
export 'components/components.dart'; export 'components/components.dart';
export 'extensions/extensions.dart'; export 'extensions/extensions.dart';
export 'flame/flame.dart';

@ -14,6 +14,8 @@ dependencies:
geometry: geometry:
path: ../geometry path: ../geometry
intl: ^0.17.0 intl: ^0.17.0
pinball_flame:
path: ../pinball_flame
dev_dependencies: dev_dependencies:

@ -1,5 +1,6 @@
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flame/input.dart'; import 'package:flame/input.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_flame/pinball_flame.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class LaunchRampGame extends BasicBallGame { class LaunchRampGame extends BasicBallGame {

@ -1,5 +1,6 @@
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flame/input.dart'; import 'package:flame/input.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_flame/pinball_flame.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
class BasicSpaceshipGame extends BasicGame with TapDetector { class BasicSpaceshipGame extends BasicGame with TapDetector {

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flame/input.dart'; import 'package:flame/input.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_flame/pinball_flame.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SpaceshipRailGame extends BasicBallGame { class SpaceshipRailGame extends BasicBallGame {

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flame/input.dart'; import 'package:flame/input.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_flame/pinball_flame.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SpaceshipRampGame extends BasicBallGame { class SpaceshipRampGame extends BasicBallGame {

@ -240,6 +240,13 @@ packages:
relative: true relative: true
source: path source: path
version: "1.0.0+1" version: "1.0.0+1"
pinball_flame:
dependency: "direct main"
description:
path: "../../pinball_flame"
relative: true
source: path
version: "1.0.0+1"
platform: platform:
dependency: transitive dependency: transitive
description: description:

@ -14,6 +14,8 @@ dependencies:
sdk: flutter sdk: flutter
pinball_components: pinball_components:
path: ../ path: ../
pinball_flame:
path: ../../pinball_flame
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

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

@ -4,6 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_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';
@ -17,6 +18,7 @@ void main() {
await game.addFromBlueprint(Boundaries()); await game.addFromBlueprint(Boundaries());
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
game.camera.zoom = 3.9; game.camera.zoom = 3.9;
await game.ready();
}, },
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(

@ -4,6 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_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';
@ -18,6 +19,7 @@ void main() {
await game.addFromBlueprint(DinoWalls()); await game.addFromBlueprint(DinoWalls());
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
game.camera.zoom = 6.5; game.camera.zoom = 6.5;
await game.ready();
}, },
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(

@ -4,6 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_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';

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

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

@ -4,6 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_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,6 +20,7 @@ void main() {
setUp: (game, tester) async { setUp: (game, tester) async {
await game.addFromBlueprint(Slingshots()); await game.addFromBlueprint(Slingshots());
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
await game.ready();
}, },
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(

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

@ -4,6 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_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';
@ -16,6 +17,7 @@ void main() {
setUp: (game, tester) async { setUp: (game, tester) async {
await game.addFromBlueprint(SpaceshipRamp()); await game.addFromBlueprint(SpaceshipRamp());
game.camera.followVector2(Vector2(-13, -50)); game.camera.followVector2(Vector2(-13, -50));
await game.ready();
}, },
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(

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

@ -4,6 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_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';

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

@ -0,0 +1,39 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# VSCode related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json

@ -0,0 +1,11 @@
# pinball_flame
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
Set of out-of-the-way solutions for common Pinball game problems.
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis

@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml

@ -0,0 +1,5 @@
library pinball_flame;
export 'src/blueprint.dart';
export 'src/component_controller.dart';
export 'src/keyboard_input_controller.dart';

@ -1,12 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
/// {@template component_controller} /// {@template component_controller}
/// A [ComponentController] is a [Component] in charge of handling the logic /// A [ComponentController] is a [Component] in charge of handling the logic
/// associated with another [Component]. /// associated with another [Component].
///
/// [ComponentController]s usually implement [BlocComponent].
/// {@endtemplate} /// {@endtemplate}
abstract class ComponentController<T extends Component> extends Component { abstract class ComponentController<T extends Component> extends Component {
/// {@macro component_controller} /// {@macro component_controller}

@ -0,0 +1,20 @@
name: pinball_flame
description: Set of out-of-the-way solutions for common Pinball game problems.
version: 1.0.0+1
publish_to: none
environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
flame: ^1.1.1
flame_forge2d: ^0.11.0
flutter:
sdk: flutter
dev_dependencies:
flame_test: ^1.3.0
flutter_test:
sdk: flutter
mocktail: ^0.3.0
very_good_analysis: ^2.4.0

@ -0,0 +1,10 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart';
class MockForge2DGame extends Mock implements Forge2DGame {}
class MockContactCallback extends Mock
implements ContactCallback<dynamic, dynamic> {}
class MockComponent extends Mock implements Component {}

@ -1,9 +1,12 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/contact_callbacks.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';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart'; import '../helpers/helpers.dart';
class TestContactCallback extends ContactCallback<dynamic, dynamic> {}
class MyBlueprint extends Blueprint { class MyBlueprint extends Blueprint {
@override @override
@ -51,19 +54,19 @@ void main() {
}); });
test('components can be added to it', () { test('components can be added to it', () {
final blueprint = MyBlueprint()..build(MockGame()); final blueprint = MyBlueprint()..build(MockForge2DGame());
expect(blueprint.components.length, equals(3)); expect(blueprint.components.length, equals(3));
}); });
test('blueprints can be added to it', () { test('blueprints can be added to it', () {
final blueprint = MyComposedBlueprint()..build(MockGame()); final blueprint = MyComposedBlueprint()..build(MockForge2DGame());
expect(blueprint.blueprints.length, equals(3)); expect(blueprint.blueprints.length, equals(3));
}); });
test('adds the components to a game on attach', () { test('adds the components to a game on attach', () {
final mockGame = MockGame(); final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
MyBlueprint().attach(mockGame); MyBlueprint().attach(mockGame);
@ -71,7 +74,7 @@ void main() {
}); });
test('adds components from a child Blueprint the to a game on attach', () { test('adds components from a child Blueprint the to a game on attach', () {
final mockGame = MockGame(); final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
MyComposedBlueprint().attach(mockGame); MyComposedBlueprint().attach(mockGame);
@ -81,7 +84,7 @@ void main() {
test( test(
'throws assertion error when adding to an already attached blueprint', 'throws assertion error when adding to an already attached blueprint',
() async { () async {
final mockGame = MockGame(); final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
final blueprint = MyBlueprint(); final blueprint = MyBlueprint();
await blueprint.attach(mockGame); await blueprint.attach(mockGame);
@ -94,17 +97,17 @@ void main() {
group('Forge2DBlueprint', () { group('Forge2DBlueprint', () {
setUpAll(() { setUpAll(() {
registerFallbackValue(SpaceshipHoleBallContactCallback()); registerFallbackValue(TestContactCallback());
}); });
test('callbacks can be added to it', () { test('callbacks can be added to it', () {
final blueprint = MyForge2dBlueprint()..build(MockGame()); final blueprint = MyForge2dBlueprint()..build(MockForge2DGame());
expect(blueprint.callbacks.length, equals(3)); expect(blueprint.callbacks.length, equals(3));
}); });
test('adds the callbacks to a game on attach', () async { test('adds the callbacks to a game on attach', () async {
final mockGame = MockGame(); final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {}); when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
await MyForge2dBlueprint().attach(mockGame); await MyForge2dBlueprint().attach(mockGame);
@ -115,7 +118,7 @@ void main() {
test( test(
'throws assertion error when adding to an already attached blueprint', 'throws assertion error when adding to an already attached blueprint',
() async { () async {
final mockGame = MockGame(); final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {}); when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {}); when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
final blueprint = MyForge2dBlueprint(); final blueprint = MyForge2dBlueprint();

@ -4,7 +4,7 @@ import 'package:flame/game.dart';
import 'package:flame/src/components/component.dart'; import 'package:flame/src/components/component.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/flame/flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
class TestComponentController extends ComponentController { class TestComponentController extends ComponentController {
TestComponentController(Component component) : super(component); TestComponentController(Component component) : super(component);

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.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';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart';
abstract class _KeyCallStub { abstract class _KeyCallStub {
bool onCall(); bool onCall();

@ -483,6 +483,13 @@ packages:
relative: true relative: true
source: path source: path
version: "1.0.0+1" version: "1.0.0+1"
pinball_flame:
dependency: "direct main"
description:
path: "packages/pinball_flame"
relative: true
source: path
version: "1.0.0+1"
pinball_theme: pinball_theme:
dependency: "direct main" dependency: "direct main"
description: description:

@ -27,6 +27,8 @@ dependencies:
path: packages/pinball_audio path: packages/pinball_audio
pinball_components: pinball_components:
path: packages/pinball_components path: packages/pinball_components
pinball_flame:
path: packages/pinball_flame
pinball_theme: pinball_theme:
path: packages/pinball_theme path: packages/pinball_theme

@ -21,8 +21,6 @@ void main() {
const GameState( const GameState(
score: 0, score: 0,
balls: 2, balls: 2,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
], ],
@ -41,15 +39,11 @@ void main() {
const GameState( const GameState(
score: 2, score: 2,
balls: 3, balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 5, score: 5,
balls: 3, balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
], ],
@ -69,158 +63,46 @@ void main() {
const GameState( const GameState(
score: 0, score: 0,
balls: 2, balls: 2,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 0, score: 0,
balls: 1, balls: 1,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
], ],
); );
}); });
group('BonusLetterActivated', () { group(
blocTest<GameBloc, GameState>( 'BonusActivated',
'adds the letter to the state', () {
build: GameBloc.new, blocTest<GameBloc, GameState>(
act: (bloc) => bloc 'adds bonus to history',
..add(const BonusLetterActivated(0)) build: GameBloc.new,
..add(const BonusLetterActivated(1)) act: (bloc) => bloc
..add(const BonusLetterActivated(2)), ..add(const BonusActivated(GameBonus.googleWord))
expect: () => const [ ..add(const BonusActivated(GameBonus.dashNest)),
GameState( expect: () => const [
score: 0, GameState(
balls: 3, score: 0,
activatedBonusLetters: [0], balls: 3,
activatedDashNests: {}, bonusHistory: [GameBonus.googleWord],
bonusHistory: [], ),
), GameState(
GameState( score: 0,
score: 0, balls: 3,
balls: 3, bonusHistory: [GameBonus.googleWord, GameBonus.dashNest],
activatedBonusLetters: [0, 1], ),
activatedDashNests: {}, ],
bonusHistory: [], );
), },
GameState( );
score: 0,
balls: 3,
activatedBonusLetters: [0, 1, 2],
activatedDashNests: {},
bonusHistory: [],
),
],
);
blocTest<GameBloc, GameState>(
'adds the bonus when the bonusWord is completed',
build: GameBloc.new,
act: (bloc) => bloc
..add(const BonusLetterActivated(0))
..add(const BonusLetterActivated(1))
..add(const BonusLetterActivated(2))
..add(const BonusLetterActivated(3))
..add(const BonusLetterActivated(4))
..add(const BonusLetterActivated(5)),
expect: () => const [
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [0, 1],
activatedDashNests: {},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [0, 1, 2],
activatedDashNests: {},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [0, 1, 2, 3],
activatedDashNests: {},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [0, 1, 2, 3, 4],
activatedDashNests: {},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.word],
),
GameState(
score: GameBloc.bonusWordScore,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.word],
),
],
);
});
group('DashNestActivated', () {
blocTest<GameBloc, GameState>(
'adds the bonus when all nests are activated',
build: GameBloc.new,
act: (bloc) => bloc
..add(const DashNestActivated('0'))
..add(const DashNestActivated('1'))
..add(const DashNestActivated('2')),
expect: () => const [
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {'0'},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {'0', '1'},
bonusHistory: [],
),
GameState(
score: 0,
balls: 4,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
),
],
);
});
group('SparkyTurboChargeActivated', () { group('SparkyTurboChargeActivated', () {
blocTest<GameBloc, GameState>( blocTest<GameBloc, GameState>(
@ -231,8 +113,6 @@ void main() {
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.sparkyTurboCharge], bonusHistory: [GameBonus.sparkyTurboCharge],
), ),
], ],

@ -41,61 +41,34 @@ void main() {
}); });
}); });
group('BonusLetterActivated', () { group('BonusActivated', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(const BonusLetterActivated(0), isNotNull); expect(const BonusActivated(GameBonus.dashNest), isNotNull);
}); });
test('supports value equality', () { test('supports value equality', () {
expect( expect(
BonusLetterActivated(0), BonusActivated(GameBonus.googleWord),
equals(BonusLetterActivated(0)), equals(const BonusActivated(GameBonus.googleWord)),
); );
expect( expect(
BonusLetterActivated(0), const BonusActivated(GameBonus.googleWord),
isNot(equals(BonusLetterActivated(1))), isNot(equals(const BonusActivated(GameBonus.dashNest))),
); );
}); });
test(
'throws assertion error if index is bigger than the word length',
() {
expect(
() => BonusLetterActivated(8),
throwsAssertionError,
);
},
);
}); });
});
group('DashNestActivated', () { group('SparkyTurboChargeActivated', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(const DashNestActivated('0'), isNotNull); expect(const SparkyTurboChargeActivated(), isNotNull);
});
test('supports value equality', () {
expect(
DashNestActivated('0'),
equals(DashNestActivated('0')),
);
expect(
DashNestActivated('0'),
isNot(equals(DashNestActivated('1'))),
);
});
}); });
group('SparkyTurboChargeActivated', () { test('supports value equality', () {
test('can be instantiated', () { expect(
expect(const SparkyTurboChargeActivated(), isNotNull); SparkyTurboChargeActivated(),
}); equals(SparkyTurboChargeActivated()),
);
test('supports value equality', () {
expect(
SparkyTurboChargeActivated(),
equals(SparkyTurboChargeActivated()),
);
});
}); });
}); });
} }

@ -10,16 +10,12 @@ void main() {
GameState( GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: const [],
activatedDashNests: const {},
bonusHistory: const [], bonusHistory: const [],
), ),
equals( equals(
const GameState( const GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
), ),
@ -32,8 +28,6 @@ void main() {
const GameState( const GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
isNotNull, isNotNull,
@ -49,8 +43,6 @@ void main() {
() => GameState( () => GameState(
balls: -1, balls: -1,
score: 0, score: 0,
activatedBonusLetters: const [],
activatedDashNests: const {},
bonusHistory: const [], bonusHistory: const [],
), ),
throwsAssertionError, throwsAssertionError,
@ -66,8 +58,6 @@ void main() {
() => GameState( () => GameState(
balls: 0, balls: 0,
score: -1, score: -1,
activatedBonusLetters: const [],
activatedDashNests: const {},
bonusHistory: const [], bonusHistory: const [],
), ),
throwsAssertionError, throwsAssertionError,
@ -82,8 +72,6 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 0, score: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isGameOver, isTrue); expect(gameState.isGameOver, isTrue);
@ -95,44 +83,12 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 1, balls: 1,
score: 0, score: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isGameOver, isFalse); expect(gameState.isGameOver, isFalse);
}); });
}); });
group('isLetterActivated', () {
test(
'is true when the letter is activated',
() {
const gameState = GameState(
balls: 3,
score: 0,
activatedBonusLetters: [1],
activatedDashNests: {},
bonusHistory: [],
);
expect(gameState.isLetterActivated(1), isTrue);
},
);
test(
'is false when the letter is not activated',
() {
const gameState = GameState(
balls: 3,
score: 0,
activatedBonusLetters: [1],
activatedDashNests: {},
bonusHistory: [],
);
expect(gameState.isLetterActivated(0), isFalse);
},
);
});
group('copyWith', () { group('copyWith', () {
test( test(
'throws AssertionError ' 'throws AssertionError '
@ -141,8 +97,6 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 2, score: 2,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect( expect(
@ -159,8 +113,6 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 2, score: 2,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect( expect(
@ -177,16 +129,12 @@ void main() {
const gameState = GameState( const gameState = GameState(
score: 2, score: 2,
balls: 0, balls: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
final otherGameState = GameState( final otherGameState = GameState(
score: gameState.score + 1, score: gameState.score + 1,
balls: gameState.balls + 1, balls: gameState.balls + 1,
activatedBonusLetters: const [0], bonusHistory: const [GameBonus.googleWord],
activatedDashNests: const {'1'},
bonusHistory: const [GameBonus.word],
); );
expect(gameState, isNot(equals(otherGameState))); expect(gameState, isNot(equals(otherGameState)));
@ -194,8 +142,6 @@ void main() {
gameState.copyWith( gameState.copyWith(
score: otherGameState.score, score: otherGameState.score,
balls: otherGameState.balls, balls: otherGameState.balls,
activatedBonusLetters: otherGameState.activatedBonusLetters,
activatedDashNests: otherGameState.activatedDashNests,
bonusHistory: otherGameState.bonusHistory, bonusHistory: otherGameState.bonusHistory,
), ),
equals(otherGameState), equals(otherGameState),

@ -13,7 +13,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballTestGame.new);
group('AlienZone', () { group('AlienZone', () {
flameTester.test( flameTester.test(
@ -52,7 +52,7 @@ void main() {
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new, gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
); );

@ -9,7 +9,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballTestGame.new);
group('Board', () { group('Board', () {
flameTester.test( flameTester.test(

@ -1,376 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/effects.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_bloc/flutter_bloc.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 '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('BonusWord', () {
flameTester.test(
'loads the letters correctly',
(game) async {
final bonusWord = BonusWord(
position: Vector2.zero(),
);
await game.ensureAdd(bonusWord);
final letters = bonusWord.descendants().whereType<BonusLetter>();
expect(letters.length, equals(GameBloc.bonusWord.length));
},
);
group('listenWhen', () {
final previousState = MockGameState();
final currentState = MockGameState();
test(
'returns true when there is a new word bonus awarded',
() {
when(() => previousState.bonusHistory).thenReturn([]);
when(() => currentState.bonusHistory).thenReturn([GameBonus.word]);
expect(
BonusWord(position: Vector2.zero()).listenWhen(
previousState,
currentState,
),
isTrue,
);
},
);
test(
'returns false when there is no new word bonus awarded',
() {
when(() => previousState.bonusHistory).thenReturn([GameBonus.word]);
when(() => currentState.bonusHistory).thenReturn([GameBonus.word]);
expect(
BonusWord(position: Vector2.zero()).listenWhen(
previousState,
currentState,
),
isFalse,
);
},
);
});
group('onNewState', () {
final state = MockGameState();
flameTester.test(
'adds sequence effect to the letters when the player receives a bonus',
(game) async {
when(() => state.bonusHistory).thenReturn([GameBonus.word]);
final bonusWord = BonusWord(position: Vector2.zero());
await game.ensureAdd(bonusWord);
await game.ready();
bonusWord.onNewState(state);
game.update(0); // Run one frame so the effects are added
final letters = bonusWord.children.whereType<BonusLetter>();
expect(letters.length, equals(GameBloc.bonusWord.length));
for (final letter in letters) {
expect(
letter.children.whereType<SequenceEffect>().length,
equals(1),
);
}
},
);
flameTester.test(
'plays the google bonus sound',
(game) async {
when(() => state.bonusHistory).thenReturn([GameBonus.word]);
final bonusWord = BonusWord(position: Vector2.zero());
await game.ensureAdd(bonusWord);
await game.ready();
bonusWord.onNewState(state);
verify(bonusWord.gameRef.audio.googleBonus).called(1);
},
);
flameTester.test(
'adds a color effect to reset the color when the sequence is finished',
(game) async {
when(() => state.bonusHistory).thenReturn([GameBonus.word]);
final bonusWord = BonusWord(position: Vector2.zero());
await game.ensureAdd(bonusWord);
await game.ready();
bonusWord.onNewState(state);
// Run the amount of time necessary for the animation to finish
game.update(3);
game.update(0); // Run one additional frame so the effects are added
final letters = bonusWord.children.whereType<BonusLetter>();
expect(letters.length, equals(GameBloc.bonusWord.length));
for (final letter in letters) {
expect(
letter.children.whereType<ColorEffect>().length,
equals(1),
);
}
},
);
});
});
group('BonusLetter', () {
final flameTester = FlameTester(EmptyPinballGameTest.new);
flameTester.test(
'loads correctly',
(game) async {
final bonusLetter = BonusLetter(
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
await game.ready();
expect(game.contains(bonusLetter), isTrue);
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final bonusLetter = BonusLetter(
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
expect(bonusLetter.body.bodyType, equals(BodyType.static));
},
);
});
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final bonusLetter = BonusLetter(
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
expect(bonusLetter.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'is sensor',
(game) async {
final bonusLetter = BonusLetter(
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
final fixture = bonusLetter.body.fixtures[0];
expect(fixture.isSensor, isTrue);
},
);
flameTester.test(
'shape is circular',
(game) async {
final bonusLetter = BonusLetter(
letter: 'G',
index: 0,
);
await game.ensureAdd(bonusLetter);
final fixture = bonusLetter.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(1.85));
},
);
});
group('bonus letter activation', () {
late GameBloc gameBloc;
late PinballAudio pinballAudio;
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
repositories: () => [
RepositoryProvider<PinballAudio>.value(value: pinballAudio),
],
);
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
pinballAudio = MockPinballAudio();
when(pinballAudio.googleBonus).thenAnswer((_) {});
});
flameBlocTester.testGameWidget(
'adds BonusLetterActivated to GameBloc when not activated',
setUp: (game, tester) async {
final bonusWord = BonusWord(
position: Vector2.zero(),
);
await game.ensureAdd(bonusWord);
final bonusLetters =
game.descendants().whereType<BonusLetter>().toList();
for (var index = 0; index < bonusLetters.length; index++) {
final bonusLetter = bonusLetters[index];
bonusLetter.activate();
await game.ready();
verify(() => gameBloc.add(BonusLetterActivated(index))).called(1);
}
},
);
flameBlocTester.testGameWidget(
"doesn't add BonusLetterActivated to GameBloc when already activated",
setUp: (game, tester) async {
const state = GameState(
score: 0,
balls: 2,
activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [],
);
whenListen(
gameBloc,
Stream.value(state),
initialState: state,
);
final bonusLetter = BonusLetter(letter: '', index: 0);
await game.add(bonusLetter);
await game.ready();
bonusLetter.activate();
await game.ready();
},
verify: (game, tester) async {
verifyNever(() => gameBloc.add(const BonusLetterActivated(0)));
},
);
flameBlocTester.testGameWidget(
'adds a ColorEffect',
setUp: (game, tester) async {
const state = GameState(
score: 0,
balls: 2,
activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [],
);
final bonusLetter = BonusLetter(letter: '', index: 0);
await game.add(bonusLetter);
await game.ready();
bonusLetter.activate();
bonusLetter.onNewState(state);
await tester.pump();
},
verify: (game, tester) async {
// TODO(aleastiago): Look into making `testGameWidget` pass the
// subject.
final bonusLetter = game.descendants().whereType<BonusLetter>().last;
expect(
bonusLetter.children.whereType<ColorEffect>().length,
equals(1),
);
},
);
flameBlocTester.testGameWidget(
'listens when there is a change on the letter status',
setUp: (game, tester) async {
final bonusWord = BonusWord(
position: Vector2.zero(),
);
await game.ensureAdd(bonusWord);
final bonusLetters =
game.descendants().whereType<BonusLetter>().toList();
for (var index = 0; index < bonusLetters.length; index++) {
final bonusLetter = bonusLetters[index];
bonusLetter.activate();
await game.ready();
final state = GameState(
score: 0,
balls: 2,
activatedBonusLetters: [index],
activatedDashNests: const {},
bonusHistory: const [],
);
expect(
bonusLetter.listenWhen(const GameState.initial(), state),
isTrue,
);
}
},
);
});
group('BonusLetterBallContactCallback', () {
test('calls ball.activate', () {
final ball = MockBall();
final bonusLetter = MockBonusLetter();
final contactCallback = BonusLetterBallContactCallback();
when(() => bonusLetter.isEnabled).thenReturn(true);
contactCallback.begin(ball, bonusLetter, MockContact());
verify(bonusLetter.activate).called(1);
});
test("doesn't call ball.activate when letter is disabled", () {
final ball = MockBall();
final bonusLetter = MockBonusLetter();
final contactCallback = BonusLetterBallContactCallback();
when(() => bonusLetter.isEnabled).thenReturn(false);
contactCallback.begin(ball, bonusLetter, MockContact());
verifyNever(bonusLetter.activate);
});
});
});
}

@ -41,7 +41,7 @@ void main() {
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new, gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
); );

@ -11,18 +11,16 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballTestGame.new);
final gameOverBlocTester = FlameBlocTester<EmptyPinballGameTest, GameBloc>( final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new, gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () { blocBuilder: () {
final bloc = MockGameBloc(); final bloc = MockGameBloc();
const state = GameState( const state = GameState(
score: 0, score: 0,
balls: 0, balls: 0,
bonusHistory: [], bonusHistory: [],
activatedBonusLetters: [],
activatedDashNests: {},
); );
whenListen(bloc, Stream.value(state), initialState: state); whenListen(bloc, Stream.value(state), initialState: state);
return bloc; return bloc;
@ -66,7 +64,7 @@ void main() {
}); });
testRawKeyDownEvents(leftKeys, (event) { testRawKeyDownEvents(leftKeys, (event) {
gameOverBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'does nothing when is game over', 'does nothing when is game over',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
@ -151,7 +149,7 @@ void main() {
}); });
testRawKeyDownEvents(rightKeys, (event) { testRawKeyDownEvents(rightKeys, (event) {
gameOverBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'does nothing when is game over', 'does nothing when is game over',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ensureAdd(flipper); await game.ensureAdd(flipper);

@ -12,18 +12,16 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballTestGame.new);
final gameOverBlocTester = FlameBlocTester<EmptyPinballGameTest, GameBloc>( final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new, gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () { blocBuilder: () {
final bloc = MockGameBloc(); final bloc = MockGameBloc();
const state = GameState( const state = GameState(
score: 0, score: 0,
balls: 0, balls: 0,
bonusHistory: [], bonusHistory: [],
activatedBonusLetters: [],
activatedDashNests: {},
); );
whenListen(bloc, Stream.value(state), initialState: state); whenListen(bloc, Stream.value(state), initialState: state);
return bloc; return bloc;
@ -92,7 +90,7 @@ void main() {
}); });
testRawKeyDownEvents(downKeys, (event) { testRawKeyDownEvents(downKeys, (event) {
gameOverBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'does nothing when is game over', 'does nothing when is game over',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ensureAdd(plunger); await game.ensureAdd(plunger);

@ -10,7 +10,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
group('SparkyComputerController', () { group('SparkyComputerController', () {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballTestGame.new);
late ControlledSparkyComputer controlledSparkyComputer; late ControlledSparkyComputer controlledSparkyComputer;

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

@ -15,9 +15,7 @@ void main() {
final state = GameState( final state = GameState(
score: 10, score: 10,
balls: 0, balls: 0,
activatedBonusLetters: const [],
bonusHistory: const [], bonusHistory: const [],
activatedDashNests: const {},
); );
final previous = GameState.initial(); final previous = GameState.initial();
@ -66,9 +64,7 @@ void main() {
GameState( GameState(
score: 10, score: 10,
balls: 0, balls: 0,
activatedBonusLetters: const [],
bonusHistory: const [], bonusHistory: const [],
activatedDashNests: const {},
), ),
); );

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

@ -30,9 +30,7 @@ void main() {
const current = GameState( const current = GameState(
score: 10, score: 10,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {},
); );
expect(controller.listenWhen(previous, current), isTrue); expect(controller.listenWhen(previous, current), isTrue);
}); });
@ -44,9 +42,7 @@ void main() {
const current = GameState( const current = GameState(
score: 10, score: 10,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {},
); );
expect(controller.listenWhen(null, current), isTrue); expect(controller.listenWhen(null, current), isTrue);
}, },
@ -70,9 +66,7 @@ void main() {
const state = GameState( const state = GameState(
score: 10, score: 10,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {},
); );
controller.onNewState(state); controller.onNewState(state);
@ -89,9 +83,7 @@ void main() {
const GameState( const GameState(
score: 10, score: 10,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {},
), ),
); );
@ -99,9 +91,7 @@ void main() {
const GameState( const GameState(
score: 14, score: 14,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {},
), ),
); );

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

Loading…
Cancel
Save