Merge branch 'main' into feat/priority-management

pull/198/head
Allison Ryan 3 years ago
commit 8da6f14178

@ -11,14 +11,11 @@ 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<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,29 +26,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) { void _onDashNestActivated(DashNestActivated event, Emitter emit) {

@ -33,17 +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 { class DashNestActivated 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,7 +22,6 @@ 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, required this.activatedDashNests,
}) : assert(score >= 0, "Score can't be negative"), }) : assert(score >= 0, "Score can't be negative"),
@ -32,7 +30,6 @@ class GameState extends Equatable {
const GameState.initial() const GameState.initial()
: score = 0, : score = 0,
balls = 3, balls = 3,
activatedBonusLetters = const [],
activatedDashNests = const {}, activatedDashNests = const {},
bonusHistory = const []; bonusHistory = const [];
@ -44,9 +41,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. /// Active dash nests.
final Set<String> activatedDashNests; final Set<String> activatedDashNests;
@ -57,14 +51,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, Set<String>? activatedDashNests,
List<GameBonus>? bonusHistory, List<GameBonus>? bonusHistory,
}) { }) {
@ -76,8 +65,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, activatedDashNests: activatedDashNests ?? this.activatedDashNests,
bonusHistory: bonusHistory ?? this.bonusHistory, bonusHistory: bonusHistory ?? this.bonusHistory,
); );
@ -87,7 +74,6 @@ class GameState extends Equatable {
List<Object?> get props => [ List<Object?> get props => [
score, score,
balls, balls,
activatedBonusLetters,
activatedDashNests, activatedDashNests,
bonusHistory, bonusHistory,
]; ];

@ -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,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,6 +1,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.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/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template controlled_flipper} /// {@template controlled_flipper}
@ -19,7 +21,7 @@ class ControlledFlipper extends Flipper with Controls<FlipperController> {
/// A [ComponentController] that controls a [Flipper]s movement. /// A [ComponentController] that controls a [Flipper]s movement.
/// {@endtemplate} /// {@endtemplate}
class FlipperController extends ComponentController<Flipper> class FlipperController extends ComponentController<Flipper>
with KeyboardHandler { with KeyboardHandler, BlocComponent<GameBloc, GameState> {
/// {@macro flipper_controller} /// {@macro flipper_controller}
FlipperController(Flipper flipper) FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys, : _keys = flipper.side.flipperKeys,
@ -35,6 +37,7 @@ class FlipperController extends ComponentController<Flipper>
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (state?.isGameOver ?? false) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -1,6 +1,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.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/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template controlled_plunger} /// {@template controlled_plunger}
@ -18,7 +20,7 @@ class ControlledPlunger extends Plunger with Controls<PlungerController> {
/// A [ComponentController] that controls a [Plunger]s movement. /// A [ComponentController] that controls a [Plunger]s movement.
/// {@endtemplate} /// {@endtemplate}
class PlungerController extends ComponentController<Plunger> class PlungerController extends ComponentController<Plunger>
with KeyboardHandler { with KeyboardHandler, BlocComponent<GameBloc, GameState> {
/// {@macro plunger_controller} /// {@macro plunger_controller}
PlungerController(Plunger plunger) : super(plunger); PlungerController(Plunger plunger) : super(plunger);
@ -36,6 +38,7 @@ class PlungerController extends ComponentController<Plunger>
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (state?.isGameOver ?? false) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -28,7 +28,11 @@ class GameFlowController extends ComponentController<PinballGame>
/// Puts the game on a game over state /// Puts the game on a game over state
void gameOver() { void gameOver() {
component.firstChild<Backboard>()?.gameOverMode(); // TODO(erickzanardo): implement score submission and "navigate" to the
// next page
component.firstChild<Backboard>()?.gameOverMode(
score: state?.score ?? 0,
);
component.firstChild<CameraController>()?.focusOnBackboard(); component.firstChild<CameraController>()?.focusOnBackboard();
} }

@ -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/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.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);
}
}
}

@ -37,7 +37,7 @@ class ScoreEffectController extends ComponentController<PinballGame>
text: newScore.toString(), text: newScore.toString(),
position: Vector2( position: Vector2(
_noise(), _noise(),
_noise() + (-BoardDimensions.bounds.topCenter.dy + 10), _noise() + (BoardDimensions.bounds.topCenter.dy + 10),
), ),
), ),
); );

@ -58,6 +58,13 @@ 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(Assets.images.components.background.path), images.load(Assets.images.components.background.path),
]; ];
} }

