refactor: add `Backbox` asset and localized text (#319)

* refactor: clean up backboard

* test: backboard changes

* refactor: rename backbox

* fix: tests

* test: localization mocking

* test: initials input display

* chore: remove extra golden file

* chore: small changes

* test: scoreFormat

* style: cascade

* fix: no optimizations for main

* fix: loading assets

Co-authored-by: Tom Arra <tarra3@gmail.com>
pull/336/head
Allison Ryan 2 years ago committed by GitHub
parent b01444c902
commit 6ed62f37e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,3 +13,4 @@ jobs:
flutter_channel: stable
flutter_version: 2.10.0
coverage_excludes: "lib/gen/*.dart"
test_optimization: false

@ -0,0 +1,56 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template backbox}
/// The [Backbox] of the pinball machine.
/// {@endtemplate}
class Backbox extends PositionComponent with HasGameRef, ZIndex {
/// {@macro backbox}
Backbox()
: super(
position: Vector2(0, -87),
anchor: Anchor.bottomCenter,
children: [
_BackboxSpriteComponent(),
],
) {
zIndex = ZIndexes.backbox;
}
/// Puts [InitialsInputDisplay] on the [Backbox].
Future<void> initialsInput({
required int score,
required String characterIconPath,
InitialsOnSubmit? onSubmit,
}) async {
removeAll(children.where((child) => child is! _BackboxSpriteComponent));
await add(
InitialsInputDisplay(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
),
);
}
}
class _BackboxSpriteComponent extends SpriteComponent with HasGameRef {
_BackboxSpriteComponent() : super(anchor: Anchor.bottomCenter);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.backbox.marquee.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}

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

@ -0,0 +1,387 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Signature for the callback called when the used has
/// submitted their initials on the [InitialsInputDisplay].
typedef InitialsOnSubmit = void Function(String);
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 3,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
final _subtitleTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.8,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template initials_input_display}
/// Display that handles the user input on the game over view.
/// {@endtemplate}
// TODO(allisonryan0002): add mobile input buttons.
class InitialsInputDisplay extends Component with HasGameRef {
/// {@macro initials_input_display}
InitialsInputDisplay({
required int score,
required String characterIconPath,
InitialsOnSubmit? onSubmit,
}) : _onSubmit = onSubmit,
super(
children: [
_ScoreLabelTextComponent(),
_ScoreTextComponent(score.formatScore()),
_NameLabelTextComponent(),
_CharacterIconSpriteComponent(characterIconPath),
_DividerSpriteComponent(),
_InstructionsComponent(),
],
);
final InitialsOnSubmit? _onSubmit;
@override
Future<void> onLoad() async {
for (var i = 0; i < 3; i++) {
await add(
InitialsLetterPrompt(
position: Vector2(
11.4 + (2.3 * i),
-20,
),
hasFocus: i == 0,
),
);
}
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowLeft: () => _movePrompt(true),
LogicalKeyboardKey.arrowRight: () => _movePrompt(false),
LogicalKeyboardKey.enter: _submit,
},
),
);
}
/// Returns the current inputed initials
String get initials => children
.whereType<InitialsLetterPrompt>()
.map((prompt) => prompt.char)
.join();
bool _submit() {
_onSubmit?.call(initials);
return true;
}
bool _movePrompt(bool left) {
final prompts = children.whereType<InitialsLetterPrompt>().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;
}
}
class _ScoreLabelTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_ScoreLabelTextComponent()
: super(
anchor: Anchor.centerLeft,
position: Vector2(-16.9, -24),
textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith(
color: PinballColors.red,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.score;
}
}
class _ScoreTextComponent extends TextComponent {
_ScoreTextComponent(String score)
: super(
text: score,
anchor: Anchor.centerLeft,
position: Vector2(-16.9, -20),
textRenderer: _bodyTextPaint,
);
}
class _NameLabelTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_NameLabelTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(11.4, -24),
textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith(
color: PinballColors.red,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.name;
}
}
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
_CharacterIconSpriteComponent(String characterIconPath)
: _characterIconPath = characterIconPath,
super(
anchor: Anchor.center,
position: Vector2(8.4, -20),
);
final String _characterIconPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
/// {@template initials_input_display}
/// Display that handles the user input on the game over view.
/// {@endtemplate}
@visibleForTesting
class InitialsLetterPrompt extends PositionComponent {
/// {@macro initials_input_display}
InitialsLetterPrompt({
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(1.9, 0.4),
anchor: Anchor.center,
position: Vector2(-0.1, 1.8),
);
await add(_underscore);
_input = TextComponent(
text: 'A',
textRenderer: _bodyTextPaint,
anchor: Anchor.center,
);
await add(_input);
_underscoreBlinker = TimerComponent(
period: 0.6,
repeat: true,
autoStart: _hasFocus,
onTick: () {
_underscore.paint.color = (_underscore.paint.color == Colors.white)
? Colors.transparent
: Colors.white;
},
);
await add(_underscoreBlinker);
await 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;
}
}
class _DividerSpriteComponent extends SpriteComponent with HasGameRef {
_DividerSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -17),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(Assets.images.backbox.displayDivider.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
class _InstructionsComponent extends PositionComponent with HasGameRef {
_InstructionsComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -12.3),
children: [
_EnterInitialsTextComponent(),
_ArrowsTextComponent(),
_AndPressTextComponent(),
_EnterReturnTextComponent(),
_ToSubmitTextComponent(),
],
);
}
class _EnterInitialsTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_EnterInitialsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.enterInitials;
}
}
class _ArrowsTextComponent extends TextComponent with HasGameRef<PinballGame> {
_ArrowsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-13.2, 0),
textRenderer: _subtitleTextPaint.copyWith(
(style) => style.copyWith(
fontWeight: FontWeight.bold,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.arrows;
}
}
class _AndPressTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_AndPressTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-3.7, 0),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.andPress;
}
}
class _EnterReturnTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_EnterReturnTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(10, 0),
textRenderer: _subtitleTextPaint.copyWith(
(style) => style.copyWith(
fontWeight: FontWeight.bold,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.enterReturn;
}
}
class _ToSubmitTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_ToSubmitTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.toSubmit;
}
}

