Merge branch 'main' into feat/ball-final-assets

pull/279/head
Tom Arra 3 years ago committed by GitHub
commit 622210c1c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,3 +13,4 @@ jobs:
flutter_channel: stable flutter_channel: stable
flutter_version: 2.10.0 flutter_version: 2.10.0
coverage_excludes: "lib/gen/*.dart" coverage_excludes: "lib/gen/*.dart"
test_optimization: false

@ -1,33 +1,77 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template scoring_behavior} /// {@template scoring_behavior}
/// Adds points to the score when the [Ball] contacts the [parent]. /// Adds [_points] to the score and shows a text effect.
///
/// The behavior removes itself after the duration.
/// {@endtemplate} /// {@endtemplate}
class ScoringBehavior extends ContactBehavior with HasGameRef<PinballGame> { class ScoringBehavior extends Component with HasGameRef<PinballGame> {
/// {@macro scoring_behavior} /// {@macto scoring_behavior}
ScoringBehavior({ ScoringBehavior({
required Points points, required Points points,
}) : _points = points; required Vector2 position,
double duration = 1,
}) : _points = points,
_position = position,
_effectController = EffectController(
duration: duration,
);
final Points _points; final Points _points;
final Vector2 _position;
final EffectController _effectController;
@override @override
void beginContact(Object other, Contact contact) { void update(double dt) {
super.beginContact(other, contact); super.update(dt);
if (other is! Ball) return; if (_effectController.completed) {
removeFromParent();
}
}
@override
Future<void> onLoad() async {
gameRef.read<GameBloc>().add(Scored(points: _points.value)); gameRef.read<GameBloc>().add(Scored(points: _points.value));
gameRef.firstChild<ZCanvasComponent>()!.add( await gameRef.firstChild<ZCanvasComponent>()!.add(
ScoreComponent( ScoreComponent(
points: _points, points: _points,
position: other.body.position, position: _position,
effectController: _effectController,
), ),
); );
} }
} }
/// {@template scoring_contact_behavior}
/// Adds points to the score when the [Ball] contacts the [parent].
/// {@endtemplate}
class ScoringContactBehavior extends ContactBehavior
with HasGameRef<PinballGame> {
/// {@macro scoring_contact_behavior}
ScoringContactBehavior({
required Points points,
}) : _points = points;
final Points _points;
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.add(
ScoringBehavior(
points: _points,
position: other.body.position,
),
);
}
}

@ -20,24 +20,24 @@ class AndroidAcres extends Component {
AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic( AndroidAnimatronic(
children: [ children: [
ScoringBehavior(points: Points.twoHundredThousand), ScoringContactBehavior(points: Points.twoHundredThousand),
], ],
)..initialPosition = Vector2(-26, -28.25), )..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a( AndroidBumper.a(
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-25, 1.3), )..initialPosition = Vector2(-25, 1.3),
AndroidBumper.b( AndroidBumper.b(
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-32.8, -9.2), )..initialPosition = Vector2(-32.8, -9.2),
AndroidBumper.cow( AndroidBumper.cow(
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-20.5, -13.8), )..initialPosition = Vector2(-20.5, -13.8),

@ -0,0 +1,56 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template backbox}
/// The [Backbox] of the pinball machine.
/// {@endtemplate}
class Backbox extends PositionComponent with HasGameRef, ZIndex {
/// {@macro backbox}
Backbox()
: super(
position: Vector2(0, -87),
anchor: Anchor.bottomCenter,
children: [
_BackboxSpriteComponent(),
],
) {
zIndex = ZIndexes.backbox;
}
/// Puts [InitialsInputDisplay] on the [Backbox].
Future<void> initialsInput({
required int score,
required String characterIconPath,
InitialsOnSubmit? onSubmit,
}) async {
removeAll(children.where((child) => child is! _BackboxSpriteComponent));
await add(
InitialsInputDisplay(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
),
);
}
}
class _BackboxSpriteComponent extends SpriteComponent with HasGameRef {
_BackboxSpriteComponent() : super(anchor: Anchor.bottomCenter);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.backbox.marquee.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}

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

@ -0,0 +1,387 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Signature for the callback called when the used has
/// submitted their initials on the [InitialsInputDisplay].
typedef InitialsOnSubmit = void Function(String);
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 3,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
final _subtitleTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.8,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template initials_input_display}
/// Display that handles the user input on the game over view.
/// {@endtemplate}
// TODO(allisonryan0002): add mobile input buttons.
class InitialsInputDisplay extends Component with HasGameRef {
/// {@macro initials_input_display}
InitialsInputDisplay({
required int score,
required String characterIconPath,
InitialsOnSubmit? onSubmit,
}) : _onSubmit = onSubmit,
super(
children: [
_ScoreLabelTextComponent(),
_ScoreTextComponent(score.formatScore()),
_NameLabelTextComponent(),
_CharacterIconSpriteComponent(characterIconPath),
_DividerSpriteComponent(),
_InstructionsComponent(),
],
);
final InitialsOnSubmit? _onSubmit;
@override
Future<void> onLoad() async {
for (var i = 0; i < 3; i++) {
await add(
InitialsLetterPrompt(
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<InitialsLetterPrompt>()
.map((prompt) => prompt.char)
.join();
bool _submit() {
_onSubmit?.call(initials);
return true;
}
bool _movePrompt(bool left) {
final prompts = children.whereType<InitialsLetterPrompt>().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<PinballGame> {
_ScoreLabelTextComponent()
: super(
anchor: Anchor.centerLeft,
position: Vector2(-16.9, -24),
textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith(
color: PinballColors.red,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.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<PinballGame> {
_NameLabelTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(11.4, -24),
textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith(
color: PinballColors.red,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.name;
}
}
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
_CharacterIconSpriteComponent(String characterIconPath)
: _characterIconPath = characterIconPath,
super(
anchor: Anchor.center,
position: Vector2(8.4, -20),
);
final String _characterIconPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
/// {@template initials_input_display}
/// Display that handles the user input on the game over view.
/// {@endtemplate}
@visibleForTesting
class InitialsLetterPrompt extends PositionComponent {
/// {@macro initials_input_display}
InitialsLetterPrompt({
required Vector2 position,
bool hasFocus = false,
}) : _hasFocus = hasFocus,
super(
position: position,
);
static const _alphabetCode = 65;
static const _alphabetLength = 25;
var _charIndex = 0;
bool _hasFocus;
late RectangleComponent _underscore;
late TextComponent _input;
late TimerComponent _underscoreBlinker;
@override
Future<void> onLoad() async {
_underscore = RectangleComponent(
size: Vector2(1.9, 0.4),
anchor: Anchor.center,
position: Vector2(-0.1, 1.8),
);
await add(_underscore);
_input = TextComponent(
text: 'A',
textRenderer: _bodyTextPaint,
anchor: Anchor.center,
);
await add(_input);
_underscoreBlinker = TimerComponent(
period: 0.6,
repeat: true,
autoStart: _hasFocus,
onTick: () {
_underscore.paint.color = (_underscore.paint.color == Colors.white)
? Colors.transparent
: Colors.white;
},
);
await add(_underscoreBlinker);
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowUp: () => _cycle(true),
LogicalKeyboardKey.arrowDown: () => _cycle(false),
},
),
);
}
/// Returns the current selected character
String get char => String.fromCharCode(_alphabetCode + _charIndex);
bool _cycle(bool up) {
if (_hasFocus) {
final newCharCode =
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength);
_input.text = String.fromCharCode(_alphabetCode + newCharCode);
_charIndex = newCharCode;
return false;
}
return true;
}
/// Returns if this prompt has focus on it
bool get hasFocus => _hasFocus;
/// Updates this prompt focus
set hasFocus(bool hasFocus) {
if (hasFocus) {
_underscoreBlinker.timer.resume();
} else {
_underscoreBlinker.timer.pause();
}
_underscore.paint.color = Colors.white;
_hasFocus = hasFocus;
}
}
class _DividerSpriteComponent extends SpriteComponent with HasGameRef {
_DividerSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -17),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(Assets.images.backbox.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<PinballGame> {
_EnterInitialsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.enterInitials;
}
}
class _ArrowsTextComponent extends TextComponent with HasGameRef<PinballGame> {
_ArrowsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-13.2, 0),
textRenderer: _subtitleTextPaint.copyWith(
(style) => style.copyWith(
fontWeight: FontWeight.bold,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.arrows;
}
}
class _AndPressTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_AndPressTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-3.7, 0),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.andPress;
}
}
class _EnterReturnTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_EnterReturnTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(10, 0),
textRenderer: _subtitleTextPaint.copyWith(
(style) => style.copyWith(
fontWeight: FontWeight.bold,
),
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.enterReturn;
}
}
class _ToSubmitTextComponent extends TextComponent
with HasGameRef<PinballGame> {
_ToSubmitTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 2.4),
textRenderer: _subtitleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = gameRef.l10n.toSubmit;
}
}

