refactor: clean up backboard

pull/319/head
Allison Ryan 3 years ago
parent 07ea110ec3
commit 83212d91ba

@ -0,0 +1,54 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:pinball/game/components/backboard/displays/displays.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template backboard}
/// The [Backboard] of the pinball machine.
/// {@endtemplate}
class Backboard extends PositionComponent with HasGameRef {
/// {@macro backboard}
Backboard()
: super(
position: Vector2(0, -87),
anchor: Anchor.bottomCenter,
priority: RenderPriority.backboardMarquee,
children: [
_BackboardSpriteComponent(),
],
);
/// Puts [InitialsInputDisplay] on the [Backboard].
Future<void> initialsInput({
required int score,
required String characterIconPath,
InitialsOnSubmit? onSubmit,
}) async {
removeAll(children);
await add(
InitialsInputDisplay(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
),
);
}
}
class _BackboardSpriteComponent extends SpriteComponent with HasGameRef {
_BackboardSpriteComponent() : super(anchor: Anchor.bottomCenter);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.backboard.marquee.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

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

@ -0,0 +1,376 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball/l10n/l10n.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(
_BackboardLetterPrompt(
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<_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 _ScoreLabelTextComponent extends TextComponent with HasGameRef {
_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.buildContext!.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 {
_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.buildContext!.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;
}
}
class _BackboardLetterPrompt extends PositionComponent {
_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(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.backboard.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 {
_EnterInitialsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.buildContext!.l10n.enterInitials;
}
}
class _ArrowsTextComponent extends TextComponent with HasGameRef {
_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.buildContext!.l10n.arrows;
}
}
class _AndPressTextComponent extends TextComponent with HasGameRef {
_AndPressTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-3.7, 0),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.buildContext!.l10n.andPress;
}
}
class _EnterReturnTextComponent extends TextComponent with HasGameRef {
_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.buildContext!.l10n.enterReturn;
}
}
class _ToSubmitTextComponent extends TextComponent with HasGameRef {
_ToSubmitTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.buildContext!.l10n.toSubmit;
}
}

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

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

@ -20,26 +20,25 @@ class GameFlowController extends ComponentController<PinballGame>
@override @override
void onNewState(GameState state) { void onNewState(GameState state) {
if (state.isGameOver) { if (state.isGameOver) {
gameOver(); initialsInput();
} else { } else {
start(); start();
} }
} }
/// Puts the game on a game over state /// Puts the game in the initials input state.
void gameOver() { void initialsInput() {
// TODO(erickzanardo): implement score submission and "navigate" to the // TODO(erickzanardo): implement score submission and "navigate" to the
// next page // next page
component.firstChild<Backboard>()?.gameOverMode( component.firstChild<Backboard>()?.initialsInput(
score: state?.score ?? 0, score: state?.score ?? 0,
characterIconPath: component.characterTheme.leaderboardIcon.keyName, characterIconPath: component.characterTheme.leaderboardIcon.keyName,
); );
component.firstChild<CameraController>()?.focusOnBackboard(); component.firstChild<CameraController>()?.focusOnGameOverBackboard();
} }
/// Puts the game on a playing state /// Puts the game in the playing state.
void start() { void start() {
component.firstChild<Backboard>()?.waitingMode();
component.firstChild<CameraController>()?.focusOnGame(); component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay); component.overlays.remove(PinballGame.playButtonOverlay);
} }

@ -96,15 +96,14 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.sparky.bumper.b.inactive.keyName), images.load(components.Assets.images.sparky.bumper.b.inactive.keyName),
images.load(components.Assets.images.sparky.bumper.c.active.keyName), images.load(components.Assets.images.sparky.bumper.c.active.keyName),
images.load(components.Assets.images.sparky.bumper.c.inactive.keyName), images.load(components.Assets.images.sparky.bumper.c.inactive.keyName),
images.load(components.Assets.images.backboard.backboardScores.keyName), images.load(components.Assets.images.backboard.marquee.keyName),
images.load(components.Assets.images.backboard.backboardGameOver.keyName), images.load(components.Assets.images.backboard.displayDivider.keyName),
images.load(components.Assets.images.googleWord.letter1.keyName), images.load(components.Assets.images.googleWord.letter1.keyName),
images.load(components.Assets.images.googleWord.letter2.keyName), images.load(components.Assets.images.googleWord.letter2.keyName),
images.load(components.Assets.images.googleWord.letter3.keyName), images.load(components.Assets.images.googleWord.letter3.keyName),
images.load(components.Assets.images.googleWord.letter4.keyName), images.load(components.Assets.images.googleWord.letter4.keyName),
images.load(components.Assets.images.googleWord.letter5.keyName), images.load(components.Assets.images.googleWord.letter5.keyName),
images.load(components.Assets.images.googleWord.letter6.keyName), images.load(components.Assets.images.googleWord.letter6.keyName),
images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName),
images.load(components.Assets.images.multiplier.x2.dimmed.keyName), images.load(components.Assets.images.multiplier.x2.dimmed.keyName),
images.load(components.Assets.images.multiplier.x3.lit.keyName), images.load(components.Assets.images.multiplier.x3.lit.keyName),

