feat: game over with initials input (#187)

* adding backboard stories

* progress on the input

* feat: finishing contrls

* adding missing tests

* tests

* removing todo

* Apply suggestions from code review

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* feat: pr suggestions

* feat: pr suggestions

* fix: lint

* feat: pr suggestions

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
pull/178/head
Erick 3 years ago committed by GitHub
parent 707e2e78b0
commit 43ca2beca7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/flame/flame.dart'; import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template controlled_flipper} /// {@template controlled_flipper}
@ -19,7 +21,7 @@ class ControlledFlipper extends Flipper with Controls<FlipperController> {
/// A [ComponentController] that controls a [Flipper]s movement. /// A [ComponentController] that controls a [Flipper]s movement.
/// {@endtemplate} /// {@endtemplate}
class FlipperController extends ComponentController<Flipper> class FlipperController extends ComponentController<Flipper>
with KeyboardHandler { with KeyboardHandler, BlocComponent<GameBloc, GameState> {
/// {@macro flipper_controller} /// {@macro flipper_controller}
FlipperController(Flipper flipper) FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys, : _keys = flipper.side.flipperKeys,
@ -35,6 +37,7 @@ class FlipperController extends ComponentController<Flipper>
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (state?.isGameOver ?? false) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -1,6 +1,8 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/flame/flame.dart'; import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template controlled_plunger} /// {@template controlled_plunger}
@ -18,7 +20,7 @@ class ControlledPlunger extends Plunger with Controls<PlungerController> {
/// A [ComponentController] that controls a [Plunger]s movement. /// A [ComponentController] that controls a [Plunger]s movement.
/// {@endtemplate} /// {@endtemplate}
class PlungerController extends ComponentController<Plunger> class PlungerController extends ComponentController<Plunger>
with KeyboardHandler { with KeyboardHandler, BlocComponent<GameBloc, GameState> {
/// {@macro plunger_controller} /// {@macro plunger_controller}
PlungerController(Plunger plunger) : super(plunger); PlungerController(Plunger plunger) : super(plunger);
@ -36,6 +38,7 @@ class PlungerController extends ComponentController<Plunger>
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (state?.isGameOver ?? false) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -28,7 +28,11 @@ class GameFlowController extends ComponentController<PinballGame>
/// Puts the game on a game over state /// Puts the game on a game over state
void gameOver() { void gameOver() {
component.firstChild<Backboard>()?.gameOverMode(); // TODO(erickzanardo): implement score submission and "navigate" to the
// next page
component.firstChild<Backboard>()?.gameOverMode(
score: state?.score ?? 0,
);
component.firstChild<CameraController>()?.focusOnBackboard(); component.firstChild<CameraController>()?.focusOnBackboard();
} }

@ -58,6 +58,7 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.sparky.bumper.c.inactive.keyName), images.load(components.Assets.images.sparky.bumper.c.inactive.keyName),
images.load(components.Assets.images.backboard.backboardScores.keyName), images.load(components.Assets.images.backboard.backboardScores.keyName),
images.load(components.Assets.images.backboard.backboardGameOver.keyName), images.load(components.Assets.images.backboard.backboardGameOver.keyName),
images.load(components.Assets.images.backboard.display.keyName),
images.load(Assets.images.components.background.path), images.load(Assets.images.components.background.path),
]; ];
} }

@ -41,7 +41,7 @@ class PinballGame extends Forge2DGame
unawaited(add(ScoreEffectController(this))); unawaited(add(ScoreEffectController(this)));
unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this))); unawaited(add(CameraController(this)));
unawaited(add(Backboard(position: Vector2(0, -88)))); unawaited(add(Backboard.waiting(position: Vector2(0, -88))));
await _addGameBoundaries(); await _addGameBoundaries();
unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(Boundaries()));

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@ -57,6 +57,8 @@ class $AssetsImagesBackboardGen {
/// File path: assets/images/backboard/backboard_scores.png /// File path: assets/images/backboard/backboard_scores.png
AssetGenImage get backboardScores => AssetGenImage get backboardScores =>
const AssetGenImage('assets/images/backboard/backboard_scores.png'); const AssetGenImage('assets/images/backboard/backboard_scores.png');
AssetGenImage get display =>
const AssetGenImage('assets/images/backboard/display.png');
} }
class $AssetsImagesBaseboardGen { class $AssetsImagesBaseboardGen {
@ -307,11 +309,8 @@ class $AssetsImagesSparkyBumperGen {
class $AssetsImagesSparkyComputerGen { class $AssetsImagesSparkyComputerGen {
const $AssetsImagesSparkyComputerGen(); const $AssetsImagesSparkyComputerGen();
/// File path: assets/images/sparky/computer/base.png
AssetGenImage get base => AssetGenImage get base =>
const AssetGenImage('assets/images/sparky/computer/base.png'); const AssetGenImage('assets/images/sparky/computer/base.png');
/// File path: assets/images/sparky/computer/top.png
AssetGenImage get top => AssetGenImage get top =>
const AssetGenImage('assets/images/sparky/computer/top.png'); const AssetGenImage('assets/images/sparky/computer/top.png');
} }
@ -355,11 +354,8 @@ class $AssetsImagesDashBumperMainGen {
class $AssetsImagesSparkyBumperAGen { class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen(); const $AssetsImagesSparkyBumperAGen();
/// File path: assets/images/sparky/bumper/a/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/a/active.png'); const AssetGenImage('assets/images/sparky/bumper/a/active.png');
/// File path: assets/images/sparky/bumper/a/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/a/inactive.png'); const AssetGenImage('assets/images/sparky/bumper/a/inactive.png');
} }
@ -367,11 +363,8 @@ class $AssetsImagesSparkyBumperAGen {
class $AssetsImagesSparkyBumperBGen { class $AssetsImagesSparkyBumperBGen {
const $AssetsImagesSparkyBumperBGen(); const $AssetsImagesSparkyBumperBGen();
/// File path: assets/images/sparky/bumper/b/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/b/active.png'); const AssetGenImage('assets/images/sparky/bumper/b/active.png');
/// File path: assets/images/sparky/bumper/b/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/b/inactive.png'); const AssetGenImage('assets/images/sparky/bumper/b/inactive.png');
} }
@ -379,11 +372,8 @@ class $AssetsImagesSparkyBumperBGen {
class $AssetsImagesSparkyBumperCGen { class $AssetsImagesSparkyBumperCGen {
const $AssetsImagesSparkyBumperCGen(); const $AssetsImagesSparkyBumperCGen();
/// File path: assets/images/sparky/bumper/c/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/c/active.png'); const AssetGenImage('assets/images/sparky/bumper/c/active.png');
/// File path: assets/images/sparky/bumper/c/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/c/inactive.png'); const AssetGenImage('assets/images/sparky/bumper/c/inactive.png');
} }

@ -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<void> onLoad() async {
await waitingMode();
}
/// Puts the Backboard in waiting mode, where the scoreboard is shown.
Future<void> 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<void> gameOverMode() async {
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardGameOver.keyName,
);
size = sprite.originalSize / 10;
this.sprite = sprite;
}
}

@ -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<void> waitingMode() async {
children.removeWhere((_) => true);
await add(BackboardWaiting());
}
/// Puts the Backboard in game over mode, where the score input is shown.
Future<void> gameOverMode({
required int score,
BackboardOnSubmit? onSubmit,
}) async {
children.removeWhere((_) => true);
await add(
BackboardGameOver(
score: score,
onSubmit: onSubmit,
),
);
}
}

@ -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<void> 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<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;
}
}