@ -52,7 +52,8 @@ class _BottomGroupSide extends Component {
final kicker = Kicker( final kicker = Kicker(
side: _side, side: _side,
children: [ children: [
ScoringBehavior(points: Points.fiveThousand)..applyTo(['bouncy_edge']), ScoringContactBehavior(points: Points.fiveThousand)
..applyTo(['bouncy_edge']),
], ],
)..initialPosition = Vector2( )..initialPosition = Vector2(
(22.64 * direction) + centerXAdjustment, (22.64 * direction) + centerXAdjustment,

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

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

@ -17,7 +17,7 @@ class DinoDesert extends Component {
children: [ children: [
ChromeDino( ChromeDino(
children: [ children: [
ScoringBehavior(points: Points.twoHundredThousand) ScoringContactBehavior(points: Points.twoHundredThousand)
..applyTo(['inside_mouth']), ..applyTo(['inside_mouth']),
], ],
)..initialPosition = Vector2(12.6, -6.9), )..initialPosition = Vector2(12.6, -6.9),

@ -18,25 +18,25 @@ class FlutterForest extends Component with ZIndex {
children: [ children: [
Signpost( Signpost(
children: [ children: [
ScoringBehavior(points: Points.fiveThousand), ScoringContactBehavior(points: Points.fiveThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(8.35, -58.3), )..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main( DashNestBumper.main(
children: [ children: [
ScoringBehavior(points: Points.twoHundredThousand), ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(18.55, -59.35), )..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a( DashNestBumper.a(
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(8.95, -51.95), )..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b( DashNestBumper.b(
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(22.3, -46.75), )..initialPosition = Vector2(22.3, -46.75),

@ -1,7 +1,6 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template game_flow_controller} /// {@template game_flow_controller}
@ -20,27 +19,26 @@ class GameFlowController extends ComponentController<PinballGame>
@override @override
void onNewState(GameState state) { void onNewState(GameState state) {
if (state.isGameOver) { if (state.isGameOver) {
gameOver(); _initialsInput();
} else { } else {
start(); start();
} }
} }
/// Puts the game on a game over state /// Puts the game in the initials input state.
void gameOver() { void _initialsInput() {
// TODO(erickzanardo): implement score submission and "navigate" to the // TODO(erickzanardo): implement score submission and "navigate" to the
// next page // next page
component.firstChild<Backboard>()?.gameOverMode( component.descendants().whereType<Backbox>().first.initialsInput(
score: state?.score ?? 0, score: state?.score ?? 0,
characterIconPath: component.characterTheme.leaderboardIcon.keyName, characterIconPath: component.characterTheme.leaderboardIcon.keyName,
); );
component.firstChild<CameraController>()?.focusOnBackboard(); component.firstChild<CameraController>()!.focusOnGameOverBackbox();
} }
/// Puts the game on a playing state /// Puts the game in the playing state.
void start() { void start() {
component.audio.backgroundMusic(); component.audio.backgroundMusic();
component.firstChild<Backboard>()?.waitingMode();
component.firstChild<CameraController>()?.focusOnGame(); component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay); component.overlays.remove(PinballGame.playButtonOverlay);
} }

@ -16,27 +16,27 @@ class GoogleWord extends Component with ZIndex {
children: [ children: [
GoogleLetter( GoogleLetter(
0, 0,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-13.1, 1.72), )..initialPosition = position + Vector2(-13.1, 1.72),
GoogleLetter( GoogleLetter(
1, 1,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-8.33, -0.75), )..initialPosition = position + Vector2(-8.33, -0.75),
GoogleLetter( GoogleLetter(
2, 2,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-2.88, -1.85), )..initialPosition = position + Vector2(-2.88, -1.85),
GoogleLetter( GoogleLetter(
3, 3,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(2.88, -1.85), )..initialPosition = position + Vector2(2.88, -1.85),
GoogleLetter( GoogleLetter(
4, 4,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(8.33, -0.75), )..initialPosition = position + Vector2(8.33, -0.75),
GoogleLetter( GoogleLetter(
5, 5,
children: [ScoringBehavior(points: Points.fiveThousand)], children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(13.1, 1.72), )..initialPosition = position + Vector2(13.1, 1.72),
GoogleWordBonusBehavior(), GoogleWordBonusBehavior(),
], ],

@ -17,19 +17,19 @@ class SparkyScorch extends Component {
children: [ children: [
SparkyBumper.a( SparkyBumper.a(
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-22.9, -41.65), )..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b( SparkyBumper.b(
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-21.25, -57.9), )..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c( SparkyBumper.c(
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoisyBehavior(),
], ],
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
@ -51,7 +51,7 @@ class SparkyComputerSensor extends BodyComponent
: super( : super(
renderBody: false, renderBody: false,
children: [ children: [
ScoringBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
], ],
); );

@ -98,8 +98,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName), images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName),
images.load(components.Assets.images.sparky.bumper.c.lit.keyName), images.load(components.Assets.images.sparky.bumper.c.lit.keyName),
images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName), images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName),
images.load(components.Assets.images.backboard.backboardScores.keyName), images.load(components.Assets.images.backbox.marquee.keyName),
images.load(components.Assets.images.backboard.backboardGameOver.keyName), images.load(components.Assets.images.backbox.displayDivider.keyName),
images.load(components.Assets.images.googleWord.letter1.lit.keyName), images.load(components.Assets.images.googleWord.letter1.lit.keyName),
images.load(components.Assets.images.googleWord.letter1.dimmed.keyName), images.load(components.Assets.images.googleWord.letter1.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter2.lit.keyName), images.load(components.Assets.images.googleWord.letter2.lit.keyName),
@ -112,7 +112,6 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter5.dimmed.keyName), images.load(components.Assets.images.googleWord.letter5.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter6.lit.keyName), images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName), images.load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.backboard.display.keyName),
images.load(components.Assets.images.multiball.lit.keyName), images.load(components.Assets.images.multiball.lit.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName), images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName),

@ -8,6 +8,7 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -22,6 +23,7 @@ class PinballGame extends PinballForge2DGame
PinballGame({ PinballGame({
required this.characterTheme, required this.characterTheme,
required this.audio, required this.audio,
required this.l10n,
}) : super(gravity: Vector2(0, 30)) { }) : super(gravity: Vector2(0, 30)) {
images.prefix = ''; images.prefix = '';
controller = _GameBallsController(this); controller = _GameBallsController(this);
@ -37,6 +39,8 @@ class PinballGame extends PinballForge2DGame
final PinballAudio audio; final PinballAudio audio;
final AppLocalizations l10n;
late final GameFlowController gameFlowController; late final GameFlowController gameFlowController;
@override @override
@ -47,7 +51,7 @@ class PinballGame extends PinballForge2DGame
final machine = [ final machine = [
BoardBackgroundSpriteComponent(), BoardBackgroundSpriteComponent(),
Boundaries(), Boundaries(),
Backboard.waiting(position: Vector2(0, -88)), Backbox(),
]; ];
final decals = [ final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)), GoogleWord(position: Vector2(-4.25, 1.8)),
@ -77,7 +81,7 @@ class PinballGame extends PinballForge2DGame
await super.onLoad(); await super.onLoad();
} }
BoardSide? focusedBoardSide; final focusedBoardSide = <int, BoardSide>{};
@override @override
void onTapDown(int pointerId, TapDownInfo info) { void onTapDown(int pointerId, TapDownInfo info) {
@ -90,9 +94,10 @@ class PinballGame extends PinballForge2DGame
descendants().whereType<Plunger>().single.pullFor(2); descendants().whereType<Plunger>().single.pullFor(2);
} else { } else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; final leftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; focusedBoardSide[pointerId] =
leftSide ? BoardSide.left : BoardSide.right;
final flippers = descendants().whereType<Flipper>().where((flipper) { final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide; return flipper.side == focusedBoardSide[pointerId];
}); });
flippers.first.moveUp(); flippers.first.moveUp();
} }
@ -103,23 +108,23 @@ class PinballGame extends PinballForge2DGame
@override @override
void onTapUp(int pointerId, TapUpInfo info) { void onTapUp(int pointerId, TapUpInfo info) {
_moveFlippersDown(); _moveFlippersDown(pointerId);
super.onTapUp(pointerId, info); super.onTapUp(pointerId, info);
} }
@override @override
void onTapCancel(int pointerId) { void onTapCancel(int pointerId) {
_moveFlippersDown(); _moveFlippersDown(pointerId);
super.onTapCancel(pointerId); super.onTapCancel(pointerId);
} }
void _moveFlippersDown() { void _moveFlippersDown(int pointerId) {
if (focusedBoardSide != null) { if (focusedBoardSide[pointerId] != null) {
final flippers = descendants().whereType<Flipper>().where((flipper) { final flippers = descendants().whereType<Flipper>().where((flipper) {
return flipper.side == focusedBoardSide; return flipper.side == focusedBoardSide[pointerId];
}); });
flippers.first.moveDown(); flippers.first.moveDown();
focusedBoardSide = null; focusedBoardSide.remove(pointerId);
} }
} }
} }
@ -163,20 +168,27 @@ class _GameBallsController extends ComponentController<PinballGame>
} }
} }
class DebugPinballGame extends PinballGame with FPSCounter { class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({ DebugPinballGame({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
required PinballAudio audio, required PinballAudio audio,
required AppLocalizations l10n,
}) : super( }) : super(
characterTheme: characterTheme, characterTheme: characterTheme,
audio: audio, audio: audio,
l10n: l10n,
) { ) {
controller = _GameBallsController(this); controller = _GameBallsController(this);
} }
Vector2? lineStart;
Vector2? lineEnd;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await add(PreviewLine());
await add(_DebugInformation()); await add(_DebugInformation());
} }
@ -190,10 +202,57 @@ class DebugPinballGame extends PinballGame with FPSCounter {
firstChild<ZCanvasComponent>()?.add(ball); firstChild<ZCanvasComponent>()?.add(ball);
} }
} }
@override
void onPanStart(DragStartInfo info) {
lineStart = info.eventPosition.game;
}
@override
void onPanUpdate(DragUpdateInfo info) {
lineEnd = info.eventPosition.game;
}
@override
void onPanEnd(DragEndInfo info) {
if (lineEnd != null) {
final line = lineEnd! - lineStart!;
_turboChargeBall(line);
lineEnd = null;
lineStart = null;
}
}
void _turboChargeBall(Vector2 line) {
final ball = ControlledBall.debug()..initialPosition = lineStart!;
final impulse = line * -1 * 10;
ball.add(BallTurboChargingBehavior(impulse: impulse));
firstChild<ZCanvasComponent>()?.add(ball);
}
} }
// TODO(wolfenrain): investigate this CI failure.
// coverage:ignore-start // coverage:ignore-start
class PreviewLine extends PositionComponent with HasGameRef<DebugPinballGame> {
static final _previewLinePaint = Paint()
..color = Colors.pink
..strokeWidth = 0.4
..style = PaintingStyle.stroke;
@override
void render(Canvas canvas) {
super.render(canvas);
if (gameRef.lineEnd != null) {
canvas.drawLine(
gameRef.lineStart!.toOffset(),
gameRef.lineEnd!.toOffset(),
_previewLinePaint,
);
}
}
}
// TODO(wolfenrain): investigate this CI failure.
class _DebugInformation extends Component with HasGameRef<DebugPinballGame> { class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
@override @override
PositionType get positionType => PositionType.widget; PositionType get positionType => PositionType.widget;

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
@ -39,8 +40,16 @@ class PinballGamePage extends StatelessWidget {
final pinballAudio = context.read<PinballAudio>(); final pinballAudio = context.read<PinballAudio>();
final game = isDebugMode final game = isDebugMode
? DebugPinballGame(characterTheme: characterTheme, audio: audio) ? DebugPinballGame(
: PinballGame(characterTheme: characterTheme, audio: audio); characterTheme: characterTheme,
audio: audio,
l10n: context.l10n,
)
: PinballGame(
characterTheme: characterTheme,
audio: audio,
l10n: context.l10n,
);
final loadables = [ final loadables = [
...game.preLoadAssets(), ...game.preLoadAssets(),

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

@ -11,7 +11,7 @@ class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
$AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen(); $AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBackboxGen get backbox => const $AssetsImagesBackboxGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
@ -50,20 +50,16 @@ class $AssetsImagesAndroidGen {
const $AssetsImagesAndroidSpaceshipGen(); const $AssetsImagesAndroidSpaceshipGen();
} }
class $AssetsImagesBackboardGen { class $AssetsImagesBackboxGen {
const $AssetsImagesBackboardGen(); const $AssetsImagesBackboxGen();
/// File path: assets/images/backboard/backboard_game_over.png /// File path: assets/images/backbox/display-divider.png
AssetGenImage get backboardGameOver => AssetGenImage get displayDivider =>
const AssetGenImage('assets/images/backboard/backboard_game_over.png'); const AssetGenImage('assets/images/backbox/display-divider.png');
/// File path: assets/images/backboard/backboard_scores.png /// File path: assets/images/backbox/marquee.png
AssetGenImage get backboardScores => AssetGenImage get marquee =>
const AssetGenImage('assets/images/backboard/backboard_scores.png'); const AssetGenImage('assets/images/backbox/marquee.png');
/// File path: assets/images/backboard/display.png
AssetGenImage get display =>
const AssetGenImage('assets/images/backboard/display.png');
} }
class $AssetsImagesBallGen { class $AssetsImagesBallGen {

@ -1,79 +0,0 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
export 'backboard_game_over.dart';
export 'backboard_letter_prompt.dart';
export 'backboard_waiting.dart';
/// {@template backboard}
/// The [Backboard] of the pinball machine.
/// {@endtemplate}
class Backboard extends PositionComponent with HasGameRef {
/// {@macro backboard}
Backboard({
required Vector2 position,
}) : super(
position: position,
anchor: Anchor.bottomCenter,
);
/// {@macro backboard}
///
/// Returns a [Backboard] initialized in the waiting mode
factory Backboard.waiting({
required Vector2 position,
}) {
return Backboard(position: position)..waitingMode();
}
/// {@macro backboard}
///
/// Returns a [Backboard] initialized in the game over mode
factory Backboard.gameOver({
required Vector2 position,
required String characterIconPath,
required int score,
required BackboardOnSubmit onSubmit,
}) {
return Backboard(position: position)
..gameOverMode(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
);
}
/// [TextPaint] used on the [Backboard]
static final textPaint = TextPaint(
style: const TextStyle(
fontSize: 6,
color: Colors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// Puts the Backboard in waiting mode, where the scoreboard is shown.
Future<void> waitingMode() async {
children.removeWhere((_) => true);
await add(BackboardWaiting());
}
/// Puts the Backboard in game over mode, where the score input is shown.
Future<void> gameOverMode({
required int score,
required String characterIconPath,
BackboardOnSubmit? onSubmit,
}) async {
children.removeWhere((_) => true);
await add(
BackboardGameOver(
score: score,
characterIconPath: characterIconPath,
onSubmit: onSubmit,
),
);
}
}

@ -1,144 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Signature for the callback called when the used has
/// submettied their initials on the [BackboardGameOver]
typedef BackboardOnSubmit = void Function(String);
/// {@template backboard_game_over}
/// [PositionComponent] that handles the user input on the
/// game over display view.
/// {@endtemplate}
class BackboardGameOver extends PositionComponent with HasGameRef {
/// {@macro backboard_game_over}
BackboardGameOver({
required int score,
required String characterIconPath,
BackboardOnSubmit? onSubmit,
}) : _onSubmit = onSubmit,
super(
children: [
_BackboardSpriteComponent(),
_BackboardDisplaySpriteComponent(),
_ScoreTextComponent(score.formatScore()),
_CharacterIconSpriteComponent(characterIconPath),
],
);
final BackboardOnSubmit? _onSubmit;
@override
Future<void> onLoad() async {
for (var i = 0; i < 3; i++) {
await add(
BackboardLetterPrompt(
position: Vector2(
24.3 + (4.5 * i),
-45,
),
hasFocus: i == 0,
),
);
}
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowLeft: () => _movePrompt(true),
LogicalKeyboardKey.arrowRight: () => _movePrompt(false),
LogicalKeyboardKey.enter: _submit,
},
),
);
}
/// Returns the current inputed initials
String get initials => children
.whereType<BackboardLetterPrompt>()
.map((prompt) => prompt.char)
.join();
bool _submit() {
_onSubmit?.call(initials);
return true;
}
bool _movePrompt(bool left) {
final prompts = children.whereType<BackboardLetterPrompt>().toList();
final current = prompts.firstWhere((prompt) => prompt.hasFocus)
..hasFocus = false;
var index = prompts.indexOf(current) + (left ? -1 : 1);
index = min(max(0, index), prompts.length - 1);
prompts[index].hasFocus = true;
return false;
}
}
class _BackboardSpriteComponent extends SpriteComponent with HasGameRef {
_BackboardSpriteComponent() : super(anchor: Anchor.bottomCenter);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardGameOver.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _BackboardDisplaySpriteComponent extends SpriteComponent with HasGameRef {
_BackboardDisplaySpriteComponent()
: super(
anchor: Anchor.bottomCenter,
position: Vector2(0, -11.5),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.backboard.display.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _ScoreTextComponent extends TextComponent {
_ScoreTextComponent(String score)
: super(
text: score,
anchor: Anchor.centerLeft,
position: Vector2(-34, -45),
textRenderer: Backboard.textPaint,
);
}
class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
_CharacterIconSpriteComponent(String characterIconPath)
: _characterIconPath = characterIconPath,
super(
anchor: Anchor.center,
position: Vector2(18.4, -45),
);
final String _characterIconPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(gameRef.images.fromCache(_characterIconPath));
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

@ -1,102 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template backboard_letter_prompt}
/// A [PositionComponent] that renders a letter prompt used
/// on the [BackboardGameOver]
/// {@endtemplate}
class BackboardLetterPrompt extends PositionComponent {
/// {@macro backboard_letter_prompt}
BackboardLetterPrompt({
required Vector2 position,
bool hasFocus = false,
}) : _hasFocus = hasFocus,
super(
position: position,
);
static const _alphabetCode = 65;
static const _alphabetLength = 25;
var _charIndex = 0;
bool _hasFocus;
late RectangleComponent _underscore;
late TextComponent _input;
late TimerComponent _underscoreBlinker;
@override
Future<void> onLoad() async {
_underscore = RectangleComponent(
size: Vector2(3.8, 0.8),
anchor: Anchor.center,
position: Vector2(-0.3, 4),
);
await add(_underscore);
_input = TextComponent(
text: 'A',
textRenderer: Backboard.textPaint,
anchor: Anchor.center,
);
await add(_input);
_underscoreBlinker = TimerComponent(
period: 0.6,
repeat: true,
autoStart: _hasFocus,
onTick: () {
_underscore.paint.color = (_underscore.paint.color == Colors.white)
? Colors.transparent
: Colors.white;
},
);
await add(_underscoreBlinker);
await add(
KeyboardInputController(
keyUp: {
LogicalKeyboardKey.arrowUp: () => _cycle(true),
LogicalKeyboardKey.arrowDown: () => _cycle(false),
},
),
);
}
/// Returns the current selected character
String get char => String.fromCharCode(_alphabetCode + _charIndex);
bool _cycle(bool up) {
if (_hasFocus) {
final newCharCode =
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength);
_input.text = String.fromCharCode(_alphabetCode + newCharCode);
_charIndex = newCharCode;
return false;
}
return true;
}
/// Returns if this prompt has focus on it
bool get hasFocus => _hasFocus;
/// Updates this prompt focus
set hasFocus(bool hasFocus) {
if (hasFocus) {
_underscoreBlinker.timer.resume();
} else {
_underscoreBlinker.timer.pause();
}
_underscore.paint.color = Colors.white;
_hasFocus = hasFocus;
}
}

@ -1,17 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// [PositionComponent] that shows the leaderboard while the player
/// has not started the game yet.
class BackboardWaiting extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardScores.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.bottomCenter;
}
}

@ -1,7 +1,6 @@
export 'android_animatronic.dart'; export 'android_animatronic.dart';
export 'android_bumper/android_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'android_spaceship/android_spaceship.dart'; export 'android_spaceship/android_spaceship.dart';
export 'backboard/backboard.dart';
export 'ball/ball.dart'; export 'ball/ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board_background_sprite_component.dart'; export 'board_background_sprite_component.dart';

@ -23,16 +23,20 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
ScoreComponent({ ScoreComponent({
required this.points, required this.points,
required Vector2 position, required Vector2 position,
}) : super( required EffectController effectController,
}) : _effectController = effectController,
super(
position: position, position: position,
anchor: Anchor.center, anchor: Anchor.center,
) { ) {
zIndex = ZIndexes.score; zIndex = ZIndexes.score;
} }
late Points points;
late final Effect _effect; late final Effect _effect;
late Points points; final EffectController _effectController;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -46,7 +50,7 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
await add( await add(
_effect = MoveEffect.by( _effect = MoveEffect.by(
Vector2(0, -5), Vector2(0, -5),
EffectController(duration: 1), _effectController,
), ),
); );
} }

@ -114,5 +114,10 @@ abstract class ZIndexes {
static const score = _above + spaceshipRampForegroundRailing; static const score = _above + spaceshipRampForegroundRailing;
// Debug information // Debug information
static const debugInfo = _above + score; static const debugInfo = _above + score;
// Backbox
static const backbox = _below + outerBoundary;
} }