@ -41,7 +41,7 @@ class PinballGame extends Forge2DGame
// unawaited(add(ScoreEffectController(this))); // unawaited(add(ScoreEffectController(this)));
unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this))); unawaited(add(CameraController(this)));
unawaited(add(Backboard(position: Vector2(0, -88)))); unawaited(add(Backboard.waiting(position: Vector2(0, -88))));
await _addGameBoundaries(); await _addGameBoundaries();
unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(Boundaries()));
@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@ -57,6 +57,8 @@ class $AssetsImagesBackboardGen {
/// File path: assets/images/backboard/backboard_scores.png /// File path: assets/images/backboard/backboard_scores.png
AssetGenImage get backboardScores => AssetGenImage get backboardScores =>
const AssetGenImage('assets/images/backboard/backboard_scores.png'); const AssetGenImage('assets/images/backboard/backboard_scores.png');
AssetGenImage get display =>
const AssetGenImage('assets/images/backboard/display.png');
} }
class $AssetsImagesBaseboardGen { class $AssetsImagesBaseboardGen {
@ -307,11 +309,8 @@ class $AssetsImagesSparkyBumperGen {
class $AssetsImagesSparkyComputerGen { class $AssetsImagesSparkyComputerGen {
const $AssetsImagesSparkyComputerGen(); const $AssetsImagesSparkyComputerGen();
/// File path: assets/images/sparky/computer/base.png
AssetGenImage get base => AssetGenImage get base =>
const AssetGenImage('assets/images/sparky/computer/base.png'); const AssetGenImage('assets/images/sparky/computer/base.png');
/// File path: assets/images/sparky/computer/top.png
AssetGenImage get top => AssetGenImage get top =>
const AssetGenImage('assets/images/sparky/computer/top.png'); const AssetGenImage('assets/images/sparky/computer/top.png');
} }
@ -355,11 +354,8 @@ class $AssetsImagesDashBumperMainGen {
class $AssetsImagesSparkyBumperAGen { class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen(); const $AssetsImagesSparkyBumperAGen();
/// File path: assets/images/sparky/bumper/a/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/a/active.png'); const AssetGenImage('assets/images/sparky/bumper/a/active.png');
/// File path: assets/images/sparky/bumper/a/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/a/inactive.png'); const AssetGenImage('assets/images/sparky/bumper/a/inactive.png');
} }
@ -367,11 +363,8 @@ class $AssetsImagesSparkyBumperAGen {
class $AssetsImagesSparkyBumperBGen { class $AssetsImagesSparkyBumperBGen {
const $AssetsImagesSparkyBumperBGen(); const $AssetsImagesSparkyBumperBGen();
/// File path: assets/images/sparky/bumper/b/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/b/active.png'); const AssetGenImage('assets/images/sparky/bumper/b/active.png');
/// File path: assets/images/sparky/bumper/b/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/b/inactive.png'); const AssetGenImage('assets/images/sparky/bumper/b/inactive.png');
} }
@ -379,11 +372,8 @@ class $AssetsImagesSparkyBumperBGen {
class $AssetsImagesSparkyBumperCGen { class $AssetsImagesSparkyBumperCGen {
const $AssetsImagesSparkyBumperCGen(); const $AssetsImagesSparkyBumperCGen();
/// File path: assets/images/sparky/bumper/c/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/c/active.png'); const AssetGenImage('assets/images/sparky/bumper/c/active.png');
/// File path: assets/images/sparky/bumper/c/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/c/inactive.png'); const AssetGenImage('assets/images/sparky/bumper/c/inactive.png');
} }

@ -73,13 +73,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);
} }

@ -1,40 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template backboard}
/// The [Backboard] of the pinball machine.
/// {@endtemplate}
class Backboard extends SpriteComponent with HasGameRef {
/// {@macro backboard}
Backboard({
required Vector2 position,
}) : super(
// TODO(erickzanardo): remove multiply after
// https://github.com/flame-engine/flame/pull/1506 is merged
position: position..clone().multiply(Vector2(1, -1)),
anchor: Anchor.bottomCenter,
);
@override
Future<void> onLoad() async {
await waitingMode();
}
/// Puts the Backboard in waiting mode, where the scoreboard is shown.
Future<void> waitingMode() async {
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardScores.keyName,
);
size = sprite.originalSize / 10;
this.sprite = sprite;
}
/// Puts the Backboard in game over mode, where the score input is shown.
Future<void> gameOverMode() async {
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardGameOver.keyName,
);
size = sprite.originalSize / 10;
this.sprite = sprite;
}
}