@ -3,15 +3,15 @@ import 'package:flame/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds helpers methods to Flame's [Camera]
/// Adds helpers methods to Flame's [Camera].
extension CameraX on Camera {
/// Instantly apply the point of focus to the [Camera]
/// Instantly apply the point of focus to the [Camera].
void snapToFocus(FocusData data) {
followVector2(data.position);
zoom = data.zoom;
}
/// Returns a [CameraZoom] that can be added to a [FlameGame]
/// Returns a [CameraZoom] that can be added to a [FlameGame].
CameraZoom focusToCameraZoom(FocusData data) {
final zoom = CameraZoom(value: data.zoom);
zoom.completed.then((_) {
@ -22,7 +22,7 @@ extension CameraX on Camera {
}
/// {@template focus_data}
/// Model class that defines a focus point of the camera
/// Model class that defines a focus point of the camera.
/// {@endtemplate}
class FocusData {
/// {@template focus_data}
@ -31,50 +31,63 @@ class FocusData {
required this.position,
});
/// The amount of zoom
/// The amount of zoom.
final double zoom;
/// The position of the camera
/// The position of the camera.
final Vector2 position;
}
/// {@template camera_controller}
/// A [Component] that controls its game camera focus
/// A [Component] that controls its game camera focus.
/// {@endtemplate}
class CameraController extends ComponentController<FlameGame> {
/// {@macro camera_controller}
CameraController(FlameGame component) : super(component) {
final gameZoom = component.size.y / 16;
final backboardZoom = component.size.y / 18;
final waitingBackboxZoom = component.size.y / 18;
final gameOverBackboxZoom = component.size.y / 10;
gameFocus = FocusData(
zoom: gameZoom,
position: Vector2(0, -7.8),
);
backboardFocus = FocusData(
zoom: backboardZoom,
position: Vector2(0, -100.8),
waitingBackboxFocus = FocusData(
zoom: waitingBackboxZoom,
position: Vector2(0, -112),
);
gameOverBackboxFocus = FocusData(
zoom: gameOverBackboxZoom,
position: Vector2(0, -111),
);
// Game starts with the camera focused on the panel
// Game starts with the camera focused on the [Backbox].
component.camera
..speed = 100
..snapToFocus(backboardFocus);
..snapToFocus(waitingBackboxFocus);
}
/// Holds the data for the game focus point
/// Holds the data for the game focus point.
late final FocusData gameFocus;
/// Holds the data for the backboard focus point
late final FocusData backboardFocus;
/// Holds the data for the waiting backbox focus point.
late final FocusData waitingBackboxFocus;
/// Holds the data for the game over backbox focus point.
late final FocusData gameOverBackboxFocus;
/// Move the camera focus to the game board
/// Move the camera focus to the game board.
void focusOnGame() {
component.add(component.camera.focusToCameraZoom(gameFocus));
}
/// Move the camera focus to the backboard
void focusOnBackboard() {
component.add(component.camera.focusToCameraZoom(backboardFocus));
/// Move the camera focus to the waiting backbox.
void focusOnWaitingBackbox() {
component.add(component.camera.focusToCameraZoom(waitingBackboxFocus));
}
/// Move the camera focus to the game over backbox.
void focusOnGameOverBackbox() {
component.add(component.camera.focusToCameraZoom(gameOverBackboxFocus));
}
}

@ -1,4 +1,5 @@
export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart';
export 'bottom_group.dart';
export 'camera_controller.dart';
export 'controlled_ball.dart';

@ -1,7 +1,6 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template game_flow_controller}
@ -20,27 +19,26 @@ class GameFlowController extends ComponentController<PinballGame>
@override
void onNewState(GameState state) {
if (state.isGameOver) {
gameOver();
_initialsInput();
} else {
start();
}
}
/// Puts the game on a game over state
void gameOver() {
/// Puts the game in the initials input state.
void _initialsInput() {
// TODO(erickzanardo): implement score submission and "navigate" to the
// next page
component.firstChild<Backboard>()?.gameOverMode(
component.descendants().whereType<Backbox>().first.initialsInput(
score: state?.score ?? 0,
characterIconPath: component.characterTheme.leaderboardIcon.keyName,
);
component.firstChild<CameraController>()?.focusOnBackboard();
component.firstChild<CameraController>()!.focusOnGameOverBackbox();
}
/// Puts the game on a playing state
/// Puts the game in the playing state.
void start() {
component.audio.backgroundMusic();
component.firstChild<Backboard>()?.waitingMode();
component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay);
}

@ -99,8 +99,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName),
images.load(components.Assets.images.sparky.bumper.c.lit.keyName),
images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName),
images.load(components.Assets.images.backboard.backboardScores.keyName),
images.load(components.Assets.images.backboard.backboardGameOver.keyName),
images.load(components.Assets.images.backbox.marquee.keyName),
images.load(components.Assets.images.backbox.displayDivider.keyName),
images.load(components.Assets.images.googleWord.letter1.lit.keyName),
images.load(components.Assets.images.googleWord.letter1.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter2.lit.keyName),
@ -113,7 +113,6 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter5.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiball.lit.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName),

@ -8,6 +8,7 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -22,6 +23,7 @@ class PinballGame extends PinballForge2DGame
PinballGame({
required this.characterTheme,
required this.audio,
required this.l10n,
}) : super(gravity: Vector2(0, 30)) {
images.prefix = '';
controller = _GameBallsController(this);
@ -37,6 +39,8 @@ class PinballGame extends PinballForge2DGame
final PinballAudio audio;
final AppLocalizations l10n;
late final GameFlowController gameFlowController;
@override
@ -47,7 +51,7 @@ class PinballGame extends PinballForge2DGame
final machine = [
BoardBackgroundSpriteComponent(),
Boundaries(),
Backboard.waiting(position: Vector2(0, -88)),
Backbox(),
];
final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)),
@ -167,9 +171,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({
required CharacterTheme characterTheme,
required PinballAudio audio,
required AppLocalizations l10n,
}) : super(
characterTheme: characterTheme,
audio: audio,
l10n: l10n,
) {
controller = _GameBallsController(this);
}

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
@ -39,8 +40,16 @@ class PinballGamePage extends StatelessWidget {
final pinballAudio = context.read<PinballAudio>();
final game = isDebugMode
? DebugPinballGame(characterTheme: characterTheme, audio: audio)
: PinballGame(characterTheme: characterTheme, audio: audio);
? DebugPinballGame(
characterTheme: characterTheme,
audio: audio,
l10n: context.l10n,
)
: PinballGame(
characterTheme: characterTheme,
audio: audio,
l10n: context.l10n,
);
final loadables = [
...game.preLoadAssets(),

@ -64,49 +64,45 @@
"@gameOver": {
"description": "Text displayed on the ending dialog when game finishes"
},
"leaderboard": "Leaderboard",
"@leaderboard": {
"description": "Text displayed on the ending dialog leaderboard button"
"rounds": "Ball Ct:",
"@rounds": {
"description": "Text displayed on the scoreboard widget to indicate rounds left"
},
"rank": "Rank",
"@rank": {
"description": "Text displayed on the leaderboard page header rank column"
"topPlayers": "Top Players",
"@topPlayers": {
"description": "Title text displayed on leaderboard screen"
},
"character": "Character",
"@character": {
"description": "Text displayed on the leaderboard page header character column"
"rank": "rank",
"@rank": {
"description": "Label text displayed above player's rank"
},
"username": "Username",
"@username": {
"description": "Text displayed on the leaderboard page header userName column"
"name": "name",
"@name": {
"description": "Label text displayed above player's initials"
},
"score": "Score",
"score": "score",
"@score": {
"description": "Text displayed on the leaderboard page header score column"
},
"retry": "Retry",
"@retry": {
"description": "Text displayed on the retry button leaders board page"
"description": "Label text displayed above player's score"
},
"addUser": "Add User",
"@addUser": {
"description": "Text displayed on the add user button at ending dialog"
"enterInitials": "Enter your initials using the",
"@enterInitials": {
"description": "Informational text displayed on initials input screen"
},
"error": "Error",
"@error": {
"description": "Text displayed on the ending dialog when there is any error on sending user"
"arrows": "arrows",
"@arrows": {
"description": "Text displayed on initials input screen indicating arrow keys"
},
"yourScore": "Your score is",
"@yourScore": {
"description": "Text displayed on the ending dialog when game finishes to show the final score"
"andPress": "and press",
"@andPress": {
"description": "Connecting text displayed on initials input screen informational text span"
},
"enterInitials": "Enter your initials",
"@enterInitials": {
"description": "Text displayed on the ending dialog when game finishes to ask the user for his initials"
"enterReturn": "enter/return",
"@enterReturn": {
"description": "Text displayed on initials input screen indicating return key"
},
"rounds": "Ball Ct:",
"@rounds": {
"description": "Text displayed on the scoreboard widget to indicate rounds left"
"toSubmit": "to submit",
"@toSubmit": {
"description": "Ending text displayed on initials input screen informational text span"
},
"footerMadeWithText": "Made with ",
"@footerMadeWithText": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

@ -11,7 +11,7 @@ class $AssetsImagesGen {
const $AssetsImagesGen();
$AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
$AssetsImagesBackboxGen get backbox => const $AssetsImagesBackboxGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
@ -50,20 +50,16 @@ class $AssetsImagesAndroidGen {
const $AssetsImagesAndroidSpaceshipGen();
}
class $AssetsImagesBackboardGen {
const $AssetsImagesBackboardGen();
class $AssetsImagesBackboxGen {
const $AssetsImagesBackboxGen();
/// File path: assets/images/backboard/backboard_game_over.png
AssetGenImage get backboardGameOver =>
const AssetGenImage('assets/images/backboard/backboard_game_over.png');
/// File path: assets/images/backbox/display-divider.png
AssetGenImage get displayDivider =>
const AssetGenImage('assets/images/backbox/display-divider.png');
/// File path: assets/images/backboard/backboard_scores.png
AssetGenImage get backboardScores =>
const AssetGenImage('assets/images/backboard/backboard_scores.png');
/// File path: assets/images/backboard/display.png
AssetGenImage get display =>
const AssetGenImage('assets/images/backboard/display.png');
/// File path: assets/images/backbox/marquee.png
AssetGenImage get marquee =>
const AssetGenImage('assets/images/backbox/marquee.png');
}
class $AssetsImagesBallGen {

@ -1,79 +0,0 @@
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 String characterIconPath,
required int score,
required BackboardOnSubmit onSubmit,
}) {
return Backboard(position: position)
..gameOverMode(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
);
}
/// [TextPaint] used on the [Backboard]
static final textPaint = TextPaint(
style: const 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,
required String characterIconPath,
BackboardOnSubmit? onSubmit,
}) async {
children.removeWhere((_) => true);
await add(
BackboardGameOver(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
),
);
}
}

@ -1,144 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.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,
required String characterIconPath,
BackboardOnSubmit? onSubmit,
}) : _onSubmit = onSubmit,
super(
children: [
_BackboardSpriteComponent(),
_BackboardDisplaySpriteComponent(),
_ScoreTextComponent(score.formatScore()),
_CharacterIconSpriteComponent(characterIconPath),
],
);
final BackboardOnSubmit? _onSubmit;
@override
Future<void> onLoad() async {
for (var i = 0; i < 3; i++) {
await add(
BackboardLetterPrompt(
position: Vector2(
24.3 + (4.5 * i),
-45,
),
hasFocus: i == 0,
),
);
}
await 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;
}
}
class _BackboardSpriteComponent extends SpriteComponent with HasGameRef {
_BackboardSpriteComponent() : super(anchor: Anchor.bottomCenter);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardGameOver.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _BackboardDisplaySpriteComponent extends SpriteComponent with HasGameRef {
_BackboardDisplaySpriteComponent()
: super(
anchor: Anchor.bottomCenter,
position: Vector2(0, -11.5),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.backboard.display.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _ScoreTextComponent extends TextComponent {
_ScoreTextComponent(String score)
: super(
text: score,
anchor: Anchor.centerLeft,
position: Vector2(-34, -45),
textRenderer: Backboard.textPaint,
);
}
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
_CharacterIconSpriteComponent(String characterIconPath)
: _characterIconPath = characterIconPath,
super(
anchor: Anchor.center,
position: Vector2(18.4, -45),
);
final String _characterIconPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

@ -1,102 +0,0 @@
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';
import 'package:pinball_flame/pinball_flame.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(3.8, 0.8),
anchor: Anchor.center,
position: Vector2(-0.3, 4),
);
await add(_underscore);
_input = TextComponent(
text: 'A',
textRenderer: Backboard.textPaint,
anchor: Anchor.center,
);
await add(_input);
_underscoreBlinker = TimerComponent(
period: 0.6,
repeat: true,
autoStart: _hasFocus,
onTick: () {
_underscore.paint.color = (_underscore.paint.color == Colors.white)
? Colors.transparent
: Colors.white;
},
);
await add(_underscoreBlinker);
await 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;
}
}

@ -1,17 +0,0 @@
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,7 +1,6 @@
export 'android_animatronic.dart';
export 'android_bumper/android_bumper.dart';
export 'android_spaceship/android_spaceship.dart';
export 'backboard/backboard.dart';
export 'ball/ball.dart';
export 'baseboard.dart';
export 'board_background_sprite_component.dart';

@ -114,5 +114,10 @@ abstract class ZIndexes {
static const score = _above + spaceshipRampForegroundRailing;
// Debug information
static const debugInfo = _above + score;
// Backbox
static const backbox = _below + outerBoundary;
}

@ -73,7 +73,6 @@ flutter:
- assets/images/sparky/bumper/a/
- assets/images/sparky/bumper/b/
- assets/images/sparky/bumper/c/
- assets/images/backboard/
- assets/images/google_word/letter1/
- assets/images/google_word/letter2/
- assets/images/google_word/letter3/
@ -88,6 +87,7 @@ flutter:
- assets/images/multiplier/x5/
- assets/images/multiplier/x6/
- assets/images/score/
- assets/images/backbox/
- assets/images/flapper/
flutter_gen:

@ -18,7 +18,6 @@ void main() {
addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook);
addScoreStories(dashbook);
addBackboardStories(dashbook);
addMultiballStories(dashbook);
addMultipliersStories(dashbook);

@ -1,63 +0,0 @@
import 'package:flame/effects.dart';
import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_theme/pinball_theme.dart';
import 'package:sandbox/common/common.dart';
class BackboardGameOverGame extends AssetsGame
with HasKeyboardHandlerComponents {
BackboardGameOverGame(this.score, this.character)
: super(
imagesFileNames: [
components.Assets.images.score.fiveThousand.keyName,
components.Assets.images.score.twentyThousand.keyName,
components.Assets.images.score.twoHundredThousand.keyName,
components.Assets.images.score.oneMillion.keyName,
...characterIconPaths.values.toList(),
],
);
static const description = '''
Shows how the Backboard in game over mode is rendered.
- Select a character to update the character icon.
''';
static final characterIconPaths = <String, String>{
'Dash': Assets.images.dash.leaderboardIcon.keyName,
'Sparky': Assets.images.sparky.leaderboardIcon.keyName,
'Android': Assets.images.android.leaderboardIcon.keyName,
'Dino': Assets.images.dino.leaderboardIcon.keyName,
};
final int score;
final String character;
@override
Future<void> onLoad() async {
await super.onLoad();
camera
..followVector2(Vector2.zero())
..zoom = 5;
await add(
components.Backboard.gameOver(
position: Vector2(0, 20),
score: score,
characterIconPath: characterIconPaths[character]!,
onSubmit: (initials) {
add(
components.ScoreComponent(
points: components.Points.values
.firstWhere((element) => element.value == score),
position: Vector2(0, 50),
effectController: EffectController(duration: 1),
),
);
},
),
);
}
}

@ -1,25 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BackboardWaitingGame extends AssetsGame {
BackboardWaitingGame()
: super(
imagesFileNames: [],
);
static const description = '''
Shows how the Backboard in waiting mode is rendered.
''';
@override
Future<void> onLoad() async {
camera
..followVector2(Vector2.zero())
..zoom = 5;
await add(
Backboard.waiting(position: Vector2(0, 20)),
);
}
}

@ -1,30 +0,0 @@
import 'package:dashbook/dashbook.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/backboard/backboard_game_over_game.dart';
import 'package:sandbox/stories/backboard/backboard_waiting_game.dart';
void addBackboardStories(Dashbook dashbook) {
dashbook.storiesOf('Backboard')
..addGame(
title: 'Waiting',
description: BackboardWaitingGame.description,
gameBuilder: (_) => BackboardWaitingGame(),
)
..addGame(
title: 'Game over',
description: BackboardGameOverGame.description,
gameBuilder: (context) => BackboardGameOverGame(
context.listProperty(
'Score',
Points.values.first.value,
Points.values.map((score) => score.value).toList(),
),
context.listProperty(
'Character',
BackboardGameOverGame.characterIconPaths.keys.first,
BackboardGameOverGame.characterIconPaths.keys.toList(),
),
),
);
}

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

@ -1,185 +0,0 @@
// 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' hide Assets;
import 'package:pinball_theme/pinball_theme.dart';
import '../../helpers/helpers.dart';
void main() {
group('Backboard', () {
final characterIconPath = Assets.images.dash.leaderboardIcon.keyName;
final tester = FlameTester(() => KeyboardTestGame([characterIconPath]));
group('on waitingMode', () {
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(Backboard.waiting(position: Vector2(0, 15)));
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/backboard/waiting.png'),
);
},
);
});
group('on gameOverMode', () {
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero());
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
characterIconPath: characterIconPath,
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,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(backboard);
// 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 {
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,
characterIconPath: characterIconPath,
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);
},
);
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('ScoreX', () {
test('formatScore correctly formats int', () {
expect(1000000.formatScore(), '1,000,000');
});
});
}

@ -6,6 +6,7 @@ abstract class PinballColors {
static const Color darkBlue = Color(0xFF0C32A4);
static const Color yellow = Color(0xFFFFEE02);
static const Color orange = Color(0xFFE5AB05);
static const Color red = Color(0xFFF03939);
static const Color blue = Color(0xFF4B94F6);
static const Color transparent = Color(0x00000000);
static const Color loadingDarkRed = Color(0xFFE33B2D);

@ -20,6 +20,10 @@ void main() {
expect(PinballColors.orange, const Color(0xFFE5AB05));
});
test('red is 0xFFF03939', () {
expect(PinballColors.red, const Color(0xFFF03939));
});
test('blue is 0xFF4B94F6', () {
expect(PinballColors.blue, const Color(0xFF4B94F6));
});

@ -0,0 +1,98 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/backbox/displays/initials_input_display.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../helpers/helpers.dart';
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get score => '';
@override
String get name => '';
@override
String get enterInitials => '';
@override
String get arrows => '';
@override
String get andPress => '';
@override
String get enterReturn => '';
@override
String get toSubmit => '';
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final characterIconPath = theme.Assets.images.dash.leaderboardIcon.keyName;
final assets = [
characterIconPath,
Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(
assets: assets,
l10n: _MockAppLocalizations(),
),
);
group('Backbox', () {
flameTester.test(
'loads correctly',
(game) async {
final backbox = Backbox();
await game.ensureAdd(backbox);
expect(game.children, contains(backbox));
},
);
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
game.camera
..followVector2(Vector2(0, -130))
..zoom = 6;
await game.ensureAdd(Backbox());
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<EmptyPinballTestGame>(),
matchesGoldenFile('../golden/backbox.png'),
);
},
);
flameTester.test(
'initialsInput adds InitialsInputDisplay',
(game) async {
final backbox = Backbox();
await game.ensureAdd(backbox);
await backbox.initialsInput(
score: 0,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ready();
expect(backbox.firstChild<InitialsInputDisplay>(), isNotNull);
},
);
});
}

@ -0,0 +1,189 @@
// ignore_for_file: 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:mocktail/mocktail.dart';
import 'package:pinball/game/components/backbox/displays/initials_input_display.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get score => '';
@override
String get name => '';
@override
String get enterInitials => '';
@override
String get arrows => '';
@override
String get andPress => '';
@override
String get enterReturn => '';
@override
String get toSubmit => '';
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final characterIconPath = theme.Assets.images.dash.leaderboardIcon.keyName;
final assets = [
characterIconPath,
Assets.images.backbox.displayDivider.keyName,
];
final flameTester = FlameTester(
() => EmptyKeyboardPinballTestGame(
assets: assets,
l10n: _MockAppLocalizations(),
),
);
group('InitialsInputDisplay', () {
flameTester.test(
'loads correctly',
(game) async {
final initialsInputDisplay = InitialsInputDisplay(
score: 0,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(initialsInputDisplay);
expect(game.children, contains(initialsInputDisplay));
},
);
flameTester.testGameWidget(
'can change the initials',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final initialsInputDisplay = InitialsInputDisplay(
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(initialsInputDisplay);
// 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 {
final initialsInputDisplay =
game.descendants().whereType<InitialsInputDisplay>().single;
expect(initialsInputDisplay.initials, equals('BCB'));
},
);
String? submitedInitials;
flameTester.testGameWidget(
'submits the initials',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final initialsInputDisplay = InitialsInputDisplay(
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (value) {
submitedInitials = value;
},
);
await game.ensureAdd(initialsInputDisplay);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
},
verify: (game, tester) async {
expect(submitedInitials, equals('AAA'));
},
);
group('BackboardLetterPrompt', () {
flameTester.testGameWidget(
'cycles the char up and down when it has focus',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
InitialsLetterPrompt(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<InitialsLetterPrompt>();
expect(prompt?.char, equals('C'));
},
);
flameTester.testGameWidget(
"does nothing when it doesn't have focus",
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
InitialsLetterPrompt(position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<InitialsLetterPrompt>();
expect(prompt?.char, equals('A'));
},
);
flameTester.testGameWidget(
'blinks the prompt when it has the focus',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
InitialsLetterPrompt(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);
},
);
});
});
}