@ -73,7 +73,6 @@ flutter:
- assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/a/
- assets/images/sparky/bumper/b/ - assets/images/sparky/bumper/b/
- assets/images/sparky/bumper/c/ - assets/images/sparky/bumper/c/
- assets/images/backboard/
- assets/images/google_word/letter1/ - assets/images/google_word/letter1/
- assets/images/google_word/letter2/ - assets/images/google_word/letter2/
- assets/images/google_word/letter3/ - assets/images/google_word/letter3/
@ -88,6 +87,7 @@ flutter:
- assets/images/multiplier/x5/ - assets/images/multiplier/x5/
- assets/images/multiplier/x6/ - assets/images/multiplier/x6/
- assets/images/score/ - assets/images/score/
- assets/images/backbox/
- assets/images/flapper/ - assets/images/flapper/
flutter_gen: flutter_gen:

@ -18,7 +18,6 @@ void main() {
addGoogleWordStories(dashbook); addGoogleWordStories(dashbook);
addLaunchRampStories(dashbook); addLaunchRampStories(dashbook);
addScoreStories(dashbook); addScoreStories(dashbook);
addBackboardStories(dashbook);
addMultiballStories(dashbook); addMultiballStories(dashbook);
addMultipliersStories(dashbook); addMultipliersStories(dashbook);

@ -1,61 +0,0 @@
import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart' as components;
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: [
components.Assets.images.score.fiveThousand.keyName,
components.Assets.images.score.twentyThousand.keyName,
components.Assets.images.score.twoHundredThousand.keyName,
components.Assets.images.score.oneMillion.keyName,
...characterIconPaths.values.toList(),
],
);
static const description = '''
Shows how the Backboard in game over mode is rendered.
- Select a character to update the character icon.
''';
static final characterIconPaths = <String, String>{
'Dash': Assets.images.dash.leaderboardIcon.keyName,
'Sparky': Assets.images.sparky.leaderboardIcon.keyName,
'Android': Assets.images.android.leaderboardIcon.keyName,
'Dino': Assets.images.dino.leaderboardIcon.keyName,
};
final int score;
final String character;
@override
Future<void> onLoad() async {
await super.onLoad();
camera
..followVector2(Vector2.zero())
..zoom = 5;
await add(
components.Backboard.gameOver(
position: Vector2(0, 20),
score: score,
characterIconPath: characterIconPaths[character]!,
onSubmit: (initials) {
add(
components.ScoreComponent(
points: components.Points.values
.firstWhere((element) => element.value == score),
position: Vector2(0, 50),
),
);
},
),
);
}
}

