diff --git a/lib/game/components/controlled_flipper.dart b/lib/game/components/controlled_flipper.dart index 9b73b6d3..f1991fe1 100644 --- a/lib/game/components/controlled_flipper.dart +++ b/lib/game/components/controlled_flipper.dart @@ -1,6 +1,8 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/services.dart'; import 'package:pinball/flame/flame.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template controlled_flipper} @@ -19,7 +21,7 @@ class ControlledFlipper extends Flipper with Controls { /// A [ComponentController] that controls a [Flipper]s movement. /// {@endtemplate} class FlipperController extends ComponentController - with KeyboardHandler { + with KeyboardHandler, BlocComponent { /// {@macro flipper_controller} FlipperController(Flipper flipper) : _keys = flipper.side.flipperKeys, @@ -35,6 +37,7 @@ class FlipperController extends ComponentController RawKeyEvent event, Set keysPressed, ) { + if (state?.isGameOver ?? false) return true; if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { diff --git a/lib/game/components/controlled_plunger.dart b/lib/game/components/controlled_plunger.dart index 167f129e..cec71876 100644 --- a/lib/game/components/controlled_plunger.dart +++ b/lib/game/components/controlled_plunger.dart @@ -1,6 +1,8 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/services.dart'; import 'package:pinball/flame/flame.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template controlled_plunger} @@ -18,7 +20,7 @@ class ControlledPlunger extends Plunger with Controls { /// A [ComponentController] that controls a [Plunger]s movement. /// {@endtemplate} class PlungerController extends ComponentController - with KeyboardHandler { + with KeyboardHandler, BlocComponent { /// {@macro plunger_controller} PlungerController(Plunger plunger) : super(plunger); @@ -36,6 +38,7 @@ class PlungerController extends ComponentController RawKeyEvent event, Set keysPressed, ) { + if (state?.isGameOver ?? false) return true; if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index b0f6f514..01ee0c32 100644 --- a/lib/game/components/game_flow_controller.dart +++ b/lib/game/components/game_flow_controller.dart @@ -28,7 +28,11 @@ class GameFlowController extends ComponentController /// Puts the game on a game over state void gameOver() { - component.firstChild()?.gameOverMode(); + // TODO(erickzanardo): implement score submission and "navigate" to the + // next page + component.firstChild()?.gameOverMode( + score: state?.score ?? 0, + ); component.firstChild()?.focusOnBackboard(); } diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 4d06bd13..4cc8bee5 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -58,6 +58,7 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.sparky.bumper.c.inactive.keyName), images.load(components.Assets.images.backboard.backboardScores.keyName), images.load(components.Assets.images.backboard.backboardGameOver.keyName), + images.load(components.Assets.images.backboard.display.keyName), images.load(Assets.images.components.background.path), ]; } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 0dd1b52e..07d637c0 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -41,7 +41,7 @@ class PinballGame extends Forge2DGame unawaited(add(ScoreEffectController(this))); unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(CameraController(this))); - unawaited(add(Backboard(position: Vector2(0, -88)))); + unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); await _addGameBoundaries(); unawaited(addFromBlueprint(Boundaries())); diff --git a/packages/pinball_components/assets/images/backboard/display.png b/packages/pinball_components/assets/images/backboard/display.png new file mode 100644 index 00000000..97dbb50b Binary files /dev/null and b/packages/pinball_components/assets/images/backboard/display.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 35a998a4..f45543b0 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -57,6 +57,8 @@ class $AssetsImagesBackboardGen { /// File path: assets/images/backboard/backboard_scores.png AssetGenImage get backboardScores => const AssetGenImage('assets/images/backboard/backboard_scores.png'); + AssetGenImage get display => + const AssetGenImage('assets/images/backboard/display.png'); } class $AssetsImagesBaseboardGen { @@ -307,11 +309,8 @@ class $AssetsImagesSparkyBumperGen { class $AssetsImagesSparkyComputerGen { const $AssetsImagesSparkyComputerGen(); - /// File path: assets/images/sparky/computer/base.png AssetGenImage get base => const AssetGenImage('assets/images/sparky/computer/base.png'); - - /// File path: assets/images/sparky/computer/top.png AssetGenImage get top => const AssetGenImage('assets/images/sparky/computer/top.png'); } @@ -355,11 +354,8 @@ class $AssetsImagesDashBumperMainGen { class $AssetsImagesSparkyBumperAGen { const $AssetsImagesSparkyBumperAGen(); - /// File path: assets/images/sparky/bumper/a/active.png AssetGenImage get active => const AssetGenImage('assets/images/sparky/bumper/a/active.png'); - - /// File path: assets/images/sparky/bumper/a/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/sparky/bumper/a/inactive.png'); } @@ -367,11 +363,8 @@ class $AssetsImagesSparkyBumperAGen { class $AssetsImagesSparkyBumperBGen { const $AssetsImagesSparkyBumperBGen(); - /// File path: assets/images/sparky/bumper/b/active.png AssetGenImage get active => const AssetGenImage('assets/images/sparky/bumper/b/active.png'); - - /// File path: assets/images/sparky/bumper/b/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/sparky/bumper/b/inactive.png'); } @@ -379,11 +372,8 @@ class $AssetsImagesSparkyBumperBGen { class $AssetsImagesSparkyBumperCGen { const $AssetsImagesSparkyBumperCGen(); - /// File path: assets/images/sparky/bumper/c/active.png AssetGenImage get active => const AssetGenImage('assets/images/sparky/bumper/c/active.png'); - - /// File path: assets/images/sparky/bumper/c/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/sparky/bumper/c/inactive.png'); } diff --git a/packages/pinball_components/lib/src/components/backboard.dart b/packages/pinball_components/lib/src/components/backboard.dart deleted file mode 100644 index 613cbc05..00000000 --- a/packages/pinball_components/lib/src/components/backboard.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template backboard} -/// The [Backboard] of the pinball machine. -/// {@endtemplate} -class Backboard extends SpriteComponent with HasGameRef { - /// {@macro backboard} - Backboard({ - required Vector2 position, - }) : super( - // TODO(erickzanardo): remove multiply after - // https://github.com/flame-engine/flame/pull/1506 is merged - position: position..clone().multiply(Vector2(1, -1)), - anchor: Anchor.bottomCenter, - ); - - @override - Future onLoad() async { - await waitingMode(); - } - - /// Puts the Backboard in waiting mode, where the scoreboard is shown. - Future waitingMode() async { - final sprite = await gameRef.loadSprite( - Assets.images.backboard.backboardScores.keyName, - ); - size = sprite.originalSize / 10; - this.sprite = sprite; - } - - /// Puts the Backboard in game over mode, where the score input is shown. - Future gameOverMode() async { - final sprite = await gameRef.loadSprite( - Assets.images.backboard.backboardGameOver.keyName, - ); - size = sprite.originalSize / 10; - this.sprite = sprite; - } -} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard.dart b/packages/pinball_components/lib/src/components/backboard/backboard.dart new file mode 100644 index 00000000..c5c4ac17 --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; + +export 'backboard_game_over.dart'; +export 'backboard_letter_prompt.dart'; +export 'backboard_waiting.dart'; + +/// {@template backboard} +/// The [Backboard] of the pinball machine. +/// {@endtemplate} +class Backboard extends PositionComponent with HasGameRef { + /// {@macro backboard} + Backboard({ + required Vector2 position, + }) : super( + position: position, + anchor: Anchor.bottomCenter, + ); + + /// {@macro backboard} + /// + /// Returns a [Backboard] initialized in the waiting mode + factory Backboard.waiting({ + required Vector2 position, + }) { + return Backboard(position: position)..waitingMode(); + } + + /// {@macro backboard} + /// + /// Returns a [Backboard] initialized in the game over mode + factory Backboard.gameOver({ + required Vector2 position, + required int score, + required BackboardOnSubmit onSubmit, + }) { + return Backboard(position: position) + ..gameOverMode( + score: score, + onSubmit: onSubmit, + ); + } + + /// [TextPaint] used on the [Backboard] + static final textPaint = TextPaint( + style: TextStyle( + fontSize: 6, + color: Colors.white, + fontFamily: PinballFonts.pixeloidSans, + ), + ); + + /// Puts the Backboard in waiting mode, where the scoreboard is shown. + Future 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, + BackboardOnSubmit? onSubmit, + }) async { + children.removeWhere((_) => true); + await add( + BackboardGameOver( + score: score, + 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 new file mode 100644 index 00000000..05f89217 --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart @@ -0,0 +1,119 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// Signature for the callback called when the used has +/// submettied their initials on the [BackboardGameOver] +typedef BackboardOnSubmit = void Function(String); + +/// {@template backboard_game_over} +/// [PositionComponent] that handles the user input on the +/// game over display view. +/// {@endtemplate} +class BackboardGameOver extends PositionComponent with HasGameRef { + /// {@macro backboard_game_over} + BackboardGameOver({ + required int score, + BackboardOnSubmit? onSubmit, + }) : _score = score, + _onSubmit = onSubmit; + + final int _score; + final BackboardOnSubmit? _onSubmit; + + @override + Future onLoad() async { + final backgroundSprite = await gameRef.loadSprite( + Assets.images.backboard.backboardGameOver.keyName, + ); + + unawaited( + add( + SpriteComponent( + sprite: backgroundSprite, + size: backgroundSprite.originalSize / 10, + anchor: Anchor.bottomCenter, + ), + ), + ); + + final displaySprite = await gameRef.loadSprite( + Assets.images.backboard.display.keyName, + ); + + unawaited( + add( + SpriteComponent( + sprite: displaySprite, + size: displaySprite.originalSize / 10, + anchor: Anchor.bottomCenter, + position: Vector2(0, -11.5), + ), + ), + ); + + unawaited( + add( + TextComponent( + text: _score.formatScore(), + position: Vector2(-22, -46.5), + anchor: Anchor.center, + textRenderer: Backboard.textPaint, + ), + ), + ); + + for (var i = 0; i < 3; i++) { + unawaited( + add( + BackboardLetterPrompt( + position: Vector2( + 20 + (6 * i).toDouble(), + -46.5, + ), + hasFocus: i == 0, + ), + ), + ); + } + + unawaited( + add( + KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowLeft: () => _movePrompt(true), + LogicalKeyboardKey.arrowRight: () => _movePrompt(false), + LogicalKeyboardKey.enter: _submit, + }, + ), + ), + ); + } + + /// Returns the current inputed initials + String get initials => children + .whereType() + .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; + } +} 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 new file mode 100644 index 00000000..8f404d53 --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart @@ -0,0 +1,106 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template backboard_letter_prompt} +/// A [PositionComponent] that renders a letter prompt used +/// on the [BackboardGameOver] +/// {@endtemplate} +class BackboardLetterPrompt extends PositionComponent { + /// {@macro backboard_letter_prompt} + BackboardLetterPrompt({ + required Vector2 position, + bool hasFocus = false, + }) : _hasFocus = hasFocus, + super( + position: position, + ); + + static const _alphabetCode = 65; + static const _alphabetLength = 25; + var _charIndex = 0; + + bool _hasFocus; + + late RectangleComponent _underscore; + late TextComponent _input; + late TimerComponent _underscoreBlinker; + + @override + Future onLoad() async { + _underscore = RectangleComponent( + size: Vector2( + 4, + 1.2, + ), + anchor: Anchor.center, + position: Vector2(0, 4), + ); + + unawaited(add(_underscore)); + + _input = TextComponent( + text: 'A', + textRenderer: Backboard.textPaint, + anchor: Anchor.center, + ); + unawaited(add(_input)); + + _underscoreBlinker = TimerComponent( + period: 0.6, + repeat: true, + autoStart: _hasFocus, + onTick: () { + _underscore.paint.color = (_underscore.paint.color == Colors.white) + ? Colors.transparent + : Colors.white; + }, + ); + + unawaited(add(_underscoreBlinker)); + + unawaited( + add( + KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowUp: () => _cycle(true), + LogicalKeyboardKey.arrowDown: () => _cycle(false), + }, + ), + ), + ); + } + + /// Returns the current selected character + String get char => String.fromCharCode(_alphabetCode + _charIndex); + + bool _cycle(bool up) { + if (_hasFocus) { + final newCharCode = + min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength); + _input.text = String.fromCharCode(_alphabetCode + newCharCode); + _charIndex = newCharCode; + + return false; + } + return true; + } + + /// Returns if this prompt has focus on it + bool get hasFocus => _hasFocus; + + /// Updates this prompt focus + set hasFocus(bool hasFocus) { + if (hasFocus) { + _underscoreBlinker.timer.resume(); + } else { + _underscoreBlinker.timer.pause(); + } + _underscore.paint.color = Colors.white; + _hasFocus = hasFocus; + } +} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart b/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart new file mode 100644 index 00000000..f7fa84bf --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart @@ -0,0 +1,17 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// [PositionComponent] that shows the leaderboard while the player +/// has not started the game yet. +class BackboardWaiting extends SpriteComponent with HasGameRef { + @override + Future 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 e719783f..7b4e1ddd 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,5 +1,5 @@ export 'alien_bumper.dart'; -export 'backboard.dart'; +export 'backboard/backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; export 'board_dimensions.dart'; diff --git a/packages/pinball_components/lib/src/extensions/extensions.dart b/packages/pinball_components/lib/src/extensions/extensions.dart new file mode 100644 index 00000000..4be86fd3 --- /dev/null +++ b/packages/pinball_components/lib/src/extensions/extensions.dart @@ -0,0 +1 @@ +export 'score.dart'; diff --git a/packages/pinball_components/lib/src/extensions/score.dart b/packages/pinball_components/lib/src/extensions/score.dart new file mode 100644 index 00000000..bd60d27e --- /dev/null +++ b/packages/pinball_components/lib/src/extensions/score.dart @@ -0,0 +1,11 @@ +import 'package:intl/intl.dart'; + +final _numberFormat = NumberFormat('#,###'); + +/// Adds score related extensions to int +extension ScoreX on int { + /// Formats this number as a score value + String formatScore() { + return _numberFormat.format(this); + } +} diff --git a/packages/pinball_components/lib/src/flame/flame.dart b/packages/pinball_components/lib/src/flame/flame.dart index 9af8dba6..9b766995 100644 --- a/packages/pinball_components/lib/src/flame/flame.dart +++ b/packages/pinball_components/lib/src/flame/flame.dart @@ -1,2 +1,3 @@ export 'blueprint.dart'; +export 'keyboard_input_controller.dart'; export 'priority.dart'; diff --git a/packages/pinball_components/lib/src/flame/keyboard_input_controller.dart b/packages/pinball_components/lib/src/flame/keyboard_input_controller.dart new file mode 100644 index 00000000..8249e599 --- /dev/null +++ b/packages/pinball_components/lib/src/flame/keyboard_input_controller.dart @@ -0,0 +1,34 @@ +import 'package:flame/components.dart'; +import 'package:flutter/services.dart'; + +/// The signature for a key handle function +typedef KeyHandlerCallback = bool Function(); + +/// {@template keyboard_input_controller} +/// A [Component] that receives keyboard input and executes registered methods. +/// {@endtemplate} +class KeyboardInputController extends Component with KeyboardHandler { + /// {@macro keyboard_input_controller} + KeyboardInputController({ + Map keyUp = const {}, + Map keyDown = const {}, + }) : _keyUp = keyUp, + _keyDown = keyDown; + + final Map _keyUp; + final Map _keyDown; + + @override + bool onKeyEvent(RawKeyEvent event, Set keysPressed) { + final isUp = event is RawKeyUpEvent; + + final handlers = isUp ? _keyUp : _keyDown; + final handler = handlers[event.logicalKey]; + + if (handler != null) { + return handler(); + } + + return true; + } +} diff --git a/packages/pinball_components/lib/src/pinball_components.dart b/packages/pinball_components/lib/src/pinball_components.dart index bd8f99de..50dee227 100644 --- a/packages/pinball_components/lib/src/pinball_components.dart +++ b/packages/pinball_components/lib/src/pinball_components.dart @@ -1,2 +1,3 @@ export 'components/components.dart'; +export 'extensions/extensions.dart'; export 'flame/flame.dart'; diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index cf2a22a2..d27084f1 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: sdk: flutter geometry: path: ../geometry + intl: ^0.17.0 dev_dependencies: diff --git a/packages/pinball_components/sandbox/lib/common/games.dart b/packages/pinball_components/sandbox/lib/common/games.dart index 4aae07cb..4fb158bd 100644 --- a/packages/pinball_components/sandbox/lib/common/games.dart +++ b/packages/pinball_components/sandbox/lib/common/games.dart @@ -11,6 +11,9 @@ abstract class BasicGame extends Forge2DGame { } } +abstract class BasicKeyboardGame extends BasicGame + with HasKeyboardHandlerComponents {} + abstract class LineGame extends BasicGame with PanDetector { Vector2? _lineEnd; diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 5cf36b3d..73ae296c 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -32,6 +32,7 @@ void main() { addGoogleWordStories(dashbook); addLaunchRampStories(dashbook); addScoreTextStories(dashbook); + addBackboardStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart b/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart new file mode 100644 index 00000000..a513276f --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart @@ -0,0 +1,37 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BackboardGameOverGame extends BasicKeyboardGame { + BackboardGameOverGame(this.score); + + static const info = ''' + Simple example showing the waiting mode of the backboard. + '''; + + final int score; + + @override + Future onLoad() async { + camera + ..followVector2(Vector2.zero()) + ..zoom = 5; + + await add( + Backboard.gameOver( + position: Vector2(0, 20), + score: score, + onSubmit: (initials) { + add( + ScoreText( + text: 'User $initials made $score', + position: Vector2(0, 50), + color: Colors.pink, + ), + ); + }, + ), + ); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart new file mode 100644 index 00000000..f85e6685 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart @@ -0,0 +1,27 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/backboard/game_over.dart'; +import 'package:sandbox/stories/backboard/waiting.dart'; + +void addBackboardStories(Dashbook dashbook) { + dashbook.storiesOf('Backboard') + ..add( + 'Waiting mode', + (context) => GameWidget( + game: BackboardWaitingGame(), + ), + codeLink: buildSourceLink('backboard/waiting.dart'), + info: BackboardWaitingGame.info, + ) + ..add( + 'Game over', + (context) => GameWidget( + game: BackboardGameOverGame( + context.numberProperty('score', 9000000000).toInt(), + ), + ), + codeLink: buildSourceLink('backboard/game_over.dart'), + info: BackboardGameOverGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/waiting.dart b/packages/pinball_components/sandbox/lib/stories/backboard/waiting.dart new file mode 100644 index 00000000..71f5c09a --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/backboard/waiting.dart @@ -0,0 +1,19 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class BackboardWaitingGame extends BasicGame { + static const info = ''' + Simple example showing the waiting mode of the backboard. + '''; + + @override + Future onLoad() async { + camera + ..followVector2(Vector2.zero()) + ..zoom = 5; + + final backboard = Backboard.waiting(position: Vector2(0, 20)); + await add(backboard); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index cdcf0825..338ca384 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,4 +1,5 @@ export 'alien_zone/stories.dart'; +export 'backboard/stories.dart'; export 'ball/stories.dart'; export 'baseboard/stories.dart'; export 'boundaries/stories.dart'; diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index 33553ba6..61af3a8a 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -149,6 +149,13 @@ packages: relative: true source: path version: "1.0.0+1" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" js: dependency: transitive description: diff --git a/packages/pinball_components/test/helpers/test_game.dart b/packages/pinball_components/test/helpers/test_game.dart index a1219868..5bd4b30d 100644 --- a/packages/pinball_components/test/helpers/test_game.dart +++ b/packages/pinball_components/test/helpers/test_game.dart @@ -1,3 +1,4 @@ +import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; class TestGame extends Forge2DGame { @@ -5,3 +6,5 @@ class TestGame extends Forge2DGame { images.prefix = ''; } } + +class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents {} diff --git a/packages/pinball_components/test/src/components/backboard_test.dart b/packages/pinball_components/test/src/components/backboard_test.dart index 2d95cc47..5e868bfc 100644 --- a/packages/pinball_components/test/src/components/backboard_test.dart +++ b/packages/pinball_components/test/src/components/backboard_test.dart @@ -1,7 +1,9 @@ -// ignore_for_file: unawaited_futures +// ignore_for_file: unawaited_futures, cascade_invocations import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -9,7 +11,7 @@ import '../../helpers/helpers.dart'; void main() { group('Backboard', () { - final tester = FlameTester(TestGame.new); + final tester = FlameTester(KeyboardTestGame.new); group('on waitingMode', () { tester.testGameWidget( @@ -17,7 +19,7 @@ void main() { setUp: (game, tester) async { game.camera.zoom = 2; game.camera.followVector2(Vector2.zero()); - await game.ensureAdd(Backboard(position: Vector2(0, 15))); + await game.ensureAdd(Backboard.waiting(position: Vector2(0, 15))); }, verify: (game, tester) async { await expectLater( @@ -34,20 +36,145 @@ void main() { setUp: (game, tester) async { game.camera.zoom = 2; game.camera.followVector2(Vector2.zero()); - final backboard = Backboard(position: Vector2(0, 15)); + final backboard = Backboard.gameOver( + position: Vector2(0, 15), + score: 1000, + onSubmit: (_) {}, + ); + await game.ensureAdd(backboard); + }, + verify: (game, tester) async { + final prompts = + game.descendants().whereType().length; + expect(prompts, equals(3)); + + final score = game.descendants().firstWhere( + (component) => + component is TextComponent && component.text == '1,000', + ); + + expect(score, isNotNull); + }, + ); + + tester.testGameWidget( + 'can change the initials', + setUp: (game, tester) async { + final backboard = Backboard.gameOver( + position: Vector2(0, 15), + score: 1000, + onSubmit: (_) {}, + ); await game.ensureAdd(backboard); - await backboard.gameOverMode(); - await game.ready(); + // Focus is already on the first letter + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + + // Move to the next an press up again + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + + // One more time + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + + // Back to the previous and increase one more + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pump(); }, verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('golden/backboard/game_over.png'), + final backboard = game + .descendants() + .firstWhere((component) => component is BackboardGameOver) + as BackboardGameOver; + + expect(backboard.initials, equals('BCB')); + }, + ); + + String? submitedInitials; + tester.testGameWidget( + 'submits the initials', + setUp: (game, tester) async { + final backboard = Backboard.gameOver( + position: Vector2(0, 15), + score: 1000, + onSubmit: (value) { + submitedInitials = value; + }, ); + await game.ensureAdd(backboard); + + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pump(); + }, + verify: (game, tester) async { + expect(submitedInitials, equals('AAA')); }, ); }); }); + + group('BackboardLetterPrompt', () { + final tester = FlameTester(KeyboardTestGame.new); + + tester.testGameWidget( + 'cycles the char up and down when it has focus', + setUp: (game, tester) async { + await game.ensureAdd( + BackboardLetterPrompt(hasFocus: true, position: Vector2.zero()), + ); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pump(); + }, + verify: (game, tester) async { + final prompt = game.firstChild(); + 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_components/test/src/components/golden/backboard/game_over.png b/packages/pinball_components/test/src/components/golden/backboard/game_over.png deleted file mode 100644 index 04a8e3ad..00000000 Binary files a/packages/pinball_components/test/src/components/golden/backboard/game_over.png and /dev/null differ diff --git a/packages/pinball_components/test/src/flame/keyboard_input_controller_test.dart b/packages/pinball_components/test/src/flame/keyboard_input_controller_test.dart new file mode 100644 index 00000000..991f1143 --- /dev/null +++ b/packages/pinball_components/test/src/flame/keyboard_input_controller_test.dart @@ -0,0 +1,78 @@ +// ignore_for_file: cascade_invocations, one_member_abstracts + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; + +abstract class _KeyCallStub { + bool onCall(); +} + +class KeyCallStub extends Mock implements _KeyCallStub {} + +class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) { + final event = MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn(key); + return event; +} + +void main() { + group('KeyboardInputController', () { + test('calls registered handlers', () { + final stub = KeyCallStub(); + when(stub.onCall).thenReturn(true); + + final input = KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowUp: stub.onCall, + }, + ); + + input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}); + verify(stub.onCall).called(1); + }); + + test( + 'returns false the handler return value', + () { + final stub = KeyCallStub(); + when(stub.onCall).thenReturn(false); + + final input = KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowUp: stub.onCall, + }, + ); + + expect( + input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}), + isFalse, + ); + }, + ); + + test( + 'returns true (allowing event to bubble) when no handler is registered', + () { + final stub = KeyCallStub(); + when(stub.onCall).thenReturn(true); + + final input = KeyboardInputController(); + + expect( + input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}), + isTrue, + ); + }, + ); + }); +} diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index 5446a672..a005de30 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:bloc_test/bloc_test.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,6 +13,22 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(EmptyPinballGameTest.new); + final gameOverBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballGameTest.new, + blocBuilder: () { + final bloc = MockGameBloc(); + const state = GameState( + score: 0, + balls: 0, + bonusHistory: [], + activatedBonusLetters: [], + activatedDashNests: {}, + ); + whenListen(bloc, Stream.value(state), initialState: state); + return bloc; + }, + ); + group('FlipperController', () { group('onKeyEvent', () { final leftKeys = UnmodifiableListView([ @@ -48,6 +65,20 @@ void main() { ); }); + testRawKeyDownEvents(leftKeys, (event) { + gameOverBlocTester.testGameWidget( + 'does nothing when is game over', + setUp: (game, tester) async { + await game.ensureAdd(flipper); + controller.onKeyEvent(event, {}); + }, + verify: (game, tester) async { + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + testRawKeyUpEvents(leftKeys, (event) { flameTester.test( 'moves downwards ' @@ -119,6 +150,20 @@ void main() { ); }); + testRawKeyDownEvents(rightKeys, (event) { + gameOverBlocTester.testGameWidget( + 'does nothing when is game over', + setUp: (game, tester) async { + await game.ensureAdd(flipper); + controller.onKeyEvent(event, {}); + }, + verify: (game, tester) async { + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + testRawKeyUpEvents(leftKeys, (event) { flameTester.test( 'does nothing ' diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index 02bf7f24..8a722c9b 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:bloc_test/bloc_test.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/services.dart'; @@ -13,6 +14,22 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(EmptyPinballGameTest.new); + final gameOverBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballGameTest.new, + blocBuilder: () { + final bloc = MockGameBloc(); + const state = GameState( + score: 0, + balls: 0, + bonusHistory: [], + activatedBonusLetters: [], + activatedDashNests: {}, + ); + whenListen(bloc, Stream.value(state), initialState: state); + return bloc; + }, + ); + group('PlungerController', () { group('onKeyEvent', () { final downKeys = UnmodifiableListView([ @@ -73,6 +90,20 @@ void main() { }, ); }); + + testRawKeyDownEvents(downKeys, (event) { + gameOverBlocTester.testGameWidget( + 'does nothing when is game over', + setUp: (game, tester) async { + await game.ensureAdd(plunger); + controller.onKeyEvent(event, {}); + }, + verify: (game, tester) async { + expect(plunger.body.linearVelocity.y, isZero); + expect(plunger.body.linearVelocity.x, isZero); + }, + ); + }); }); }); } diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index dc1d9ab8..42ce82d7 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -42,7 +42,12 @@ void main() { gameFlowController = GameFlowController(game); overlays = MockActiveOverlaysNotifier(); - when(backboard.gameOverMode).thenAnswer((_) async {}); + when( + () => backboard.gameOverMode( + score: any(named: 'score'), + onSubmit: any(named: 'onSubmit'), + ), + ).thenAnswer((_) async {}); when(backboard.waitingMode).thenAnswer((_) async {}); when(cameraController.focusOnBackboard).thenAnswer((_) async {}); when(cameraController.focusOnGame).thenAnswer((_) async {}); @@ -67,7 +72,12 @@ void main() { ), ); - verify(backboard.gameOverMode).called(1); + verify( + () => backboard.gameOverMode( + score: 0, + onSubmit: any(named: 'onSubmit'), + ), + ).called(1); verify(cameraController.focusOnBackboard).called(1); }, );