@ -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<void> 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;
}
}

@ -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<void> onLoad() async {
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardScores.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.bottomCenter;
}
}

@ -1,5 +1,5 @@
export 'alien_bumper.dart'; export 'alien_bumper.dart';
export 'backboard.dart'; export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board_dimensions.dart'; export 'board_dimensions.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);
}
}

@ -1,2 +1,3 @@
export 'blueprint.dart'; export 'blueprint.dart';
export 'keyboard_input_controller.dart';
export 'priority.dart'; export 'priority.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<LogicalKeyboardKey, KeyHandlerCallback> keyUp = const {},
Map<LogicalKeyboardKey, KeyHandlerCallback> keyDown = const {},
}) : _keyUp = keyUp,
_keyDown = keyDown;
final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyUp;
final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyDown;
@override
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
final isUp = event is RawKeyUpEvent;
final handlers = isUp ? _keyUp : _keyDown;
final handler = handlers[event.logicalKey];
if (handler != null) {
return handler();
}
return true;
}
}

@ -1,2 +1,3 @@
export 'components/components.dart'; export 'components/components.dart';
export 'extensions/extensions.dart';
export 'flame/flame.dart'; export 'flame/flame.dart';

@ -13,6 +13,7 @@ dependencies:
sdk: flutter sdk: flutter
geometry: geometry:
path: ../geometry path: ../geometry
intl: ^0.17.0
dev_dependencies: dev_dependencies:

@ -11,6 +11,9 @@ abstract class BasicGame extends Forge2DGame {
} }
} }
abstract class BasicKeyboardGame extends BasicGame
with HasKeyboardHandlerComponents {}
abstract class LineGame extends BasicGame with PanDetector { abstract class LineGame extends BasicGame with PanDetector {
Vector2? _lineEnd; Vector2? _lineEnd;

@ -32,6 +32,7 @@ void main() {
addGoogleWordStories(dashbook); addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook); addLaunchRampStories(dashbook);
addScoreTextStories(dashbook); addScoreTextStories(dashbook);
addBackboardStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -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<void> 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,
),
);
},
),
);
}
}