@ -1,25 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BackboardWaitingGame extends AssetsGame {
BackboardWaitingGame()
: super(
imagesFileNames: [],
);
static const description = '''
Shows how the Backboard in waiting mode is rendered.
''';
@override
Future<void> onLoad() async {
camera
..followVector2(Vector2.zero())
..zoom = 5;
await add(
Backboard.waiting(position: Vector2(0, 20)),
);
}
}

@ -1,30 +0,0 @@
import 'package:dashbook/dashbook.dart';
import 'package:pinball_components/pinball_components.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.listProperty(
'Score',
Points.values.first.value,
Points.values.map((score) => score.value).toList(),
),
context.listProperty(
'Character',
BackboardGameOverGame.characterIconPaths.keys.first,
BackboardGameOverGame.characterIconPaths.keys.toList(),
),
),
);
}

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:flame/effects.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
@ -38,6 +39,7 @@ class ScoreGame extends AssetsGame with TapDetector {
ScoreComponent( ScoreComponent(
points: score, points: score,
position: info.eventPosition.game..multiply(Vector2(1, -1)), position: info.eventPosition.game..multiply(Vector2(1, -1)),
effectController: EffectController(duration: 1),
), ),
); );
} }

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

@ -1,185 +0,0 @@
// ignore_for_file: unawaited_futures, cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_theme/pinball_theme.dart';
import '../../helpers/helpers.dart';
void main() {
group('Backboard', () {
final characterIconPath = Assets.images.dash.leaderboardIcon.keyName;
final tester = FlameTester(() => KeyboardTestGame([characterIconPath]));
group('on waitingMode', () {
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero());
await game.ensureAdd(Backboard.waiting(position: Vector2(0, 15)));
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/backboard/waiting.png'),
);
},
);
});
group('on gameOverMode', () {
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.zoom = 2;
game.camera.followVector2(Vector2.zero());
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(backboard);
},
verify: (game, tester) async {
final prompts =
game.descendants().whereType<BackboardLetterPrompt>().length;
expect(prompts, equals(3));
final score = game.descendants().firstWhere(
(component) =>
component is TextComponent && component.text == '1,000',
);
expect(score, isNotNull);
},
);
tester.testGameWidget(
'can change the initials',
setUp: (game, tester) async {
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(backboard);
// Focus is already on the first letter
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// Move to the next an press up again
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// One more time
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
// Back to the previous and increase one more
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final backboard = game
.descendants()
.firstWhere((component) => component is BackboardGameOver)
as BackboardGameOver;
expect(backboard.initials, equals('BCB'));
},
);
String? submitedInitials;
tester.testGameWidget(
'submits the initials',
setUp: (game, tester) async {
final backboard = Backboard.gameOver(
position: Vector2(0, 15),
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (value) {
submitedInitials = value;
},
);
await game.ensureAdd(backboard);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
},
verify: (game, tester) async {
expect(submitedInitials, equals('AAA'));
},
);
});
});
group('BackboardLetterPrompt', () {
final tester = FlameTester(KeyboardTestGame.new);
tester.testGameWidget(
'cycles the char up and down when it has focus',
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(hasFocus: true, position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<BackboardLetterPrompt>();
expect(prompt?.char, equals('C'));
},
);
tester.testGameWidget(
"does nothing when it doesn't have focus",
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<BackboardLetterPrompt>();
expect(prompt?.char, equals('A'));
},
);
tester.testGameWidget(
'blinks the prompt when it has the focus',
setUp: (game, tester) async {
await game.ensureAdd(
BackboardLetterPrompt(position: Vector2.zero(), hasFocus: true),
);
},
verify: (game, tester) async {
final underscore = game.descendants().whereType<ShapeComponent>().first;
expect(underscore.paint.color, Colors.white);
game.update(2);
expect(underscore.paint.color, Colors.transparent);
},
);
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

@ -28,6 +28,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.oneMillion, points: Points.oneMillion,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
}, },
@ -46,6 +47,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.oneMillion, points: Points.oneMillion,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -67,6 +69,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.oneMillion, points: Points.oneMillion,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -88,6 +91,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.fiveThousand, points: Points.fiveThousand,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -113,6 +117,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.twentyThousand, points: Points.twentyThousand,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -138,6 +143,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.twoHundredThousand, points: Points.twoHundredThousand,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );
@ -163,6 +169,7 @@ void main() {
ScoreComponent( ScoreComponent(
points: Points.oneMillion, points: Points.oneMillion,
position: Vector2.zero(), position: Vector2.zero(),
effectController: EffectController(duration: 1),
), ),
); );

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('ScoreX', () {
test('formatScore correctly formats int', () {
expect(1000000.formatScore(), '1,000,000');
});
});
}

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

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