@ -0,0 +1,75 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
export 'backboard_game_over.dart';
export 'backboard_letter_prompt.dart';
export 'backboard_waiting.dart';
/// {@template backboard}
/// The [Backboard] of the pinball machine.
/// {@endtemplate}
class Backboard extends PositionComponent with HasGameRef {
/// {@macro backboard}
Backboard({
required Vector2 position,
}) : super(
position: position,
anchor: Anchor.bottomCenter,
);
/// {@macro backboard}
///
/// Returns a [Backboard] initialized in the waiting mode
factory Backboard.waiting({
required Vector2 position,
}) {
return Backboard(position: position)..waitingMode();
}
/// {@macro backboard}
///
/// Returns a [Backboard] initialized in the game over mode
factory Backboard.gameOver({
required Vector2 position,
required int score,
required BackboardOnSubmit onSubmit,
}) {
return Backboard(position: position)
..gameOverMode(
score: score,
onSubmit: onSubmit,
);
}
/// [TextPaint] used on the [Backboard]
static final textPaint = TextPaint(
style: TextStyle(
fontSize: 6,
color: Colors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// Puts the Backboard in waiting mode, where the scoreboard is shown.
Future<void> waitingMode() async {
children.removeWhere((_) => true);
await add(BackboardWaiting());
}
/// Puts the Backboard in game over mode, where the score input is shown.
Future<void> gameOverMode({
required int score,
BackboardOnSubmit? onSubmit,
}) async {
children.removeWhere((_) => true);
await add(
BackboardGameOver(
score: score,
onSubmit: onSubmit,
),
);
}
}

@ -0,0 +1,119 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
/// Signature for the callback called when the used has
/// submettied their initials on the [BackboardGameOver]
typedef BackboardOnSubmit = void Function(String);
/// {@template backboard_game_over}
/// [PositionComponent] that handles the user input on the
/// game over display view.
/// {@endtemplate}
class BackboardGameOver extends PositionComponent with HasGameRef {
/// {@macro backboard_game_over}
BackboardGameOver({
required int score,
BackboardOnSubmit? onSubmit,
}) : _score = score,
_onSubmit = onSubmit;
final int _score;
final BackboardOnSubmit? _onSubmit;
@override
Future<void> onLoad() async {
final backgroundSprite = await gameRef.loadSprite(
Assets.images.backboard.backboardGameOver.keyName,
);
unawaited(
add(
SpriteComponent(
sprite: backgroundSprite,
size: backgroundSprite.originalSize / 10,
anchor: Anchor.bottomCenter,
),
),
);
final displaySprite = await gameRef.loadSprite(
Assets.images.backboard.display.keyName,
);
unawaited(
add(
SpriteComponent(
sprite: displaySprite,
size: displaySprite.originalSize / 10,
anchor: Anchor.bottomCenter,
position: Vector2(0, -11.5),
),
),
);
unawaited(
add(
TextComponent(
text: _score.formatScore(),
position: Vector2(-22, -46.5),
anchor: Anchor.center,
textRenderer: Backboard.textPaint,
),
),
);
for (var i = 0; i < 3; i++) {
unawaited(
add(
BackboardLetterPrompt(
position: Vector2(
20 + (6 * i).toDouble(),
-46.5,
),
hasFocus: i == 0,
),
),
);
}
unawaited(
add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowLeft: () => _movePrompt(true),
LogicalKeyboardKey.arrowRight: () => _movePrompt(false),
LogicalKeyboardKey.enter: _submit,
},
),
),
);
}
/// Returns the current inputed initials
String get initials => children
.whereType<BackboardLetterPrompt>()
.map((prompt) => prompt.char)
.join();
bool _submit() {
_onSubmit?.call(initials);
return true;
}
bool _movePrompt(bool left) {
final prompts = children.whereType<BackboardLetterPrompt>().toList();
final current = prompts.firstWhere((prompt) => prompt.hasFocus)
..hasFocus = false;
var index = prompts.indexOf(current) + (left ? -1 : 1);
index = min(max(0, index), prompts.length - 1);
prompts[index].hasFocus = true;
return false;
}
}

@ -0,0 +1,106 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template backboard_letter_prompt}
/// A [PositionComponent] that renders a letter prompt used
/// on the [BackboardGameOver]
/// {@endtemplate}
class BackboardLetterPrompt extends PositionComponent {
/// {@macro backboard_letter_prompt}
BackboardLetterPrompt({
required Vector2 position,
bool hasFocus = false,
}) : _hasFocus = hasFocus,
super(
position: position,
);
static const _alphabetCode = 65;
static const _alphabetLength = 25;
var _charIndex = 0;
bool _hasFocus;
late RectangleComponent _underscore;
late TextComponent _input;
late TimerComponent _underscoreBlinker;
@override
Future<void> onLoad() async {
_underscore = RectangleComponent(
size: Vector2(
4,
1.2,
),
anchor: Anchor.center,
position: Vector2(0, 4),
);
unawaited(add(_underscore));
_input = TextComponent(
text: 'A',
textRenderer: Backboard.textPaint,
anchor: Anchor.center,
);
unawaited(add(_input));
_underscoreBlinker = TimerComponent(
period: 0.6,
repeat: true,
autoStart: _hasFocus,
onTick: () {
_underscore.paint.color = (_underscore.paint.color == Colors.white)
? Colors.transparent
: Colors.white;
},
);
unawaited(add(_underscoreBlinker));
unawaited(
add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowUp: () => _cycle(true),
LogicalKeyboardKey.arrowDown: () => _cycle(false),
},
),
),
);
}
/// Returns the current selected character
String get char => String.fromCharCode(_alphabetCode + _charIndex);
bool _cycle(bool up) {
if (_hasFocus) {
final newCharCode =
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength);
_input.text = String.fromCharCode(_alphabetCode + newCharCode);
_charIndex = newCharCode;
return false;
}
return true;
}
/// Returns if this prompt has focus on it
bool get hasFocus => _hasFocus;
/// Updates this prompt focus
set hasFocus(bool hasFocus) {
if (hasFocus) {
_underscoreBlinker.timer.resume();
} else {
_underscoreBlinker.timer.pause();
}
_underscore.paint.color = Colors.white;
_hasFocus = hasFocus;
}
}