@ -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,
);
}

@ -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<void> onLoad() async {
camera
..followVector2(Vector2.zero())
..zoom = 5;
final backboard = Backboard.waiting(position: Vector2(0, 20));
await add(backboard);
}
}

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

@ -149,6 +149,13 @@ packages:
relative: true relative: true
source: path source: path
version: "1.0.0+1" version: "1.0.0+1"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
js: js:
dependency: transitive dependency: transitive
description: description:

@ -1,3 +1,4 @@
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
class TestGame extends Forge2DGame { class TestGame extends Forge2DGame {
@ -5,3 +6,5 @@ class TestGame extends Forge2DGame {
images.prefix = ''; images.prefix = '';
} }
} }
class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents {}

@ -1,7 +1,9 @@
// ignore_for_file: unawaited_futures // ignore_for_file: unawaited_futures, cascade_invocations
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_test/flame_test.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:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -9,7 +11,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
group('Backboard', () { group('Backboard', () {
final tester = FlameTester(TestGame.new); final tester = FlameTester(KeyboardTestGame.new);
group('on waitingMode', () { group('on waitingMode', () {
tester.testGameWidget( tester.testGameWidget(
@ -17,7 +19,7 @@ void main() {
setUp: (game, tester) async { setUp: (game, tester) async {
game.camera.zoom = 2; game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero()); 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 { verify: (game, tester) async {
await expectLater( await expectLater(
@ -34,20 +36,145 @@ void main() {
setUp: (game, tester) async { setUp: (game, tester) async {
game.camera.zoom = 2; game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero()); 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<BackboardLetterPrompt>().length;
expect(prompts, equals(3));
final score = game.descendants().firstWhere(
(component) =>
component is TextComponent && component.text == '1,000',
);
expect(score, isNotNull);
},
);
tester.testGameWidget(
'can change the initials',
setUp: (game, tester) async {
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
onSubmit: (_) {},
);
await game.ensureAdd(backboard); await game.ensureAdd(backboard);
await backboard.gameOverMode(); // Focus is already on the first letter
await game.ready(); 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(); await tester.pump();
}, },
verify: (game, tester) async { verify: (game, tester) async {
await expectLater( final backboard = game
find.byGame<TestGame>(), .descendants()
matchesGoldenFile('golden/backboard/game_over.png'), .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<BackboardLetterPrompt>();
expect(prompt?.char, equals('C'));
},
);
tester.testGameWidget(
"does nothing when it doesn't have focus",
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<BackboardLetterPrompt>();
expect(prompt?.char, equals('A'));
},
);
tester.testGameWidget(
'blinks the prompt when it has the focus',
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(position: Vector2.zero(), hasFocus: true),
);
},
verify: (game, tester) async {
final underscore = game.descendants().whereType<ShapeComponent>().first;
expect(underscore.paint.color, Colors.white);
game.update(2);
expect(underscore.paint.color, Colors.transparent);
},
);
});
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

@ -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,
);
},
);
});
}

@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -12,6 +13,22 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballGameTest.new);
final gameOverBlocTester = FlameBlocTester<EmptyPinballGameTest, GameBloc>(
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('FlipperController', () {
group('onKeyEvent', () { group('onKeyEvent', () {
final leftKeys = UnmodifiableListView([ 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) { testRawKeyUpEvents(leftKeys, (event) {
flameTester.test( flameTester.test(
'moves downwards ' '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) { testRawKeyUpEvents(leftKeys, (event) {
flameTester.test( flameTester.test(
'does nothing ' 'does nothing '

@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -13,6 +14,22 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballGameTest.new);
final gameOverBlocTester = FlameBlocTester<EmptyPinballGameTest, GameBloc>(
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('PlungerController', () {
group('onKeyEvent', () { group('onKeyEvent', () {
final downKeys = UnmodifiableListView([ 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);
},
);
});
}); });
}); });
} }

@ -42,7 +42,12 @@ void main() {
gameFlowController = GameFlowController(game); gameFlowController = GameFlowController(game);
overlays = MockActiveOverlaysNotifier(); 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(backboard.waitingMode).thenAnswer((_) async {});
when(cameraController.focusOnBackboard).thenAnswer((_) async {}); when(cameraController.focusOnBackboard).thenAnswer((_) async {});
when(cameraController.focusOnGame).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); verify(cameraController.focusOnBackboard).called(1);
}, },
); );

Loading…
Cancel
Save