@ -34,80 +34,177 @@ void main() {
Assets.images.score.oneMillion.keyName, Assets.images.score.oneMillion.keyName,
]; ];
group('ScoringBehavior', () { late GameBloc bloc;
group('beginContact', () { late Ball ball;
late GameBloc bloc; late BodyComponent parent;
late Ball ball;
late BodyComponent parent; setUp(() {
ball = _MockBall();
setUp(() { final ballBody = _MockBody();
ball = _MockBall(); when(() => ball.body).thenReturn(ballBody);
final ballBody = _MockBody(); when(() => ballBody.position).thenReturn(Vector2.all(4));
when(() => ball.body).thenReturn(ballBody);
when(() => ballBody.position).thenReturn(Vector2.all(4)); parent = _TestBodyComponent();
});
parent = _TestBodyComponent();
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () {
bloc = _MockGameBloc();
const state = GameState(
score: 0,
multiplier: 1,
rounds: 3,
bonusHistory: [],
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
assets: assets,
);
flameBlocTester.testGameWidget( final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
'emits Scored event with points', gameBuilder: EmptyPinballTestGame.new,
setUp: (game, tester) async { blocBuilder: () {
const points = Points.oneMillion; bloc = _MockGameBloc();
final scoringBehavior = ScoringBehavior(points: points); const state = GameState(
await parent.add(scoringBehavior); score: 0,
final canvas = ZCanvasComponent(children: [parent]); multiplier: 1,
await game.ensureAdd(canvas); rounds: 3,
bonusHistory: [],
scoringBehavior.beginContact(ball, _MockContact());
verify(
() => bloc.add(
Scored(points: points.value),
),
).called(1);
},
); );
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
assets: assets,
);
flameBlocTester.testGameWidget( group('ScoringBehavior', () {
"adds a ScoreComponent at Ball's position with points", test('can be instantiated', () {
setUp: (game, tester) async { expect(
const points = Points.oneMillion; ScoringBehavior(
final scoringBehavior = ScoringBehavior(points: points); points: Points.fiveThousand,
await parent.add(scoringBehavior); position: Vector2.zero(),
final canvas = ZCanvasComponent(children: [parent]); ),
await game.ensureAdd(canvas); isA<ScoringBehavior>(),
scoringBehavior.beginContact(ball, _MockContact());
await game.ready();
final scoreText = game.descendants().whereType<ScoreComponent>();
expect(scoreText.length, equals(1));
expect(
scoreText.first.points,
equals(points),
);
expect(
scoreText.first.position,
equals(ball.body.position),
);
},
); );
}); });
flameBlocTester.testGameWidget(
'can be loaded',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
final behavior = ScoringBehavior(
points: Points.fiveThousand,
position: Vector2.zero(),
);
await parent.add(behavior);
await game.ensureAdd(canvas);
expect(
parent.firstChild<ScoringBehavior>(),
equals(behavior),
);
},
);
flameBlocTester.testGameWidget(
'emits Scored event with points when added',
setUp: (game, tester) async {
const points = Points.oneMillion;
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
final behavior = ScoringBehavior(
points: points,
position: Vector2(0, 0),
);
await parent.ensureAdd(behavior);
verify(
() => bloc.add(
Scored(points: points.value),
),
).called(1);
},
);
flameBlocTester.testGameWidget(
'correctly renders text',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
const points = Points.oneMillion;
final position = Vector2.all(1);
final behavior = ScoringBehavior(
points: points,
position: position,
);
await parent.ensureAdd(behavior);
final scoreText = game.descendants().whereType<ScoreComponent>();
expect(scoreText.length, equals(1));
expect(
scoreText.first.points,
equals(points),
);
expect(
scoreText.first.position,
equals(position),
);
},
);
flameBlocTester.testGameWidget(
'is removed after duration',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
const duration = 2.0;
final behavior = ScoringBehavior(
points: Points.oneMillion,
position: Vector2(0, 0),
duration: duration,
);
await parent.ensureAdd(behavior);
game.update(duration);
game.update(0);
await tester.pump();
},
verify: (game, _) async {
expect(
game.descendants().whereType<ScoringBehavior>(),
isEmpty,
);
},
);
});
group('ScoringContactBehavior', () {
flameBlocTester.testGameWidget(
'beginContact adds a ScoringBehavior',
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
final behavior = ScoringContactBehavior(points: Points.oneMillion);
await parent.ensureAdd(behavior);
behavior.beginContact(ball, _MockContact());
await game.ready();
expect(
parent.firstChild<ScoringBehavior>(),
isNotNull,
);
},
);
flameBlocTester.testGameWidget(
"beginContact positions text at contact's position",
setUp: (game, tester) async {
final canvas = ZCanvasComponent(children: [parent]);
await game.ensureAdd(canvas);
final behavior = ScoringContactBehavior(points: Points.oneMillion);
await parent.ensureAdd(behavior);
behavior.beginContact(ball, _MockContact());
await game.ready();
final scoreText = game.descendants().whereType<ScoreComponent>();
expect(
scoreText.first.position,
equals(ball.body.position),
);
},
);
}); });
} }

