diff --git a/lib/game/components/backboard/backboard.dart b/lib/game/components/backboard/backboard.dart new file mode 100644 index 00000000..4c61e28c --- /dev/null +++ b/lib/game/components/backboard/backboard.dart @@ -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 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 onLoad() async { + await super.onLoad(); + + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.backboard.marquee.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} diff --git a/lib/game/components/backboard/displays/displays.dart b/lib/game/components/backboard/displays/displays.dart new file mode 100644 index 00000000..194212ab --- /dev/null +++ b/lib/game/components/backboard/displays/displays.dart @@ -0,0 +1 @@ +export 'initials_input_display.dart'; diff --git a/lib/game/components/backboard/displays/initials_input_display.dart b/lib/game/components/backboard/displays/initials_input_display.dart new file mode 100644 index 00000000..89b31150 --- /dev/null +++ b/lib/game/components/backboard/displays/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 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 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 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 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 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 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 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 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 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 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 onLoad() async { + await super.onLoad(); + text = gameRef.buildContext!.l10n.toSubmit; + } +} diff --git a/lib/game/components/camera_controller.dart b/lib/game/components/camera_controller.dart index a411942e..77ab9c6a 100644 --- a/lib/game/components/camera_controller.dart +++ b/lib/game/components/camera_controller.dart @@ -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 { /// {@macro camera_controller} CameraController(FlameGame component) : super(component) { 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( zoom: gameZoom, position: Vector2(0, -7.8), ); - backboardFocus = FocusData( - zoom: backboardZoom, - position: Vector2(0, -100.8), + waitingBackboardFocus = FocusData( + zoom: waitingBackboardZoom, + 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 - ..speed = 100 - ..snapToFocus(backboardFocus); + ..speed = 70 + ..snapToFocus(waitingBackboardFocus); } - /// 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 backboard focus point. + 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() { 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 backboard. + void focusOnWaitingBackboard() { + component.add(component.camera.focusToCameraZoom(waitingBackboardFocus)); + } + + /// Move the camera focus to the game over backboard. + void focusOnGameOverBackboard() { + component.add(component.camera.focusToCameraZoom(gameOverBackboardFocus)); } } diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index afef04f0..7183f88c 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,4 +1,5 @@ export 'android_acres.dart'; +export 'backboard/backboard.dart'; export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index 48dd5518..f99715cf 100644 --- a/lib/game/components/game_flow_controller.dart +++ b/lib/game/components/game_flow_controller.dart @@ -20,26 +20,25 @@ class GameFlowController extends ComponentController @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()?.gameOverMode( + component.firstChild()?.initialsInput( score: state?.score ?? 0, characterIconPath: component.characterTheme.leaderboardIcon.keyName, ); - component.firstChild()?.focusOnBackboard(); + component.firstChild()?.focusOnGameOverBackboard(); } - /// Puts the game on a playing state + /// Puts the game in the playing state. void start() { - component.firstChild()?.waitingMode(); component.firstChild()?.focusOnGame(); component.overlays.remove(PinballGame.playButtonOverlay); } diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 84b206d9..df6cfaca 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -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.c.active.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.backboardGameOver.keyName), + images.load(components.Assets.images.backboard.marquee.keyName), + images.load(components.Assets.images.backboard.displayDivider.keyName), images.load(components.Assets.images.googleWord.letter1.keyName), images.load(components.Assets.images.googleWord.letter2.keyName), images.load(components.Assets.images.googleWord.letter3.keyName), images.load(components.Assets.images.googleWord.letter4.keyName), images.load(components.Assets.images.googleWord.letter5.keyName), images.load(components.Assets.images.googleWord.letter6.keyName), - images.load(components.Assets.images.backboard.display.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.multiplier.x2.dimmed.keyName), images.load(components.Assets.images.multiplier.x3.lit.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index a374ba6e..35986b71 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -44,7 +44,7 @@ class PinballGame extends Forge2DGame Future onLoad() async { unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(CameraController(this))); - unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); + await add(Backboard()); await add(BoardBackgroundSpriteComponent()); await add(Drain()); await add(BottomGroup()); diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7691e2dd..e1d47b1c 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -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": { diff --git a/packages/pinball_components/assets/images/backboard/backboard_game_over.png b/packages/pinball_components/assets/images/backboard/backboard_game_over.png deleted file mode 100644 index 70bd4544..00000000 Binary files a/packages/pinball_components/assets/images/backboard/backboard_game_over.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/backboard/backboard_scores.png b/packages/pinball_components/assets/images/backboard/backboard_scores.png deleted file mode 100644 index dab850d2..00000000 Binary files a/packages/pinball_components/assets/images/backboard/backboard_scores.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/backboard/display-divider.png b/packages/pinball_components/assets/images/backboard/display-divider.png new file mode 100644 index 00000000..c7be2066 Binary files /dev/null and b/packages/pinball_components/assets/images/backboard/display-divider.png differ diff --git a/packages/pinball_components/assets/images/backboard/display.png b/packages/pinball_components/assets/images/backboard/display.png deleted file mode 100644 index 97dbb50b..00000000 Binary files a/packages/pinball_components/assets/images/backboard/display.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/backboard/marquee.png b/packages/pinball_components/assets/images/backboard/marquee.png new file mode 100644 index 00000000..bdb95b02 Binary files /dev/null and b/packages/pinball_components/assets/images/backboard/marquee.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 0a67751b..cf0b2085 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -49,17 +49,13 @@ class $AssetsImagesAndroidGen { class $AssetsImagesBackboardGen { const $AssetsImagesBackboardGen(); - /// 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/backboard/display-divider.png + AssetGenImage get displayDivider => + const AssetGenImage('assets/images/backboard/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/backboard/marquee.png + AssetGenImage get marquee => + const AssetGenImage('assets/images/backboard/marquee.png'); } class $AssetsImagesBallGen { diff --git a/packages/pinball_components/lib/src/components/backboard/backboard.dart b/packages/pinball_components/lib/src/components/backboard/backboard.dart deleted file mode 100644 index fe5fd37c..00000000 --- a/packages/pinball_components/lib/src/components/backboard/backboard.dart +++ /dev/null @@ -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 waitingMode() async { - children.removeWhere((_) => true); - await add(BackboardWaiting()); - } - - /// Puts the Backboard in game over mode, where the score input is shown. - Future gameOverMode({ - required int score, - required String characterIconPath, - BackboardOnSubmit? onSubmit, - }) async { - children.removeWhere((_) => true); - await add( - BackboardGameOver( - score: score, - characterIconPath: characterIconPath, - onSubmit: onSubmit, - ), - ); - } -} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart b/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart deleted file mode 100644 index cfea0bc6..00000000 --- a/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart +++ /dev/null @@ -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 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() - .map((prompt) => prompt.char) - .join(); - - bool _submit() { - _onSubmit?.call(initials); - return true; - } - - bool _movePrompt(bool left) { - final prompts = children.whereType().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 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 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 onLoad() async { - await super.onLoad(); - final sprite = Sprite(gameRef.images.fromCache(_characterIconPath)); - this.sprite = sprite; - size = sprite.originalSize / 10; - } -} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart b/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart deleted file mode 100644 index fe582210..00000000 --- a/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart +++ /dev/null @@ -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 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; - } -} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart b/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart deleted file mode 100644 index f7fa84bf..00000000 --- a/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart +++ /dev/null @@ -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 onLoad() async { - final sprite = await gameRef.loadSprite( - Assets.images.backboard.backboardScores.keyName, - ); - - this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.bottomCenter; - } -} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 2781030e..f107e53e 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,6 +1,5 @@ export 'android_bumper/android_bumper.dart'; export 'android_spaceship.dart'; -export 'backboard/backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; export 'board_background_sprite_component.dart'; diff --git a/packages/pinball_components/lib/src/components/render_priority.dart b/packages/pinball_components/lib/src/components/render_priority.dart index 2359229f..d2d08628 100644 --- a/packages/pinball_components/lib/src/components/render_priority.dart +++ b/packages/pinball_components/lib/src/components/render_priority.dart @@ -113,5 +113,10 @@ abstract class RenderPriority { static const int scoreText = _above + spaceshipRampForegroundRailing; // Debug information + static const int debugInfo = _above + scoreText; + + // Backboard + + static const int backboardMarquee = _below + outerBoundary; } diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 1f0077fb..d6b4c8aa 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -25,7 +25,6 @@ void main() { addGoogleWordStories(dashbook); addLaunchRampStories(dashbook); addScoreTextStories(dashbook); - addBackboardStories(dashbook); addDinoWallStories(dashbook); addMultipliersStories(dashbook); diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart deleted file mode 100644 index 639a4b57..00000000 --- a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart +++ /dev/null @@ -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 = { - '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 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, - ), - ); - }, - ), - ); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart deleted file mode 100644 index 6da9206c..00000000 --- a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart +++ /dev/null @@ -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 onLoad() async { - camera - ..followVector2(Vector2.zero()) - ..zoom = 5; - - await add( - Backboard.waiting(position: Vector2(0, 20)), - ); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart deleted file mode 100644 index b8c85d10..00000000 --- a/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart +++ /dev/null @@ -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(), - ), - ), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index d5e410b4..e86a6f2a 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -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'; diff --git a/packages/pinball_components/test/src/components/backboard_test.dart b/packages/pinball_components/test/src/components/backboard_test.dart deleted file mode 100644 index aee2481a..00000000 --- a/packages/pinball_components/test/src/components/backboard_test.dart +++ /dev/null @@ -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(), - 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().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(); - 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(); - 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().first; - expect(underscore.paint.color, Colors.white); - - game.update(2); - expect(underscore.paint.color, Colors.transparent); - }, - ); - }); -} diff --git a/packages/pinball_ui/lib/src/theme/pinball_colors.dart b/packages/pinball_ui/lib/src/theme/pinball_colors.dart index 5db27229..7018ee3c 100644 --- a/packages/pinball_ui/lib/src/theme/pinball_colors.dart +++ b/packages/pinball_ui/lib/src/theme/pinball_colors.dart @@ -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); } diff --git a/packages/pinball_ui/test/src/theme/pinball_colors_test.dart b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart index 36e45c0d..7e6bc4e0 100644 --- a/packages/pinball_ui/test/src/theme/pinball_colors_test.dart +++ b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart @@ -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)); });