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()) {
on<BallLost>(_onBallLost);
on<Scored>(_onScored);
on<BonusLetterActivated>(_onBonusLetterActivated);
on<BonusActivated>(_onBonusActivated);
on<DashNestActivated>(_onDashNestActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
}
static const bonusWord = 'GOOGLE';
static const bonusWordScore = 10000;
void _onBallLost(BallLost event, Emitter emit) {
emit(state.copyWith(balls: state.balls - 1));
}
@ -29,29 +26,12 @@ class GameBloc extends Bloc<GameEvent, GameState> {
}
}
void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) {
final newBonusLetters = [
...state.activatedBonusLetters,
event.letterIndex,
];
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 _onBonusActivated(BonusActivated event, Emitter emit) {
emit(
state.copyWith(
bonusHistory: [...state.bonusHistory, event.bonus],
),
);
}
void _onDashNestActivated(DashNestActivated event, Emitter emit) {

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

@ -4,9 +4,8 @@ part of 'game_bloc.dart';
/// Defines bonuses that a player can gain during a PinballGame.
enum GameBonus {
/// Bonus achieved when the user activate all of the bonus
/// letters on the board, forming the bonus word.
word,
/// Bonus achieved when the ball activates all Google letters.
googleWord,
/// Bonus achieved when the user activates all dash nest bumpers.
dashNest,
@ -23,7 +22,6 @@ class GameState extends Equatable {
const GameState({
required this.score,
required this.balls,
required this.activatedBonusLetters,
required this.bonusHistory,
required this.activatedDashNests,
}) : assert(score >= 0, "Score can't be negative"),
@ -32,7 +30,6 @@ class GameState extends Equatable {
const GameState.initial()
: score = 0,
balls = 3,
activatedBonusLetters = const [],
activatedDashNests = const {},
bonusHistory = const [];
@ -44,9 +41,6 @@ class GameState extends Equatable {
/// When the number of balls is 0, the game is over.
final int balls;
/// Active bonus letters.
final List<int> activatedBonusLetters;
/// Active dash nests.
final Set<String> activatedDashNests;
@ -57,14 +51,9 @@ class GameState extends Equatable {
/// Determines when the game is over.
bool get isGameOver => balls == 0;
/// Shortcut method to check if the given [i]
/// is activated.
bool isLetterActivated(int i) => activatedBonusLetters.contains(i);
GameState copyWith({
int? score,
int? balls,
List<int>? activatedBonusLetters,
Set<String>? activatedDashNests,
List<GameBonus>? bonusHistory,
}) {
@ -76,8 +65,6 @@ class GameState extends Equatable {
return GameState(
score: score ?? this.score,
balls: balls ?? this.balls,
activatedBonusLetters:
activatedBonusLetters ?? this.activatedBonusLetters,
activatedDashNests: activatedDashNests ?? this.activatedDashNests,
bonusHistory: bonusHistory ?? this.bonusHistory,
);
@ -87,7 +74,6 @@ class GameState extends Equatable {
List<Object?> get props => [
score,
balls,
activatedBonusLetters,
activatedDashNests,
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 'board.dart';
export 'bonus_word.dart';
export 'camera_controller.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart';
@ -8,6 +7,7 @@ export 'controlled_plunger.dart';
export 'controlled_sparky_computer.dart';
export 'flutter_forest.dart';
export 'game_flow_controller.dart';
export 'google_word.dart';
export 'launcher.dart';
export 'score_effect_controller.dart';
export 'score_points.dart';

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

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

@ -28,7 +28,11 @@ class GameFlowController extends ComponentController<PinballGame>
/// Puts the game on a game over state
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();
}

@ -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(),
position: Vector2(
_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.backboard.backboardScores.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),
];
}

@ -41,7 +41,7 @@ class PinballGame extends Forge2DGame
// unawaited(add(ScoreEffectController(this)));
unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this)));
unawaited(add(Backboard(position: Vector2(0, -88))));
unawaited(add(Backboard.waiting(position: Vector2(0, -88))));
await _addGameBoundaries();
unawaited(addFromBlueprint(Boundaries()));
@ -72,7 +72,6 @@ class PinballGame extends Forge2DGame
void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback(this));
addContactCallback(BottomWallBallContactCallback());
addContactCallback(BonusLetterBallContactCallback());
}
Future<void> _addGameBoundaries() async {
@ -82,7 +81,7 @@ class PinballGame extends Forge2DGame
Future<void> _addBonusWord() async {
await add(
BonusWord(
GoogleWord(
position: Vector2(
BoardDimensions.bounds.center.dx - 4.1,
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
AssetGenImage get backboardScores =>
const AssetGenImage('assets/images/backboard/backboard_scores.png');
AssetGenImage get display =>
const AssetGenImage('assets/images/backboard/display.png');
}
class $AssetsImagesBaseboardGen {
@ -307,11 +309,8 @@ class $AssetsImagesSparkyBumperGen {
class $AssetsImagesSparkyComputerGen {
const $AssetsImagesSparkyComputerGen();
/// File path: assets/images/sparky/computer/base.png
AssetGenImage get base =>
const AssetGenImage('assets/images/sparky/computer/base.png');
/// File path: assets/images/sparky/computer/top.png
AssetGenImage get top =>
const AssetGenImage('assets/images/sparky/computer/top.png');
}
@ -355,11 +354,8 @@ class $AssetsImagesDashBumperMainGen {
class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen();
/// File path: assets/images/sparky/bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/a/active.png');
/// File path: assets/images/sparky/bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/a/inactive.png');
}
@ -367,11 +363,8 @@ class $AssetsImagesSparkyBumperAGen {
class $AssetsImagesSparkyBumperBGen {
const $AssetsImagesSparkyBumperBGen();
/// File path: assets/images/sparky/bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/b/active.png');
/// File path: assets/images/sparky/bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/b/inactive.png');
}
@ -379,11 +372,8 @@ class $AssetsImagesSparkyBumperBGen {
class $AssetsImagesSparkyBumperCGen {
const $AssetsImagesSparkyBumperCGen();
/// File path: assets/images/sparky/bumper/c/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/c/active.png');
/// File path: assets/images/sparky/bumper/c/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/c/inactive.png');
}

@ -73,13 +73,14 @@ class AlienBumper extends BodyComponent with InitialPosition {
majorRadius: _majorRadius,
minorRadius: _minorRadius,
)..rotate(1.29);
final fixtureDef = FixtureDef(shape)
..friction = 0
..restitution = 4;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
final fixtureDef = FixtureDef(
shape,
restitution: 4,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
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:math' as math;
import 'dart:ui';
import 'package:flame/components.dart';
@ -48,11 +49,15 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(shape)..density = 1;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this
..type = BodyType.dynamic;
final fixtureDef = FixtureDef(
shape,
density: 1,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
type: BodyType.dynamic,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@ -92,7 +97,8 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
unawaited(gameRef.add(effect));
}
_rescale();
_rescaleSize();
_setPositionalGravity();
}
/// Applies a boost on this [Ball].
@ -101,19 +107,36 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
_boostTimer = _boostDuration;
}
void _rescale() {
void _rescaleSize() {
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) /
BoardDimensions.shrinkAdjustedHeight) +
maxShrinkAmount;
final scaleFactor = maxShrinkValue +
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
body.fixtures.first.shape.radius = (size.x / 2) * 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 {

@ -89,10 +89,10 @@ class Baseboard extends BodyComponent with InitialPosition {
@override
Body createBody() {
const angle = 37.1 * (math.pi / 180);
final bodyDef = BodyDef()
..position = initialPosition
..angle = _side.isLeft ? angle : -angle;
final bodyDef = BodyDef(
position: initialPosition,
angle: -angle * _side.direction,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);

@ -22,8 +22,4 @@ class BoardDimensions {
/// Factor the board shrinks by from the closest point to the farthest.
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.
final box = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2);
final fixtureDef = FixtureDef(box)
..shape = box
..density = 999
..friction = 0.3
..restitution = 0.1
..isSensor = true;
final fixtureDef = FixtureDef(
box,
density: 999,
friction: 0.3,
restitution: 0.1,
isSensor: true,
);
fixtureDefs.add(fixtureDef);
// FIXME(alestiago): Investigate why adding these fixtures is considered as
@ -95,10 +97,11 @@ class ChromeDino extends BodyComponent with InitialPosition {
@override
Body createBody() {
final bodyDef = BodyDef()
..gravityScale = Vector2.zero()
..position = initialPosition
..type = BodyType.dynamic;
final bodyDef = BodyDef(
position: initialPosition,
type: BodyType.dynamic,
gravityScale: Vector2.zero(),
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
@ -113,10 +116,7 @@ class ChromeDino extends BodyComponent with InitialPosition {
class _ChromeDinoAnchor extends JointAnchor {
/// {@macro flipper_anchor}
_ChromeDinoAnchor() {
initialPosition = Vector2(
ChromeDino.size.x / 2,
0,
);
initialPosition = Vector2(ChromeDino.size.x / 2, 0);
}
}

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

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

@ -81,10 +81,11 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(
(fixture) => body.createFixture(
@ -128,6 +129,7 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
const restitution = 1.0;
final topStraightControlPoints = [
Vector2(32.4, -8.3),
@ -138,7 +140,10 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
topStraightControlPoints.first,
topStraightControlPoints.last,
);
final topStraightFixtureDef = FixtureDef(topStraightShape);
final topStraightFixtureDef = FixtureDef(
topStraightShape,
restitution: restitution,
);
fixturesDef.add(topStraightFixtureDef);
final topLeftCurveControlPoints = [
@ -149,7 +154,11 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
final topLeftCurveShape = BezierCurveShape(
controlPoints: topLeftCurveControlPoints,
);
fixturesDef.add(FixtureDef(topLeftCurveShape));
final topLeftCurveFixtureDef = FixtureDef(
topLeftCurveShape,
restitution: restitution,
);
fixturesDef.add(topLeftCurveFixtureDef);
final bottomLeftStraightControlPoints = [
topLeftCurveControlPoints.last,
@ -160,7 +169,10 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
bottomLeftStraightControlPoints.first,
bottomLeftStraightControlPoints.last,
);
final bottomLeftStraightFixtureDef = FixtureDef(bottomLeftStraightShape);
final bottomLeftStraightFixtureDef = FixtureDef(
bottomLeftStraightShape,
restitution: restitution,
);
fixturesDef.add(bottomLeftStraightFixtureDef);
final bottomStraightControlPoints = [
@ -172,7 +184,10 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
bottomStraightControlPoints.first,
bottomStraightControlPoints.last,
);
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
final bottomStraightFixtureDef = FixtureDef(
bottomStraightShape,
restitution: restitution,
);
fixturesDef.add(bottomStraightFixtureDef);
return fixturesDef;
@ -180,19 +195,13 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(
(fixture) => body.createFixture(
fixture
..restitution = 0.1
..friction = 0,
),
);
_createFixtureDefs().forEach(body.createFixture);
return body;
}

@ -99,9 +99,11 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
];
final trapezium = PolygonShape()..set(trapeziumVertices);
final trapeziumFixtureDef = FixtureDef(trapezium)
..density = 50.0 // TODO(alestiago): Use a proper density.
..friction = .1; // TODO(alestiago): Use a proper friction.
final trapeziumFixtureDef = FixtureDef(
trapezium,
density: 50, // TODO(alestiago): Use a proper density.
friction: .1, // TODO(alestiago): Use a proper friction.
);
fixturesDef.add(trapeziumFixtureDef);
return fixturesDef;
@ -118,10 +120,12 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
@override
Body createBody() {
final bodyDef = BodyDef()
..position = initialPosition
..gravityScale = Vector2.zero()
..type = BodyType.dynamic;
final bodyDef = BodyDef(
position: initialPosition,
gravityScale: Vector2.zero(),
type: BodyType.dynamic,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);

@ -21,7 +21,9 @@ class FlutterSignPost extends BodyComponent with InitialPosition {
Body createBody() {
final shape = CircleShape()..radius = 0.25;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()..position = initialPosition;
final bodyDef = BodyDef(
position: initialPosition,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}

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

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

@ -35,7 +35,7 @@ class Kicker extends BodyComponent with InitialPosition {
final upperCircle = CircleShape()..radius = 1.6;
upperCircle.position.setValues(0, upperCircle.radius / 2);
final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0;
final upperCircleFixtureDef = FixtureDef(upperCircle);
fixturesDefs.add(upperCircleFixtureDef);
final lowerCircle = CircleShape()..radius = 1.6;
@ -43,7 +43,7 @@ class Kicker extends BodyComponent with InitialPosition {
size.x * -direction,
size.y + 0.8,
);
final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0;
final lowerCircleFixtureDef = FixtureDef(lowerCircle);
fixturesDefs.add(lowerCircleFixtureDef);
final wallFacingEdge = EdgeShape()
@ -55,7 +55,7 @@ class Kicker extends BodyComponent with InitialPosition {
),
Vector2(2.5 * direction, size.y - 2),
);
final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0;
final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge);
fixturesDefs.add(wallFacingLineFixtureDef);
final bottomEdge = EdgeShape()
@ -67,7 +67,7 @@ class Kicker extends BodyComponent with InitialPosition {
lowerCircle.radius * math.sin(quarterPi),
),
);
final bottomLineFixtureDef = FixtureDef(bottomEdge)..friction = 0;
final bottomLineFixtureDef = FixtureDef(bottomEdge);
fixturesDefs.add(bottomLineFixtureDef);
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.
..restitution = 10.0
..friction = 0;
restitution: 10,
);
fixturesDefs.add(bouncyFixtureDef);
// TODO(alestiago): Evaluate if there is value on centering the fixtures.
@ -111,7 +112,9 @@ class Kicker extends BodyComponent with InitialPosition {
@override
Body createBody() {
final bodyDef = BodyDef()..position = initialPosition;
final bodyDef = BodyDef(
position: initialPosition,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);

@ -103,9 +103,10 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);

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

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

@ -56,12 +56,12 @@ class Slingshot extends BodyComponent with InitialPosition {
final topCircleShape = CircleShape()..radius = circleRadius;
topCircleShape.position.setValues(0, -_length / 2);
final topCircleFixtureDef = FixtureDef(topCircleShape)..friction = 0;
final topCircleFixtureDef = FixtureDef(topCircleShape);
fixturesDef.add(topCircleFixtureDef);
final bottomCircleShape = CircleShape()..radius = circleRadius;
bottomCircleShape.position.setValues(0, _length / 2);
final bottomCircleFixtureDef = FixtureDef(bottomCircleShape)..friction = 0;
final bottomCircleFixtureDef = FixtureDef(bottomCircleShape);
fixturesDef.add(bottomCircleFixtureDef);
final leftEdgeShape = EdgeShape()
@ -69,9 +69,11 @@ class Slingshot extends BodyComponent with InitialPosition {
Vector2(circleRadius, _length / 2),
Vector2(circleRadius, -_length / 2),
);
final leftEdgeShapeFixtureDef = FixtureDef(leftEdgeShape)
..friction = 0
..restitution = 5;
final leftEdgeShapeFixtureDef = FixtureDef(
leftEdgeShape,
restitution: 5,
);
fixturesDef.add(leftEdgeShapeFixtureDef);
final rightEdgeShape = EdgeShape()
@ -79,9 +81,10 @@ class Slingshot extends BodyComponent with InitialPosition {
Vector2(-circleRadius, _length / 2),
Vector2(-circleRadius, -_length / 2),
);
final rightEdgeShapeFixtureDef = FixtureDef(rightEdgeShape)
..friction = 0
..restitution = 5;
final rightEdgeShapeFixtureDef = FixtureDef(
rightEdgeShape,
restitution: 5,
);
fixturesDef.add(rightEdgeShapeFixtureDef);
return fixturesDef;
@ -89,10 +92,11 @@ class Slingshot extends BodyComponent with InitialPosition {
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..angle = _angle;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
angle: _angle,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);

@ -71,17 +71,17 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
@override
Body createBody() {
final circleShape = CircleShape()..radius = 3;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
final shape = CircleShape()..radius = 3;
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)
..createFixture(
FixtureDef(circleShape)..isSensor = true,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
@ -107,10 +107,10 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered {
Body createBody() {
final circleShape = CircleShape()..radius = 2;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)
..createFixture(
@ -246,18 +246,16 @@ class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
Body createBody() {
renderBody = false;
final wallShape = _SpaceshipWallShape();
final shape = _SpaceshipWallShape();
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..angle = -1.7
..type = BodyType.static;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
angle: -1.7,
);
return world.createBody(bodyDef)
..createFixture(
FixtureDef(wallShape)..restitution = 1,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -119,9 +119,10 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered {
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
@ -183,12 +184,11 @@ class _SpaceshipRailBase extends BodyComponent with InitialPosition {
@override
Body createBody() {
final shape = CircleShape()..radius = radius;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}

@ -75,7 +75,6 @@ class _SpaceshipRampBackground extends BodyComponent
Vector2(-14.2, -71.25),
],
);
final outerLeftCurveFixtureDef = FixtureDef(outerLeftCurveShape);
fixturesDef.add(outerLeftCurveFixtureDef);
@ -86,7 +85,6 @@ class _SpaceshipRampBackground extends BodyComponent
Vector2(6.1, -44.9),
],
);
final outerRightCurveFixtureDef = FixtureDef(outerRightCurveShape);
fixturesDef.add(outerRightCurveFixtureDef);
@ -103,9 +101,10 @@ class _SpaceshipRampBackground extends BodyComponent
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
@ -213,9 +212,10 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
@ -267,10 +267,10 @@ class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered {
],
);
final fixtureDef = FixtureDef(baseShape);
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}

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

@ -56,9 +56,10 @@ class _ComputerBase extends BodyComponent with InitialPosition {
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final body = world.createBody(bodyDef);
_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 'keyboard_input_controller.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 'extensions/extensions.dart';
export 'flame/flame.dart';

@ -13,6 +13,7 @@ dependencies:
sdk: flutter
geometry:
path: ../geometry
intl: ^0.17.0
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 {
Vector2? _lineEnd;

@ -32,6 +32,7 @@ void main() {
addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook);
addScoreTextStories(dashbook);
addBackboardStories(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 'backboard/stories.dart';
export 'ball/stories.dart';
export 'baseboard/stories.dart';
export 'boundaries/stories.dart';

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

@ -1,3 +1,4 @@
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
class TestGame extends Forge2DGame {
@ -5,3 +6,5 @@ class TestGame extends Forge2DGame {
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_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
@ -9,7 +11,7 @@ import '../../helpers/helpers.dart';
void main() {
group('Backboard', () {
final tester = FlameTester(TestGame.new);
final tester = FlameTester(KeyboardTestGame.new);
group('on waitingMode', () {
tester.testGameWidget(
@ -17,7 +19,7 @@ void main() {
setUp: (game, tester) async {
game.camera.zoom = 2;
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 {
await expectLater(
@ -34,20 +36,145 @@ void main() {
setUp: (game, tester) async {
game.camera.zoom = 2;
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 backboard.gameOverMode();
await game.ready();
// Focus is already on the first letter
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();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/backboard/game_over.png'),
final backboard = game
.descendants()
.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', () {
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(
score: 0,
balls: 2,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
@ -41,14 +40,12 @@ void main() {
const GameState(
score: 2,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 5,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
@ -69,21 +66,18 @@ void main() {
const GameState(
score: 0,
balls: 2,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 1,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
activatedDashNests: {},
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', () {
blocTest<GameBloc, GameState>(
'adds the bonus when all nests are activated',
@ -200,21 +97,18 @@ void main() {
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {'0'},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {'0', '1'},
bonusHistory: [],
),
GameState(
score: 0,
balls: 4,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
),
@ -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', () {
blocTest<GameBloc, GameState>(
'adds game bonus',
@ -231,7 +152,6 @@ void main() {
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.sparkyTurboCharge],
),

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

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

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

@ -9,7 +9,7 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
final flameTester = FlameTester(EmptyPinballTestGame.new);
group('Board', () {
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>(
gameBuilder: EmptyPinballGameTest.new,
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);

@ -1,5 +1,6 @@
import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@ -10,7 +11,22 @@ import '../../helpers/helpers.dart';
void main() {
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('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) {
flameTester.test(
'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) {
flameTester.test(
'does nothing '

@ -1,5 +1,6 @@
import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart';
@ -11,7 +12,22 @@ import '../../helpers/helpers.dart';
void main() {
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('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() {
group('SparkyComputerController', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
final flameTester = FlameTester(EmptyPinballTestGame.new);
late ControlledSparkyComputer controlledSparkyComputer;

@ -12,7 +12,7 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
final flameTester = FlameTester(EmptyPinballTestGame.new);
group('FlutterForest', () {
flameTester.test(
@ -95,7 +95,6 @@ void main() {
const state = GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
);
@ -158,7 +157,7 @@ void main() {
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);

@ -15,7 +15,6 @@ void main() {
final state = GameState(
score: 10,
balls: 0,
activatedBonusLetters: const [],
bonusHistory: const [],
activatedDashNests: const {},
);
@ -42,7 +41,12 @@ void main() {
gameFlowController = GameFlowController(game);
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(cameraController.focusOnBackboard).thenAnswer((_) async {});
when(cameraController.focusOnGame).thenAnswer((_) async {});
@ -61,13 +65,17 @@ void main() {
GameState(
score: 10,
balls: 0,
activatedBonusLetters: const [],
bonusHistory: const [],
activatedDashNests: const {},
),
);
verify(backboard.gameOverMode).called(1);
verify(
() => backboard.gameOverMode(
score: 0,
onSubmit: any(named: 'onSubmit'),
),
).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(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
);
@ -44,7 +43,6 @@ void main() {
const current = GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
);
@ -70,7 +68,6 @@ void main() {
const state = GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
);
@ -89,7 +86,6 @@ void main() {
const GameState(
score: 10,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
),
@ -99,7 +95,6 @@ void main() {
const GameState(
score: 14,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
activatedDashNests: {},
),

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

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

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

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

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

@ -5,11 +5,10 @@
// https://verygood.ventures
// license that can be found in the LICENSE file or at
export 'builders.dart';
export 'extensions.dart';
export 'fakes.dart';
export 'forge2d.dart';
export 'key_testers.dart';
export 'mocks.dart';
export 'navigator.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 MockBonusLetter extends Mock implements BonusLetter {}
class MockFilter extends Mock implements Filter {}
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
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart';
import 'helpers.dart';
class PinballGameTest extends PinballGame {
PinballGameTest()
class TestGame extends Forge2DGame with FlameBloc {
TestGame() {
images.prefix = '';
}
}
class PinballTestGame extends PinballGame {
PinballTestGame()
: super(
audio: MockPinballAudio(),
theme: const PinballTheme(
@ -15,8 +23,8 @@ class PinballGameTest extends PinballGame {
);
}
class DebugPinballGameTest extends DebugPinballGame {
DebugPinballGameTest()
class DebugPinballTestGame extends DebugPinballGame {
DebugPinballTestGame()
: super(
audio: MockPinballAudio(),
theme: const PinballTheme(
@ -25,7 +33,7 @@ class DebugPinballGameTest extends DebugPinballGame {
);
}
class EmptyPinballGameTest extends PinballGameTest {
class EmptyPinballTestGame extends PinballTestGame {
@override
Future<void> onLoad() async {}
}
Loading…
Cancel
Save