@ -0,0 +1,17 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// [PositionComponent] that shows the leaderboard while the player
/// has not started the game yet.
class BackboardWaiting extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardScores.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.bottomCenter;
}
}

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
@ -48,11 +49,15 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
@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);
} }
@ -92,7 +97,8 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
unawaited(gameRef.add(effect)); unawaited(gameRef.add(effect));
} }
_rescale(); _rescaleSize();
_setPositionalGravity();
} }
/// Applies a boost on this [Ball]. /// Applies a boost on this [Ball].
@ -101,19 +107,36 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
_boostTimer = _boostDuration; _boostTimer = _boostDuration;
} }
void _rescale() { void _rescaleSize() {
final boardHeight = BoardDimensions.bounds.height; final boardHeight = BoardDimensions.bounds.height;
const maxShrinkAmount = BoardDimensions.perspectiveShrinkFactor; const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
final adjustedYPosition = -body.position.y + (boardHeight / 2); final standardizedYPosition = body.position.y + (boardHeight / 2);
final scaleFactor = ((boardHeight - adjustedYPosition) / final scaleFactor = maxShrinkValue +
BoardDimensions.shrinkAdjustedHeight) + ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
maxShrinkAmount;
body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor; body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor;
_spriteComponent.scale = Vector2.all(scaleFactor); _spriteComponent.scale = Vector2.all(scaleFactor);
} }
void _setPositionalGravity() {
final defaultGravity = gameRef.world.gravity.y;
final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2;
const maxXGravityPercentage =
(1 - BoardDimensions.perspectiveShrinkFactor) / 2;
final xDeviationFromCenter = body.position.x;
final positionalXForce = ((xDeviationFromCenter / maxXDeviationFromCenter) *
maxXGravityPercentage) *
defaultGravity;
final positionalYForce = math.sqrt(
math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2),
);
body.gravityOverride = Vector2(positionalXForce, positionalYForce);
}
} }
class _BallSpriteComponent extends SpriteComponent with HasGameRef { class _BallSpriteComponent extends SpriteComponent with HasGameRef {

@ -89,10 +89,10 @@ class Baseboard extends BodyComponent with InitialPosition {
@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);

@ -22,8 +22,4 @@ class BoardDimensions {
/// Factor the board shrinks by from the closest point to the farthest. /// Factor the board shrinks by from the closest point to the farthest.
static const perspectiveShrinkFactor = 0.63; static const perspectiveShrinkFactor = 0.63;
/// Board height based on the [perspectiveShrinkFactor].
static final shrinkAdjustedHeight =
(1 / (1 - perspectiveShrinkFactor)) * size.y;
} }

@ -56,12 +56,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
@ -95,10 +97,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);
@ -113,10 +116,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,
);
} }
} }

@ -1,5 +1,5 @@
export 'alien_bumper.dart'; export 'alien_bumper.dart';
export 'backboard.dart'; export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board_dimensions.dart'; export 'board_dimensions.dart';

@ -80,10 +80,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);
} }
@ -131,13 +131,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);
} }

@ -81,10 +81,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(
@ -128,6 +129,7 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
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 +140,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 +154,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 +169,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 +184,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,19 +195,13 @@ 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;
} }

@ -99,9 +99,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;
@ -118,10 +120,12 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
@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);

@ -21,7 +21,9 @@ class FlutterSignPost extends BodyComponent with InitialPosition {
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);
} }
} }

@ -35,7 +35,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 +43,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 +55,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 +67,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 +84,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,7 +112,9 @@ 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);

@ -103,9 +103,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);

@ -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);
} }

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

@ -56,12 +56,12 @@ class Slingshot extends BodyComponent with InitialPosition {
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 +69,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 +81,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,10 +92,11 @@ 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);

@ -71,17 +71,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,
);
} }
} }
@ -107,10 +107,10 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered {
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(
@ -246,18 +246,16 @@ 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() final bodyDef = BodyDef(
..userData = this position: initialPosition,
..position = initialPosition userData: this,
..angle = -1.7 angle: -1.7,
..type = BodyType.static; );
return world.createBody(bodyDef) return world.createBody(bodyDef)..createFixture(fixtureDef);
..createFixture(
FixtureDef(wallShape)..restitution = 1,
);
} }
} }

@ -119,9 +119,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);
@ -183,12 +184,11 @@ class _SpaceshipRailBase extends BodyComponent with InitialPosition {
@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);
} }

@ -75,7 +75,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 +85,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 +101,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);
@ -213,9 +212,10 @@ 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);
@ -267,10 +267,10 @@ 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);
} }

@ -91,10 +91,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;

@ -56,9 +56,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);

@ -0,0 +1,11 @@
import 'package:intl/intl.dart';
final _numberFormat = NumberFormat('#,###');
/// Adds score related extensions to int
extension ScoreX on int {
/// Formats this number as a score value
String formatScore() {
return _numberFormat.format(this);
}
}

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

@ -0,0 +1,34 @@
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
/// The signature for a key handle function
typedef KeyHandlerCallback = bool Function();
/// {@template keyboard_input_controller}
/// A [Component] that receives keyboard input and executes registered methods.
/// {@endtemplate}
class KeyboardInputController extends Component with KeyboardHandler {
/// {@macro keyboard_input_controller}
KeyboardInputController({
Map<LogicalKeyboardKey, KeyHandlerCallback> keyUp = const {},
Map<LogicalKeyboardKey, KeyHandlerCallback> keyDown = const {},
}) : _keyUp = keyUp,
_keyDown = keyDown;
final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyUp;
final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyDown;
@override
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
final isUp = event is RawKeyUpEvent;
final handlers = isUp ? _keyUp : _keyDown;
final handler = handlers[event.logicalKey];
if (handler != null) {
return handler();
}
return true;
}
}

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

@ -13,6 +13,7 @@ dependencies:
sdk: flutter sdk: flutter
geometry: geometry:
path: ../geometry path: ../geometry
intl: ^0.17.0
dev_dependencies: dev_dependencies:

@ -11,6 +11,9 @@ abstract class BasicGame extends Forge2DGame {
} }
} }
abstract class BasicKeyboardGame extends BasicGame
with HasKeyboardHandlerComponents {}
abstract class LineGame extends BasicGame with PanDetector { abstract class LineGame extends BasicGame with PanDetector {
Vector2? _lineEnd; Vector2? _lineEnd;

@ -32,6 +32,7 @@ void main() {
addGoogleWordStories(dashbook); addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook); addLaunchRampStories(dashbook);
addScoreTextStories(dashbook); addScoreTextStories(dashbook);
addBackboardStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -0,0 +1,37 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BackboardGameOverGame extends BasicKeyboardGame {
BackboardGameOverGame(this.score);
static const info = '''
Simple example showing the waiting mode of the backboard.
''';
final int score;
@override
Future<void> onLoad() async {
camera
..followVector2(Vector2.zero())
..zoom = 5;
await add(
Backboard.gameOver(
position: Vector2(0, 20),
score: score,
onSubmit: (initials) {
add(
ScoreText(
text: 'User $initials made $score',
position: Vector2(0, 50),
color: Colors.pink,
),
);
},
),
);
}
}

@ -0,0 +1,27 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/backboard/game_over.dart';
import 'package:sandbox/stories/backboard/waiting.dart';
void addBackboardStories(Dashbook dashbook) {
dashbook.storiesOf('Backboard')
..add(
'Waiting mode',
(context) => GameWidget(
game: BackboardWaitingGame(),
),
codeLink: buildSourceLink('backboard/waiting.dart'),
info: BackboardWaitingGame.info,
)
..add(
'Game over',
(context) => GameWidget(
game: BackboardGameOverGame(
context.numberProperty('score', 9000000000).toInt(),
),
),
codeLink: buildSourceLink('backboard/game_over.dart'),
info: BackboardGameOverGame.info,
);
}

@ -0,0 +1,19 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BackboardWaitingGame extends BasicGame {
static const info = '''
Simple example showing the waiting mode of the backboard.
''';
@override
Future<void> onLoad() async {
camera
..followVector2(Vector2.zero())
..zoom = 5;
final backboard = Backboard.waiting(position: Vector2(0, 20));
await add(backboard);
}
}

@ -1,4 +1,5 @@
export 'alien_zone/stories.dart'; export 'alien_zone/stories.dart';
export 'backboard/stories.dart';
export 'ball/stories.dart'; export 'ball/stories.dart';
export 'baseboard/stories.dart'; export 'baseboard/stories.dart';
export 'boundaries/stories.dart'; export 'boundaries/stories.dart';

@ -149,6 +149,13 @@ packages:
relative: true relative: true
source: path source: path
version: "1.0.0+1" version: "1.0.0+1"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
js: js:
dependency: transitive dependency: transitive
description: description:

@ -1,3 +1,4 @@
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
class TestGame extends Forge2DGame { class TestGame extends Forge2DGame {
@ -5,3 +6,5 @@ class TestGame extends Forge2DGame {
images.prefix = ''; images.prefix = '';
} }
} }
class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents {}

@ -1,7 +1,9 @@
// ignore_for_file: unawaited_futures // ignore_for_file: unawaited_futures, cascade_invocations
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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';
@ -9,7 +11,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
group('Backboard', () { group('Backboard', () {
final tester = FlameTester(TestGame.new); final tester = FlameTester(KeyboardTestGame.new);
group('on waitingMode', () { group('on waitingMode', () {
tester.testGameWidget( tester.testGameWidget(
@ -17,7 +19,7 @@ void main() {
setUp: (game, tester) async { setUp: (game, tester) async {
game.camera.zoom = 2; game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
await game.ensureAdd(Backboard(position: Vector2(0, 15))); await game.ensureAdd(Backboard.waiting(position: Vector2(0, 15)));
}, },
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( await expectLater(
@ -34,20 +36,145 @@ void main() {
setUp: (game, tester) async { setUp: (game, tester) async {
game.camera.zoom = 2; game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
final backboard = Backboard(position: Vector2(0, 15)); final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
onSubmit: (_) {},
);
await game.ensureAdd(backboard);
},
verify: (game, tester) async {
final prompts =
game.descendants().whereType<BackboardLetterPrompt>().length;
expect(prompts, equals(3));
final score = game.descendants().firstWhere(
(component) =>
component is TextComponent && component.text == '1,000',
);
expect(score, isNotNull);
},
);
tester.testGameWidget(
'can change the initials',
setUp: (game, tester) async {
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
onSubmit: (_) {},
);
await game.ensureAdd(backboard); await game.ensureAdd(backboard);
await backboard.gameOverMode(); // Focus is already on the first letter
await game.ready(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// Move to the next an press up again
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// One more time
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// Back to the previous and increase one more
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump(); await tester.pump();
}, },
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( final backboard = game
find.byGame<TestGame>(), .descendants()
matchesGoldenFile('golden/backboard/game_over.png'), .firstWhere((component) => component is BackboardGameOver)
as BackboardGameOver;
expect(backboard.initials, equals('BCB'));
},
);
String? submitedInitials;
tester.testGameWidget(
'submits the initials',
setUp: (game, tester) async {
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
onSubmit: (value) {
submitedInitials = value;
},
); );
await game.ensureAdd(backboard);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
},
verify: (game, tester) async {
expect(submitedInitials, equals('AAA'));
}, },
); );
}); });
}); });
group('BackboardLetterPrompt', () {
final tester = FlameTester(KeyboardTestGame.new);
tester.testGameWidget(
'cycles the char up and down when it has focus',
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(hasFocus: true, position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<BackboardLetterPrompt>();
expect(prompt?.char, equals('C'));
},
);
tester.testGameWidget(
"does nothing when it doesn't have focus",
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<BackboardLetterPrompt>();
expect(prompt?.char, equals('A'));
},
);
tester.testGameWidget(
'blinks the prompt when it has the focus',
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(position: Vector2.zero(), hasFocus: true),
);
},
verify: (game, tester) async {
final underscore = game.descendants().whereType<ShapeComponent>().first;
expect(underscore.paint.color, Colors.white);
game.update(2);
expect(underscore.paint.color, Colors.transparent);
},
);
});
} }