@ -44,7 +44,7 @@ class PinballGame extends Forge2DGame
Future<void> onLoad() async { Future<void> onLoad() async {
unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this))); unawaited(add(CameraController(this)));
unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); await add(Backboard());
await add(BoardBackgroundSpriteComponent()); await add(BoardBackgroundSpriteComponent());
await add(Drain()); await add(Drain());
await add(BottomGroup()); await add(BottomGroup());

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

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 KiB

@ -49,17 +49,13 @@ class $AssetsImagesAndroidGen {
class $AssetsImagesBackboardGen { class $AssetsImagesBackboardGen {
const $AssetsImagesBackboardGen(); const $AssetsImagesBackboardGen();
/// File path: assets/images/backboard/backboard_game_over.png /// File path: assets/images/backboard/display-divider.png
AssetGenImage get backboardGameOver => AssetGenImage get displayDivider =>
const AssetGenImage('assets/images/backboard/backboard_game_over.png'); const AssetGenImage('assets/images/backboard/display-divider.png');
/// File path: assets/images/backboard/backboard_scores.png /// File path: assets/images/backboard/marquee.png
AssetGenImage get backboardScores => AssetGenImage get marquee =>
const AssetGenImage('assets/images/backboard/backboard_scores.png'); const AssetGenImage('assets/images/backboard/marquee.png');
/// File path: assets/images/backboard/display.png
AssetGenImage get display =>
const AssetGenImage('assets/images/backboard/display.png');
} }
class $AssetsImagesBallGen { 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,6 +1,5 @@
export 'android_bumper/android_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'android_spaceship.dart'; export 'android_spaceship.dart';
export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board_background_sprite_component.dart'; export 'board_background_sprite_component.dart';

@ -113,5 +113,10 @@ abstract class RenderPriority {
static const int scoreText = _above + spaceshipRampForegroundRailing; static const int scoreText = _above + spaceshipRampForegroundRailing;
// Debug information // Debug information
static const int debugInfo = _above + scoreText; static const int debugInfo = _above + scoreText;
// Backboard
static const int backboardMarquee = _below + outerBoundary;
} }

@ -25,7 +25,6 @@ void main() {
addGoogleWordStories(dashbook); addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook); addLaunchRampStories(dashbook);
addScoreTextStories(dashbook); addScoreTextStories(dashbook);
addBackboardStories(dashbook);
addDinoWallStories(dashbook); addDinoWallStories(dashbook);
addMultipliersStories(dashbook); addMultipliersStories(dashbook);

@ -1,54 +0,0 @@
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
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: 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 {
camera
..followVector2(Vector2.zero())
..zoom = 5;
await add(
Backboard.gameOver(
position: Vector2(0, 20),
score: score,
characterIconPath: characterIconPaths[character]!,
onSubmit: (initials) {
add(
ScoreText(
text: 'User $initials made $score',
position: Vector2(0, 50),
color: Colors.pink,
),
);
},
),
);
}
}

@ -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,25 +0,0 @@
import 'package:dashbook/dashbook.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.numberProperty('Score', 9000000000).toInt(),
context.listProperty(
'Character',
BackboardGameOverGame.characterIconPaths.keys.first,
BackboardGameOverGame.characterIconPaths.keys.toList(),
),
),
);
}

@ -1,5 +1,4 @@
export 'android_acres/stories.dart'; export 'android_acres/stories.dart';
export 'backboard/stories.dart';
export 'ball/stories.dart'; export 'ball/stories.dart';
export 'bottom_group/stories.dart'; export 'bottom_group/stories.dart';
export 'boundaries/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);
},
);
});
}

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

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

Loading…
Cancel
Save