@ -0,0 +1,98 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/backbox/displays/initials_input_display.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../helpers/helpers.dart';
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get score => '';
@override
String get name => '';
@override
String get enterInitials => '';
@override
String get arrows => '';
@override
String get andPress => '';
@override
String get enterReturn => '';
@override
String get toSubmit => '';
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final characterIconPath = theme.Assets.images.dash.leaderboardIcon.keyName;
final assets = [
characterIconPath,
Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName,
];
final flameTester = FlameTester(
() => EmptyPinballTestGame(
assets: assets,
l10n: _MockAppLocalizations(),
),
);
group('Backbox', () {
flameTester.test(
'loads correctly',
(game) async {
final backbox = Backbox();
await game.ensureAdd(backbox);
expect(game.children, contains(backbox));
},
);
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
game.camera
..followVector2(Vector2(0, -130))
..zoom = 6;
await game.ensureAdd(Backbox());
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<EmptyPinballTestGame>(),
matchesGoldenFile('../golden/backbox.png'),
);
},
);
flameTester.test(
'initialsInput adds InitialsInputDisplay',
(game) async {
final backbox = Backbox();
await game.ensureAdd(backbox);
await backbox.initialsInput(
score: 0,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ready();
expect(backbox.firstChild<InitialsInputDisplay>(), isNotNull);
},
);
});
}

@ -0,0 +1,189 @@
// ignore_for_file: 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:mocktail/mocktail.dart';
import 'package:pinball/game/components/backbox/displays/initials_input_display.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get score => '';
@override
String get name => '';
@override
String get enterInitials => '';
@override
String get arrows => '';
@override
String get andPress => '';
@override
String get enterReturn => '';
@override
String get toSubmit => '';
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final characterIconPath = theme.Assets.images.dash.leaderboardIcon.keyName;
final assets = [
characterIconPath,
Assets.images.backbox.displayDivider.keyName,
];
final flameTester = FlameTester(
() => EmptyKeyboardPinballTestGame(
assets: assets,
l10n: _MockAppLocalizations(),
),
);
group('InitialsInputDisplay', () {
flameTester.test(
'loads correctly',
(game) async {
final initialsInputDisplay = InitialsInputDisplay(
score: 0,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(initialsInputDisplay);
expect(game.children, contains(initialsInputDisplay));
},
);
flameTester.testGameWidget(
'can change the initials',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final initialsInputDisplay = InitialsInputDisplay(
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (_) {},
);
await game.ensureAdd(initialsInputDisplay);
// 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 initialsInputDisplay =
game.descendants().whereType<InitialsInputDisplay>().single;
expect(initialsInputDisplay.initials, equals('BCB'));
},
);
String? submitedInitials;
flameTester.testGameWidget(
'submits the initials',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final initialsInputDisplay = InitialsInputDisplay(
score: 1000,
characterIconPath: characterIconPath,
onSubmit: (value) {
submitedInitials = value;
},
);
await game.ensureAdd(initialsInputDisplay);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
},
verify: (game, tester) async {
expect(submitedInitials, equals('AAA'));
},
);
group('BackboardLetterPrompt', () {
flameTester.testGameWidget(
'cycles the char up and down when it has focus',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
InitialsLetterPrompt(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<InitialsLetterPrompt>();
expect(prompt?.char, equals('C'));
},
);
flameTester.testGameWidget(
"does nothing when it doesn't have focus",
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
InitialsLetterPrompt(position: Vector2.zero()),
);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
},
verify: (game, tester) async {
final prompt = game.firstChild<InitialsLetterPrompt>();
expect(prompt?.char, equals('A'));
},
);
flameTester.testGameWidget(
'blinks the prompt when it has the focus',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(
InitialsLetterPrompt(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);
},
);
});
});
}

@ -24,15 +24,18 @@ void main() {
test('correctly calculates the zooms', () async { test('correctly calculates the zooms', () async {
expect(controller.gameFocus.zoom.toInt(), equals(12)); expect(controller.gameFocus.zoom.toInt(), equals(12));
expect(controller.backboardFocus.zoom.toInt(), equals(11)); expect(controller.waitingBackboxFocus.zoom.toInt(), equals(11));
}); });
test('correctly sets the initial zoom and position', () async { test('correctly sets the initial zoom and position', () async {
expect(game.camera.zoom, equals(controller.backboardFocus.zoom)); expect(game.camera.zoom, equals(controller.waitingBackboxFocus.zoom));
expect(game.camera.follow, equals(controller.backboardFocus.position)); expect(
game.camera.follow,
equals(controller.waitingBackboxFocus.position),
);
}); });
group('focusOnBoard', () { group('focusOnGame', () {
test('changes the zoom', () async { test('changes the zoom', () async {
controller.focusOnGame(); controller.focusOnGame();
@ -53,22 +56,22 @@ void main() {
await future; await future;
expect(game.camera.position, Vector2(-4, -108.8)); expect(game.camera.position, Vector2(-4, -120));
}); });
}); });
group('focusOnBackboard', () { group('focusOnWaitingBackbox', () {
test('changes the zoom', () async { test('changes the zoom', () async {
controller.focusOnBackboard(); controller.focusOnWaitingBackbox();
await game.ready(); await game.ready();
final zoom = game.firstChild<CameraZoom>(); final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull); expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.backboardFocus.zoom)); expect(zoom?.value, equals(controller.waitingBackboxFocus.zoom));
}); });
test('moves the camera after the zoom is completed', () async { test('moves the camera after the zoom is completed', () async {
controller.focusOnBackboard(); controller.focusOnWaitingBackbox();
await game.ready(); await game.ready();
final cameraZoom = game.firstChild<CameraZoom>()!; final cameraZoom = game.firstChild<CameraZoom>()!;
final future = cameraZoom.completed; final future = cameraZoom.completed;
@ -78,7 +81,32 @@ void main() {
await future; await future;
expect(game.camera.position, Vector2(-4.5, -109.8)); expect(game.camera.position, Vector2(-4.5, -121));
});
});
group('focusOnGameOverBackbox', () {
test('changes the zoom', () async {
controller.focusOnGameOverBackbox();
await game.ready();
final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.gameOverBackboxFocus.zoom));
});
test('moves the camera after the zoom is completed', () async {
controller.focusOnGameOverBackbox();
await game.ready();
final cameraZoom = game.firstChild<CameraZoom>()!;
final future = cameraZoom.completed;
game.update(10);
game.update(0); // Ensure that the component was removed
await future;
expect(game.camera.position, Vector2(-2.5, -117));
}); });
}); });
}); });