@ -19,9 +19,5 @@ void main() {
test('has perspectiveShrinkFactor', () { test('has perspectiveShrinkFactor', () {
expect(BoardDimensions.perspectiveShrinkFactor, equals(0.63)); expect(BoardDimensions.perspectiveShrinkFactor, equals(0.63));
}); });
test('has shrinkAdjustedHeight', () {
expect(BoardDimensions.shrinkAdjustedHeight, isNotNull);
});
}); });
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

@ -0,0 +1,78 @@
// ignore_for_file: cascade_invocations, one_member_abstracts
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
abstract class _KeyCallStub {
bool onCall();
}
class KeyCallStub extends Mock implements _KeyCallStub {}
class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString();
}
}
RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) {
final event = MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(key);
return event;
}
void main() {
group('KeyboardInputController', () {
test('calls registered handlers', () {
final stub = KeyCallStub();
when(stub.onCall).thenReturn(true);
final input = KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowUp: stub.onCall,
},
);
input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {});
verify(stub.onCall).called(1);
});
test(
'returns false the handler return value',
() {
final stub = KeyCallStub();
when(stub.onCall).thenReturn(false);
final input = KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowUp: stub.onCall,
},
);
expect(
input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}),
isFalse,
);
},
);
test(
'returns true (allowing event to bubble) when no handler is registered',
() {
final stub = KeyCallStub();
when(stub.onCall).thenReturn(true);
final input = KeyboardInputController();
expect(
input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}),
isTrue,
);
},
);
});
}

@ -21,7 +21,6 @@ void main() {
const GameState( const GameState(
score: 0, score: 0,
balls: 2, balls: 2,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
@ -41,14 +40,12 @@ void main() {
const GameState( const GameState(
score: 2, score: 2,
balls: 3, balls: 3,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 5, score: 5,
balls: 3, balls: 3,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
@ -69,21 +66,18 @@ void main() {
const GameState( const GameState(
score: 0, score: 0,
balls: 2, balls: 2,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 0, score: 0,
balls: 1, balls: 1,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
@ -91,103 +85,6 @@ void main() {
); );
}); });
group('BonusLetterActivated', () {
blocTest<GameBloc, GameState>(
'adds the letter to the state',
build: GameBloc.new,
act: (bloc) => bloc
..add(const BonusLetterActivated(0))
..add(const BonusLetterActivated(1))
..add(const BonusLetterActivated(2)),
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: [],
),
],
);
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', () { group('DashNestActivated', () {
blocTest<GameBloc, GameState>( blocTest<GameBloc, GameState>(
'adds the bonus when all nests are activated', 'adds the bonus when all nests are activated',
@ -200,21 +97,18 @@ void main() {
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [],
activatedDashNests: {'0'}, activatedDashNests: {'0'},
bonusHistory: [], bonusHistory: [],
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [],
activatedDashNests: {'0', '1'}, activatedDashNests: {'0', '1'},
bonusHistory: [], bonusHistory: [],
), ),
GameState( GameState(
score: 0, score: 0,
balls: 4, balls: 4,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [GameBonus.dashNest], bonusHistory: [GameBonus.dashNest],
), ),
@ -222,6 +116,33 @@ void main() {
); );
}); });
group(
'BonusActivated',
() {
blocTest<GameBloc, GameState>(
'adds bonus to history',
build: GameBloc.new,
act: (bloc) => bloc
..add(const BonusActivated(GameBonus.googleWord))
..add(const BonusActivated(GameBonus.dashNest)),
expect: () => const [
GameState(
score: 0,
balls: 3,
activatedDashNests: {},
bonusHistory: [GameBonus.googleWord],
),
GameState(
score: 0,
balls: 3,
activatedDashNests: {},
bonusHistory: [GameBonus.googleWord, GameBonus.dashNest],
),
],
);
},
);
group('SparkyTurboChargeActivated', () { group('SparkyTurboChargeActivated', () {
blocTest<GameBloc, GameState>( blocTest<GameBloc, GameState>(
'adds game bonus', 'adds game bonus',
@ -231,7 +152,6 @@ void main() {
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [GameBonus.sparkyTurboCharge], bonusHistory: [GameBonus.sparkyTurboCharge],
), ),

@ -41,61 +41,51 @@ 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('DashNestActivated', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(const DashNestActivated('0'), isNotNull); expect(const DashNestActivated('0'), isNotNull);
}); });
test('supports value equality', () { test('supports value equality', () {
expect( expect(
DashNestActivated('0'), DashNestActivated('0'),
equals(DashNestActivated('0')), equals(DashNestActivated('0')),
); );
expect( expect(
DashNestActivated('0'), DashNestActivated('0'),
isNot(equals(DashNestActivated('1'))), isNot(equals(DashNestActivated('1'))),
); );
});
}); });
});
group('SparkyTurboChargeActivated', () { group('SparkyTurboChargeActivated', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(const SparkyTurboChargeActivated(), isNotNull); expect(const SparkyTurboChargeActivated(), isNotNull);
}); });
test('supports value equality', () { test('supports value equality', () {
expect( expect(
SparkyTurboChargeActivated(), SparkyTurboChargeActivated(),
equals(SparkyTurboChargeActivated()), equals(SparkyTurboChargeActivated()),
); );
});
}); });
}); });
} }

