diff --git a/packages/pinball_components/lib/src/components/backboard/backboard.dart b/packages/pinball_components/lib/src/components/backboard/backboard.dart index f3903d82..5b6b73f8 100644 --- a/packages/pinball_components/lib/src/components/backboard/backboard.dart +++ b/packages/pinball_components/lib/src/components/backboard/backboard.dart @@ -37,8 +37,7 @@ class Backboard extends PositionComponent with HasGameRef { factory Backboard.waiting({ required Vector2 position, }) { - return Backboard(position: position) - ..waitingMode(); + return Backboard(position: position)..waitingMode(); } /// {@macro backboard} @@ -47,9 +46,13 @@ class Backboard extends PositionComponent with HasGameRef { factory Backboard.gameOver({ required Vector2 position, required int score, + required BackboardOnSubmit onSubmit, }) { return Backboard(position: position) - ..gameOverMode(score: score); + ..gameOverMode( + score: score, + onSubmit: onSubmit, + ); } /// Puts the Backboard in waiting mode, where the scoreboard is shown. @@ -59,8 +62,14 @@ class Backboard extends PositionComponent with HasGameRef { } /// Puts the Backboard in game over mode, where the score input is shown. - Future gameOverMode({ required int score}) async { + Future gameOverMode({ + required int score, + required BackboardOnSubmit onSubmit, + }) async { children.removeWhere((element) => true); - await add(BackboardGameOver(score: score)); + 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 index 10448395..d8aaf98e 100644 --- a/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart +++ b/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart @@ -2,11 +2,14 @@ import 'dart:async'; import 'dart:math'; import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.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. @@ -15,9 +18,12 @@ class BackboardGameOver extends PositionComponent with HasGameRef { /// {@macro backboard_game_over} BackboardGameOver({ required int score, - }) : _score = score; + required BackboardOnSubmit onSubmit, + }) : _score = score, + _onSubmit = onSubmit; final int _score; + final BackboardOnSubmit _onSubmit; final _numberFormat = NumberFormat('#,###,###'); @@ -83,12 +89,24 @@ class BackboardGameOver extends PositionComponent with HasGameRef { 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(initials); + return true; + } + bool _movePrompt(bool left) { final prompts = children.whereType().toList(); 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 index 554a27c4..8f404d53 100644 --- a/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart +++ b/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart @@ -75,6 +75,9 @@ class BackboardLetterPrompt extends PositionComponent { ); } + /// Returns the current selected character + String get char => String.fromCharCode(_alphabetCode + _charIndex); + bool _cycle(bool up) { if (_hasFocus) { final newCharCode = diff --git a/packages/pinball_components/lib/src/flame/keyboard_input_controller.dart b/packages/pinball_components/lib/src/flame/keyboard_input_controller.dart index 5fcdbca1..d6a67459 100644 --- a/packages/pinball_components/lib/src/flame/keyboard_input_controller.dart +++ b/packages/pinball_components/lib/src/flame/keyboard_input_controller.dart @@ -1,5 +1,4 @@ import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// The signature for a key handle function @@ -9,13 +8,12 @@ typedef KeyHandlerCallback = bool Function(); /// A that receives keyboard input and execute registered methods /// {@endtemplate} class KeyboardInputController extends Component with KeyboardHandler { - /// {@macro keyboard_input_controller} KeyboardInputController({ Map keyUp = const {}, Map keyDown = const {}, - }) : _keyUp = keyUp, - _keyDown = keyDown; + }) : _keyUp = keyUp, + _keyDown = keyDown; final Map _keyUp; final Map _keyDown; diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart b/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart index 90e7fe19..a513276f 100644 --- a/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart +++ b/packages/pinball_components/sandbox/lib/stories/backboard/game_over.dart @@ -1,4 +1,5 @@ import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; @@ -21,6 +22,15 @@ class BackboardGameOverGame extends BasicKeyboardGame { 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 index e6b28ba1..f85e6685 100644 --- a/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart @@ -18,7 +18,7 @@ void addBackboardStories(Dashbook dashbook) { 'Game over', (context) => GameWidget( game: BackboardGameOverGame( - context.numberProperty('score', 9000000000).toInt(), + context.numberProperty('score', 9000000000).toInt(), ), ), codeLink: buildSourceLink('backboard/game_over.dart'), 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..54d65570 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 initial', + 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 don'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 promp when have 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, + ); + }, + ); + }); +}