@ -68,13 +68,13 @@ void main() {
group('adds', () { group('adds', () {
flameTester.test( flameTester.test(
'ScoringBehavior to ChromeDino', 'ScoringContactBehavior to ChromeDino',
(game) async { (game) async {
await game.ensureAdd(DinoDesert()); await game.ensureAdd(DinoDesert());
final chromeDino = game.descendants().whereType<ChromeDino>().single; final chromeDino = game.descendants().whereType<ChromeDino>().single;
expect( expect(
chromeDino.firstChild<ScoringBehavior>(), chromeDino.firstChild<ScoringContactBehavior>(),
isNotNull, isNotNull,
); );
}, },

@ -5,12 +5,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
class _MockPinballGame extends Mock implements PinballGame {} class _MockPinballGame extends Mock implements PinballGame {}
class _MockBackboard extends Mock implements Backboard {} class _MockBackbox extends Mock implements Backbox {}
class _MockCameraController extends Mock implements CameraController {} class _MockCameraController extends Mock implements CameraController {}
@ -40,7 +39,7 @@ void main() {
group('onNewState', () { group('onNewState', () {
late PinballGame game; late PinballGame game;
late Backboard backboard; late Backbox backbox;
late CameraController cameraController; late CameraController cameraController;
late GameFlowController gameFlowController; late GameFlowController gameFlowController;
late PinballAudio pinballAudio; late PinballAudio pinballAudio;
@ -48,26 +47,26 @@ void main() {
setUp(() { setUp(() {
game = _MockPinballGame(); game = _MockPinballGame();
backboard = _MockBackboard(); backbox = _MockBackbox();
cameraController = _MockCameraController(); cameraController = _MockCameraController();
gameFlowController = GameFlowController(game); gameFlowController = GameFlowController(game);
overlays = _MockActiveOverlaysNotifier(); overlays = _MockActiveOverlaysNotifier();
pinballAudio = _MockPinballAudio(); pinballAudio = _MockPinballAudio();
when( when(
() => backboard.gameOverMode( () => backbox.initialsInput(
score: any(named: 'score'), score: any(named: 'score'),
characterIconPath: any(named: 'characterIconPath'), characterIconPath: any(named: 'characterIconPath'),
onSubmit: any(named: 'onSubmit'), onSubmit: any(named: 'onSubmit'),
), ),
).thenAnswer((_) async {}); ).thenAnswer((_) async {});
when(backboard.waitingMode).thenAnswer((_) async {}); when(cameraController.focusOnWaitingBackbox).thenAnswer((_) async {});
when(cameraController.focusOnBackboard).thenAnswer((_) async {});
when(cameraController.focusOnGame).thenAnswer((_) async {}); when(cameraController.focusOnGame).thenAnswer((_) async {});
when(() => overlays.remove(any())).thenAnswer((_) => true); when(() => overlays.remove(any())).thenAnswer((_) => true);
when(game.firstChild<Backboard>).thenReturn(backboard); when(() => game.descendants().whereType<Backbox>())
.thenReturn([backbox]);
when(game.firstChild<CameraController>).thenReturn(cameraController); when(game.firstChild<CameraController>).thenReturn(cameraController);
when(() => game.overlays).thenReturn(overlays); when(() => game.overlays).thenReturn(overlays);
when(() => game.characterTheme).thenReturn(DashTheme()); when(() => game.characterTheme).thenReturn(DashTheme());
@ -75,11 +74,12 @@ void main() {
}); });
test( test(
'changes the backboard and camera correctly when it is a game over', 'changes the backbox display and camera correctly '
'when the game is over',
() { () {
gameFlowController.onNewState( gameFlowController.onNewState(
GameState( GameState(
score: 10, score: 0,
multiplier: 1, multiplier: 1,
rounds: 0, rounds: 0,
bonusHistory: const [], bonusHistory: const [],
@ -87,22 +87,21 @@ void main() {
); );
verify( verify(
() => backboard.gameOverMode( () => backbox.initialsInput(
score: 0, score: 0,
characterIconPath: any(named: 'characterIconPath'), characterIconPath: any(named: 'characterIconPath'),
onSubmit: any(named: 'onSubmit'), onSubmit: any(named: 'onSubmit'),
), ),
).called(1); ).called(1);
verify(cameraController.focusOnBackboard).called(1); verify(cameraController.focusOnGameOverBackbox).called(1);
}, },
); );
test( test(
'changes the backboard and camera correctly when it is not a game over', 'changes the backbox and camera correctly when it is not a game over',
() { () {
gameFlowController.onNewState(GameState.initial()); gameFlowController.onNewState(GameState.initial());
verify(backboard.waitingMode).called(1);
verify(cameraController.focusOnGame).called(1); verify(cameraController.focusOnGame).called(1);
verify(() => overlays.remove(PinballGame.playButtonOverlay)) verify(() => overlays.remove(PinballGame.playButtonOverlay))
.called(1); .called(1);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@ -26,6 +26,12 @@ class _MockTapUpDetails extends Mock implements TapUpDetails {}
class _MockTapUpInfo extends Mock implements TapUpInfo {} class _MockTapUpInfo extends Mock implements TapUpInfo {}
class _MockDragStartInfo extends Mock implements DragStartInfo {}
class _MockDragUpdateInfo extends Mock implements DragUpdateInfo {}
class _MockDragEndInfo extends Mock implements DragEndInfo {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
@ -35,9 +41,8 @@ void main() {
Assets.images.android.bumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.android.bumper.cow.lit.keyName, Assets.images.android.bumper.cow.lit.keyName,
Assets.images.android.bumper.cow.dimmed.keyName, Assets.images.android.bumper.cow.dimmed.keyName,
Assets.images.backboard.backboardScores.keyName, Assets.images.backbox.marquee.keyName,
Assets.images.backboard.backboardGameOver.keyName, Assets.images.backbox.displayDivider.keyName,
Assets.images.backboard.display.keyName,
Assets.images.boardBackground.keyName, Assets.images.boardBackground.keyName,
theme.Assets.images.android.ball.keyName, theme.Assets.images.android.ball.keyName,
theme.Assets.images.dash.ball.keyName, theme.Assets.images.dash.ball.keyName,
@ -414,6 +419,51 @@ void main() {
expect(flippers.first.body.linearVelocity.y, isPositive); expect(flippers.first.body.linearVelocity.y, isPositive);
}); });
flameTester.test(
'multiple touches control both flippers',
(game) async {
await game.ready();
final raw = _MockTapDownDetails();
when(() => raw.kind).thenReturn(PointerDeviceKind.touch);
final leftEventPosition = _MockEventPosition();
when(() => leftEventPosition.game).thenReturn(Vector2.zero());
when(() => leftEventPosition.widget).thenReturn(Vector2.zero());
final rightEventPosition = _MockEventPosition();
when(() => rightEventPosition.game).thenReturn(Vector2.zero());
when(() => rightEventPosition.widget).thenReturn(game.canvasSize);
final leftTapDownEvent = _MockTapDownInfo();
when(() => leftTapDownEvent.eventPosition)
.thenReturn(leftEventPosition);
when(() => leftTapDownEvent.raw).thenReturn(raw);
final rightTapDownEvent = _MockTapDownInfo();
when(() => rightTapDownEvent.eventPosition)
.thenReturn(rightEventPosition);
when(() => rightTapDownEvent.raw).thenReturn(raw);
final flippers = game.descendants().whereType<Flipper>();
final rightFlipper = flippers.elementAt(0);
final leftFlipper = flippers.elementAt(1);
game.onTapDown(0, leftTapDownEvent);
game.onTapDown(1, rightTapDownEvent);
expect(leftFlipper.body.linearVelocity.y, isNegative);
expect(leftFlipper.side, equals(BoardSide.left));
expect(rightFlipper.body.linearVelocity.y, isNegative);
expect(rightFlipper.side, equals(BoardSide.right));
expect(
game.focusedBoardSide,
equals({0: BoardSide.left, 1: BoardSide.right}),
);
},
);
}); });
group('plunger control', () { group('plunger control', () {
@ -442,8 +492,9 @@ void main() {
}); });
group('DebugPinballGame', () { group('DebugPinballGame', () {
final debugAssets = [Assets.images.ball.flameEffect.keyName, ...assets];
final debugModeFlameTester = FlameTester( final debugModeFlameTester = FlameTester(
() => DebugPinballTestGame(assets: assets), () => DebugPinballTestGame(assets: debugAssets),
); );
debugModeFlameTester.test( debugModeFlameTester.test(
@ -472,5 +523,68 @@ void main() {
); );
}, },
); );
debugModeFlameTester.test(
'set lineStart on pan start',
(game) async {
final startPosition = Vector2.all(10);
final eventPosition = _MockEventPosition();
when(() => eventPosition.game).thenReturn(startPosition);
final dragStartInfo = _MockDragStartInfo();
when(() => dragStartInfo.eventPosition).thenReturn(eventPosition);
game.onPanStart(dragStartInfo);
await game.ready();
expect(
game.lineStart,
equals(startPosition),
);
},
);
debugModeFlameTester.test(
'set lineEnd on pan update',
(game) async {
final endPosition = Vector2.all(10);
final eventPosition = _MockEventPosition();
when(() => eventPosition.game).thenReturn(endPosition);
final dragUpdateInfo = _MockDragUpdateInfo();
when(() => dragUpdateInfo.eventPosition).thenReturn(eventPosition);
game.onPanUpdate(dragUpdateInfo);
await game.ready();
expect(
game.lineEnd,
equals(endPosition),
);
},
);
debugModeFlameTester.test(
'launch ball on pan end',
(game) async {
final startPosition = Vector2.zero();
final endPosition = Vector2.all(10);
game.lineStart = startPosition;
game.lineEnd = endPosition;
await game.ready();
final previousBalls =
game.descendants().whereType<ControlledBall>().toList();
game.onPanEnd(_MockDragEndInfo());
await game.ready();
expect(
game.descendants().whereType<ControlledBall>().length,
equals(previousBalls.length + 1),
);
},
);
}); });
} }

@ -1,9 +1,5 @@
// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: invalid_use_of_protected_member
import 'dart:typed_data';
import 'package:flame/assets.dart';
import 'package:flame/flame.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -13,8 +9,6 @@ import 'package:pinball_flame/pinball_flame.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockImages extends Mock implements Images {}
class _MockCallback extends Mock { class _MockCallback extends Mock {
void call(); void call();
} }
@ -24,13 +18,7 @@ void main() {
const animationDuration = 6; const animationDuration = 6;
setUp(() async { setUp(() async {
// TODO(arturplaczek): need to find for a better solution for loading image await mockFlameImages();
// or use original images from BonusAnimation.loadAssets()
final image = await decodeImageFromList(Uint8List.fromList(fakeImage));
final images = _MockImages();
when(() => images.fromCache(any())).thenReturn(image);
when(() => images.load(any())).thenAnswer((_) => Future.value(image));
Flame.images = images;
}); });
group('loads SpriteAnimationWidget correctly for', () { group('loads SpriteAnimationWidget correctly for', () {

@ -1,11 +1,8 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/assets.dart';
import 'package:flame/flame.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -18,8 +15,6 @@ import 'package:pinball_ui/pinball_ui.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
class _MockImages extends Mock implements Images {}
class _MockGameBloc extends Mock implements GameBloc {} class _MockGameBloc extends Mock implements GameBloc {}
void main() { void main() {
@ -34,15 +29,9 @@ void main() {
); );
setUp(() async { setUp(() async {
gameBloc = _MockGameBloc(); await mockFlameImages();
// TODO(arturplaczek): need to find for a better solution for loading gameBloc = _MockGameBloc();
// image or use original images from BonusAnimation.loadAssets()
final image = await decodeImageFromList(Uint8List.fromList(fakeImage));
final images = _MockImages();
when(() => images.fromCache(any())).thenReturn(image);
when(() => images.load(any())).thenAnswer((_) => Future.value(image));
Flame.images = images;
whenListen( whenListen(
gameBloc, gameBloc,

@ -1,10 +1,8 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/flame.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_theme/pinball_theme.dart';
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
@ -21,14 +19,12 @@ void main() {
late CharacterThemeCubit characterThemeCubit; late CharacterThemeCubit characterThemeCubit;
setUp(() async { setUp(() async {
Flame.images.prefix = ''; await mockFlameImages();
await Flame.images.load(const DashTheme().animation.keyName);
await Flame.images.load(const AndroidTheme().animation.keyName);
await Flame.images.load(const DinoTheme().animation.keyName);
await Flame.images.load(const SparkyTheme().animation.keyName);
game = _MockPinballGame(); game = _MockPinballGame();
gameFlowController = _MockGameFlowController(); gameFlowController = _MockGameFlowController();
characterThemeCubit = _MockCharacterThemeCubit(); characterThemeCubit = _MockCharacterThemeCubit();
whenListen( whenListen(
characterThemeCubit, characterThemeCubit,
const Stream<CharacterThemeState>.empty(), const Stream<CharacterThemeState>.empty(),

@ -5,70 +5,3 @@ import 'package:pinball/game/game.dart';
class FakeContact extends Fake implements Contact {} class FakeContact extends Fake implements Contact {}
class FakeGameEvent extends Fake implements GameEvent {} class FakeGameEvent extends Fake implements GameEvent {}
const fakeImage = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
];

@ -2,6 +2,7 @@ export 'builders.dart';
export 'fakes.dart'; export 'fakes.dart';
export 'forge2d.dart'; export 'forge2d.dart';
export 'key_testers.dart'; export 'key_testers.dart';
export 'mock_flame_images.dart';
export 'pump_app.dart'; export 'pump_app.dart';
export 'test_games.dart'; export 'test_games.dart';
export 'text_span.dart'; export 'text_span.dart';

@ -0,0 +1,92 @@
import 'dart:typed_data';
import 'package:flame/assets.dart';
import 'package:flame/flame.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class _MockImages extends Mock implements Images {}
/// {@template mock_flame_images}
/// Mock for flame images instance.
///
/// Using real images blocks the tests, for this reason we need fake image
/// everywhere we use [Images.fromCache] or [Images.load].
/// {@endtemplate}
// TODO(arturplaczek): need to find for a better solution for loading image
// or use original images.
Future<void> mockFlameImages() async {
final image = await decodeImageFromList(Uint8List.fromList(_fakeImage));
final images = _MockImages();
when(() => images.fromCache(any())).thenReturn(image);
when(() => images.load(any())).thenAnswer((_) => Future.value(image));
Flame.images = images;
}
const _fakeImage = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
];

@ -2,15 +2,19 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
class _MockPinballAudio extends Mock implements PinballAudio {} class _MockPinballAudio extends Mock implements PinballAudio {}
class _MockAppLocalizations extends Mock implements AppLocalizations {}
class TestGame extends Forge2DGame with FlameBloc { class TestGame extends Forge2DGame with FlameBloc {
TestGame() { TestGame() {
images.prefix = ''; images.prefix = '';
@ -22,10 +26,12 @@ class PinballTestGame extends PinballGame {
List<String>? assets, List<String>? assets,
PinballAudio? audio, PinballAudio? audio,
CharacterTheme? theme, CharacterTheme? theme,
AppLocalizations? l10n,
}) : _assets = assets, }) : _assets = assets,
super( super(
audio: audio ?? _MockPinballAudio(), audio: audio ?? _MockPinballAudio(),
characterTheme: theme ?? const DashTheme(), characterTheme: theme ?? const DashTheme(),
l10n: l10n ?? _MockAppLocalizations(),
); );
final List<String>? _assets; final List<String>? _assets;
@ -43,10 +49,12 @@ class DebugPinballTestGame extends DebugPinballGame {
List<String>? assets, List<String>? assets,
PinballAudio? audio, PinballAudio? audio,
CharacterTheme? theme, CharacterTheme? theme,
AppLocalizations? l10n,
}) : _assets = assets, }) : _assets = assets,
super( super(
audio: audio ?? _MockPinballAudio(), audio: audio ?? _MockPinballAudio(),
characterTheme: theme ?? const DashTheme(), characterTheme: theme ?? const DashTheme(),
l10n: l10n ?? _MockAppLocalizations(),
); );
final List<String>? _assets; final List<String>? _assets;
@ -65,10 +73,34 @@ class EmptyPinballTestGame extends PinballTestGame {
List<String>? assets, List<String>? assets,
PinballAudio? audio, PinballAudio? audio,
CharacterTheme? theme, CharacterTheme? theme,
AppLocalizations? l10n,
}) : super(
assets: assets,
audio: audio,
theme: theme,
l10n: l10n ?? _MockAppLocalizations(),
);
@override
Future<void> onLoad() async {
if (_assets != null) {
await images.loadAll(_assets!);
}
}
}
class EmptyKeyboardPinballTestGame extends PinballTestGame
with HasKeyboardHandlerComponents {
EmptyKeyboardPinballTestGame({
List<String>? assets,
PinballAudio? audio,
CharacterTheme? theme,
AppLocalizations? l10n,
}) : super( }) : super(
assets: assets, assets: assets,
audio: audio, audio: audio,
theme: theme, theme: theme,
l10n: l10n ?? _MockAppLocalizations(),
); );
@override @override

@ -1,5 +1,4 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/flame.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -17,11 +16,8 @@ void main() {
late CharacterThemeCubit characterThemeCubit; late CharacterThemeCubit characterThemeCubit;
setUp(() async { setUp(() async {
Flame.images.prefix = ''; await mockFlameImages();
await Flame.images.load(const DashTheme().animation.keyName);
await Flame.images.load(const AndroidTheme().animation.keyName);
await Flame.images.load(const DinoTheme().animation.keyName);
await Flame.images.load(const SparkyTheme().animation.keyName);
characterThemeCubit = _MockCharacterThemeCubit(); characterThemeCubit = _MockCharacterThemeCubit();
whenListen( whenListen(
characterThemeCubit, characterThemeCubit,

Loading…
Cancel
Save