@ -24,15 +24,18 @@ void main() {
test('correctly calculates the zooms', () async {
expect(controller.gameFocus.zoom.toInt(), equals(12));
expect(controller.backboardFocus.zoom.toInt(), equals(11));
expect(controller.waitingBackboxFocus.zoom.toInt(), equals(11));
});
test('correctly sets the initial zoom and position', () async {
expect(game.camera.zoom, equals(controller.backboardFocus.zoom));
expect(game.camera.follow, equals(controller.backboardFocus.position));
expect(game.camera.zoom, equals(controller.waitingBackboxFocus.zoom));
expect(
game.camera.follow,
equals(controller.waitingBackboxFocus.position),
);
});
group('focusOnBoard', () {
group('focusOnGame', () {
test('changes the zoom', () async {
controller.focusOnGame();
@ -53,22 +56,22 @@ void main() {
await future;
expect(game.camera.position, Vector2(-4, -108.8));
expect(game.camera.position, Vector2(-4, -120));
});
});
group('focusOnBackboard', () {
group('focusOnWaitingBackbox', () {
test('changes the zoom', () async {
controller.focusOnBackboard();
controller.focusOnWaitingBackbox();
await game.ready();
final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.backboardFocus.zoom));
expect(zoom?.value, equals(controller.waitingBackboxFocus.zoom));
});
test('moves the camera after the zoom is completed', () async {
controller.focusOnBackboard();
controller.focusOnWaitingBackbox();
await game.ready();
final cameraZoom = game.firstChild<CameraZoom>()!;
final future = cameraZoom.completed;
@ -78,7 +81,32 @@ void main() {
await future;
expect(game.camera.position, Vector2(-4.5, -109.8));
expect(game.camera.position, Vector2(-4.5, -121));
});
});
group('focusOnGameOverBackbox', () {
test('changes the zoom', () async {
controller.focusOnGameOverBackbox();
await game.ready();
final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.gameOverBackboxFocus.zoom));
});
test('moves the camera after the zoom is completed', () async {
controller.focusOnGameOverBackbox();
await game.ready();
final cameraZoom = game.firstChild<CameraZoom>()!;
final future = cameraZoom.completed;
game.update(10);
game.update(0); // Ensure that the component was removed
await future;
expect(game.camera.position, Vector2(-2.5, -117));
});
});
});

@ -5,12 +5,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart';
class _MockPinballGame extends Mock implements PinballGame {}
class _MockBackboard extends Mock implements Backboard {}
class _MockBackbox extends Mock implements Backbox {}
class _MockCameraController extends Mock implements CameraController {}
@ -40,7 +39,7 @@ void main() {
group('onNewState', () {
late PinballGame game;
late Backboard backboard;
late Backbox backbox;
late CameraController cameraController;
late GameFlowController gameFlowController;
late PinballAudio pinballAudio;
@ -48,26 +47,26 @@ void main() {
setUp(() {
game = _MockPinballGame();
backboard = _MockBackboard();
backbox = _MockBackbox();
cameraController = _MockCameraController();
gameFlowController = GameFlowController(game);
overlays = _MockActiveOverlaysNotifier();
pinballAudio = _MockPinballAudio();
when(
() => backboard.gameOverMode(
() => backbox.initialsInput(
score: any(named: 'score'),
characterIconPath: any(named: 'characterIconPath'),
onSubmit: any(named: 'onSubmit'),
),
).thenAnswer((_) async {});
when(backboard.waitingMode).thenAnswer((_) async {});
when(cameraController.focusOnBackboard).thenAnswer((_) async {});
when(cameraController.focusOnWaitingBackbox).thenAnswer((_) async {});
when(cameraController.focusOnGame).thenAnswer((_) async {});
when(() => overlays.remove(any())).thenAnswer((_) => true);
when(game.firstChild<Backboard>).thenReturn(backboard);
when(() => game.descendants().whereType<Backbox>())
.thenReturn([backbox]);
when(game.firstChild<CameraController>).thenReturn(cameraController);
when(() => game.overlays).thenReturn(overlays);
when(() => game.characterTheme).thenReturn(DashTheme());
@ -75,11 +74,12 @@ void main() {
});
test(
'changes the backboard and camera correctly when it is a game over',
'changes the backbox display and camera correctly '
'when the game is over',
() {
gameFlowController.onNewState(
GameState(
score: 10,
score: 0,
multiplier: 1,
rounds: 0,
bonusHistory: const [],
@ -87,22 +87,21 @@ void main() {
);
verify(
() => backboard.gameOverMode(
() => backbox.initialsInput(
score: 0,
characterIconPath: any(named: 'characterIconPath'),
onSubmit: any(named: 'onSubmit'),
),
).called(1);
verify(cameraController.focusOnBackboard).called(1);
verify(cameraController.focusOnGameOverBackbox).called(1);
},
);
test(
'changes the backboard and camera correctly when it is not a game over',
'changes the backbox and camera correctly when it is not a game over',
() {
gameFlowController.onNewState(GameState.initial());
verify(backboard.waitingMode).called(1);
verify(cameraController.focusOnGame).called(1);
verify(() => overlays.remove(PinballGame.playButtonOverlay))
.called(1);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@ -40,9 +40,8 @@ void main() {
Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName,
Assets.images.backboard.backboardScores.keyName,
Assets.images.backboard.backboardGameOver.keyName,
Assets.images.backboard.display.keyName,
Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName,
Assets.images.boardBackground.keyName,
Assets.images.ball.ball.keyName,
Assets.images.ball.flameEffect.keyName,

@ -2,15 +2,19 @@
import 'dart:async';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_theme/pinball_theme.dart';
class _MockPinballAudio extends Mock implements PinballAudio {}
class _MockAppLocalizations extends Mock implements AppLocalizations {}
class TestGame extends Forge2DGame with FlameBloc {
TestGame() {
images.prefix = '';
@ -22,10 +26,12 @@ class PinballTestGame extends PinballGame {
List<String>? assets,
PinballAudio? audio,
CharacterTheme? theme,
AppLocalizations? l10n,
}) : _assets = assets,
super(
audio: audio ?? _MockPinballAudio(),
characterTheme: theme ?? const DashTheme(),
l10n: l10n ?? _MockAppLocalizations(),
);
final List<String>? _assets;
@ -43,10 +49,12 @@ class DebugPinballTestGame extends DebugPinballGame {
List<String>? assets,
PinballAudio? audio,
CharacterTheme? theme,
AppLocalizations? l10n,
}) : _assets = assets,
super(
audio: audio ?? _MockPinballAudio(),
characterTheme: theme ?? const DashTheme(),
l10n: l10n ?? _MockAppLocalizations(),
);
final List<String>? _assets;
@ -65,10 +73,34 @@ class EmptyPinballTestGame extends PinballTestGame {
List<String>? assets,
PinballAudio? audio,
CharacterTheme? theme,
AppLocalizations? l10n,
}) : super(
assets: assets,
audio: audio,
theme: theme,
l10n: l10n ?? _MockAppLocalizations(),
);
@override
Future<void> onLoad() async {
if (_assets != null) {
await images.loadAll(_assets!);
}
}
}
class EmptyKeyboardPinballTestGame extends PinballTestGame
with HasKeyboardHandlerComponents {
EmptyKeyboardPinballTestGame({
List<String>? assets,
PinballAudio? audio,
CharacterTheme? theme,
AppLocalizations? l10n,
}) : super(
assets: assets,
audio: audio,
theme: theme,
l10n: l10n ?? _MockAppLocalizations(),
);
@override

Loading…
Cancel
Save