@ -10,7 +10,6 @@ void main() {
GameState( GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: const [],
activatedDashNests: const {}, activatedDashNests: const {},
bonusHistory: const [], bonusHistory: const [],
), ),
@ -18,7 +17,6 @@ void main() {
const GameState( const GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
@ -32,7 +30,6 @@ void main() {
const GameState( const GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
@ -49,7 +46,6 @@ void main() {
() => GameState( () => GameState(
balls: -1, balls: -1,
score: 0, score: 0,
activatedBonusLetters: const [],
activatedDashNests: const {}, activatedDashNests: const {},
bonusHistory: const [], bonusHistory: const [],
), ),
@ -66,7 +62,6 @@ void main() {
() => GameState( () => GameState(
balls: 0, balls: 0,
score: -1, score: -1,
activatedBonusLetters: const [],
activatedDashNests: const {}, activatedDashNests: const {},
bonusHistory: const [], bonusHistory: const [],
), ),
@ -82,7 +77,6 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 0, score: 0,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
@ -95,7 +89,6 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 1, balls: 1,
score: 0, score: 0,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
@ -103,36 +96,6 @@ void main() {
}); });
}); });
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,7 +104,6 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 2, score: 2,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
@ -159,7 +121,6 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 2, score: 2,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
@ -177,16 +138,14 @@ void main() {
const gameState = GameState( const gameState = GameState(
score: 2, score: 2,
balls: 0, balls: 0,
activatedBonusLetters: [],
activatedDashNests: {}, 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],
activatedDashNests: const {'1'}, activatedDashNests: const {'1'},
bonusHistory: const [GameBonus.word], bonusHistory: const [GameBonus.googleWord],
); );
expect(gameState, isNot(equals(otherGameState))); expect(gameState, isNot(equals(otherGameState)));
@ -194,7 +153,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, activatedDashNests: otherGameState.activatedDashNests,
bonusHistory: otherGameState.bonusHistory, bonusHistory: otherGameState.bonusHistory,
), ),

@ -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,
); );

@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.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';
@ -10,7 +11,22 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballTestGame.new);
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () {
final bloc = MockGameBloc();
const state = GameState(
score: 0,
balls: 0,
bonusHistory: [],
activatedDashNests: {},
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
);
group('FlipperController', () { group('FlipperController', () {
group('onKeyEvent', () { group('onKeyEvent', () {
@ -48,6 +64,20 @@ void main() {
); );
}); });
testRawKeyDownEvents(leftKeys, (event) {
flameBlocTester.testGameWidget(
'does nothing when is game over',
setUp: (game, tester) async {
await game.ensureAdd(flipper);
controller.onKeyEvent(event, {});
},
verify: (game, tester) async {
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(leftKeys, (event) { testRawKeyUpEvents(leftKeys, (event) {
flameTester.test( flameTester.test(
'moves downwards ' 'moves downwards '
@ -119,6 +149,20 @@ void main() {
); );
}); });
testRawKeyDownEvents(rightKeys, (event) {
flameBlocTester.testGameWidget(
'does nothing when is game over',
setUp: (game, tester) async {
await game.ensureAdd(flipper);
controller.onKeyEvent(event, {});
},
verify: (game, tester) async {
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(leftKeys, (event) { testRawKeyUpEvents(leftKeys, (event) {
flameTester.test( flameTester.test(
'does nothing ' 'does nothing '

@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
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/services.dart'; import 'package:flutter/services.dart';
@ -11,7 +12,22 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballTestGame.new);
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () {
final bloc = MockGameBloc();
const state = GameState(
score: 0,
balls: 0,
bonusHistory: [],
activatedDashNests: {},
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
);
group('PlungerController', () { group('PlungerController', () {
group('onKeyEvent', () { group('onKeyEvent', () {
@ -73,6 +89,20 @@ void main() {
}, },
); );
}); });
testRawKeyDownEvents(downKeys, (event) {
flameBlocTester.testGameWidget(
'does nothing when is game over',
setUp: (game, tester) async {
await game.ensureAdd(plunger);
controller.onKeyEvent(event, {});
},
verify: (game, tester) async {
expect(plunger.body.linearVelocity.y, isZero);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
}); });
}); });
} }

@ -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(
@ -95,7 +95,6 @@ void main() {
const state = GameState( const state = GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [GameBonus.dashNest], bonusHistory: [GameBonus.dashNest],
); );
@ -158,7 +157,7 @@ void main() {
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new, gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
); );

@ -15,7 +15,6 @@ void main() {
final state = GameState( final state = GameState(
score: 10, score: 10,
balls: 0, balls: 0,
activatedBonusLetters: const [],
bonusHistory: const [], bonusHistory: const [],
activatedDashNests: const {}, activatedDashNests: const {},
); );
@ -42,7 +41,12 @@ void main() {
gameFlowController = GameFlowController(game); gameFlowController = GameFlowController(game);
overlays = MockActiveOverlaysNotifier(); overlays = MockActiveOverlaysNotifier();
when(backboard.gameOverMode).thenAnswer((_) async {}); when(
() => backboard.gameOverMode(
score: any(named: 'score'),
onSubmit: any(named: 'onSubmit'),
),
).thenAnswer((_) async {});
when(backboard.waitingMode).thenAnswer((_) async {}); when(backboard.waitingMode).thenAnswer((_) async {});
when(cameraController.focusOnBackboard).thenAnswer((_) async {}); when(cameraController.focusOnBackboard).thenAnswer((_) async {});
when(cameraController.focusOnGame).thenAnswer((_) async {}); when(cameraController.focusOnGame).thenAnswer((_) async {});
@ -61,13 +65,17 @@ void main() {
GameState( GameState(
score: 10, score: 10,
balls: 0, balls: 0,
activatedBonusLetters: const [],
bonusHistory: const [], bonusHistory: const [],
activatedDashNests: const {}, activatedDashNests: const {},
), ),
); );
verify(backboard.gameOverMode).called(1); verify(
() => backboard.gameOverMode(
score: 0,
onSubmit: any(named: 'onSubmit'),
),
).called(1);
verify(cameraController.focusOnBackboard).called(1); verify(cameraController.focusOnBackboard).called(1);
}, },
); );

@ -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,7 +30,6 @@ void main() {
const current = GameState( const current = GameState(
score: 10, score: 10,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {}, activatedDashNests: {},
); );
@ -44,7 +43,6 @@ void main() {
const current = GameState( const current = GameState(
score: 10, score: 10,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {}, activatedDashNests: {},
); );
@ -70,7 +68,6 @@ void main() {
const state = GameState( const state = GameState(
score: 10, score: 10,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {}, activatedDashNests: {},
); );
@ -89,7 +86,6 @@ void main() {
const GameState( const GameState(
score: 10, score: 10,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {}, activatedDashNests: {},
), ),
@ -99,7 +95,6 @@ void main() {
const GameState( const GameState(
score: 14, score: 14,
balls: 3, balls: 3,
activatedBonusLetters: [],
bonusHistory: [], bonusHistory: [],
activatedDashNests: {}, activatedDashNests: {},
), ),

@ -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('SparkyFireZone', () { group('SparkyFireZone', () {
flameTester.test( flameTester.test(
@ -59,7 +59,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('Wall', () { group('Wall', () {
flameTester.test( flameTester.test(
@ -110,7 +110,7 @@ void main() {
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new, gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
); );

@ -12,8 +12,8 @@ import '../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.new); final flameTester = FlameTester(PinballTestGame.new);
final debugModeFlameTester = FlameTester(DebugPinballGameTest.new); final debugModeFlameTester = FlameTester(DebugPinballTestGame.new);
group('PinballGame', () { group('PinballGame', () {
// TODO(alestiago): test if [PinballGame] registers // TODO(alestiago): test if [PinballGame] registers
@ -88,7 +88,7 @@ void main() {
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new, gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
); );
@ -206,7 +206,7 @@ void main() {
final debugModeFlameBlocTester = final debugModeFlameBlocTester =
FlameBlocTester<DebugPinballGame, GameBloc>( FlameBlocTester<DebugPinballGame, GameBloc>(
gameBuilder: DebugPinballGameTest.new, gameBuilder: DebugPinballTestGame.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
); );

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

@ -11,7 +11,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
const theme = PinballTheme(characterTheme: DashTheme()); const theme = PinballTheme(characterTheme: DashTheme());
final game = PinballGameTest(); final game = PinballTestGame();
group('PinballGamePage', () { group('PinballGamePage', () {
testWidgets('renders PinballGameView', (tester) async { testWidgets('renders PinballGameView', (tester) async {

@ -5,11 +5,10 @@
// https://verygood.ventures // https://verygood.ventures
// license that can be found in the LICENSE file or at // license that can be found in the LICENSE file or at
export 'builders.dart'; export 'builders.dart';
export 'extensions.dart';
export 'fakes.dart'; export 'fakes.dart';
export 'forge2d.dart'; export 'forge2d.dart';
export 'key_testers.dart'; export 'key_testers.dart';
export 'mocks.dart'; export 'mocks.dart';
export 'navigator.dart'; export 'navigator.dart';
export 'pump_app.dart'; export 'pump_app.dart';
export 'test_game.dart'; export 'test_games.dart';

@ -64,8 +64,6 @@ class MockTapUpInfo extends Mock implements TapUpInfo {}
class MockEventPosition extends Mock implements EventPosition {} class MockEventPosition extends Mock implements EventPosition {}
class MockBonusLetter extends Mock implements BonusLetter {}
class MockFilter extends Mock implements Filter {} class MockFilter extends Mock implements Filter {}
class MockFixture extends Mock implements Fixture {} class MockFixture extends Mock implements Fixture {}

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

@ -1,12 +1,20 @@
// ignore_for_file: must_call_super // ignore_for_file: must_call_super
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import 'helpers.dart'; import 'helpers.dart';
class PinballGameTest extends PinballGame { class TestGame extends Forge2DGame with FlameBloc {
PinballGameTest() TestGame() {
images.prefix = '';
}
}
class PinballTestGame extends PinballGame {
PinballTestGame()
: super( : super(
audio: MockPinballAudio(), audio: MockPinballAudio(),
theme: const PinballTheme( theme: const PinballTheme(
@ -15,8 +23,8 @@ class PinballGameTest extends PinballGame {
); );
} }
class DebugPinballGameTest extends DebugPinballGame { class DebugPinballTestGame extends DebugPinballGame {
DebugPinballGameTest() DebugPinballTestGame()
: super( : super(
audio: MockPinballAudio(), audio: MockPinballAudio(),
theme: const PinballTheme( theme: const PinballTheme(
@ -25,7 +33,7 @@ class DebugPinballGameTest extends DebugPinballGame {
); );
} }
class EmptyPinballGameTest extends PinballGameTest { class EmptyPinballTestGame extends PinballTestGame {
@override @override
Future<void> onLoad() async {} Future<void> onLoad() async {}
} }
Loading…
Cancel
Save