diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8ed906a2..a805cebc 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -13,3 +13,4 @@ jobs: flutter_channel: stable flutter_version: 2.10.0 coverage_excludes: "lib/gen/*.dart" + test_optimization: false diff --git a/lib/game/behaviors/scoring_behavior.dart b/lib/game/behaviors/scoring_behavior.dart index 119efe6f..84597838 100644 --- a/lib/game/behaviors/scoring_behavior.dart +++ b/lib/game/behaviors/scoring_behavior.dart @@ -1,33 +1,77 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@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} -class ScoringBehavior extends ContactBehavior with HasGameRef { - /// {@macro scoring_behavior} +class ScoringBehavior extends Component with HasGameRef { + /// {@macto scoring_behavior} ScoringBehavior({ required Points points, - }) : _points = points; + required Vector2 position, + double duration = 1, + }) : _points = points, + _position = position, + _effectController = EffectController( + duration: duration, + ); final Points _points; + final Vector2 _position; + + final EffectController _effectController; @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is! Ball) return; + void update(double dt) { + super.update(dt); + if (_effectController.completed) { + removeFromParent(); + } + } + @override + Future onLoad() async { gameRef.read().add(Scored(points: _points.value)); - gameRef.firstChild()!.add( + await gameRef.firstChild()!.add( ScoreComponent( 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 { + /// {@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, + ), + ); + } +} diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index 032c5b22..82b71741 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -20,24 +20,24 @@ class AndroidAcres extends Component { AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidAnimatronic( children: [ - ScoringBehavior(points: Points.twoHundredThousand), + ScoringContactBehavior(points: Points.twoHundredThousand), ], )..initialPosition = Vector2(-26, -28.25), AndroidBumper.a( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-25, 1.3), AndroidBumper.b( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-32.8, -9.2), AndroidBumper.cow( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-20.5, -13.8), diff --git a/lib/game/components/backbox/backbox.dart b/lib/game/components/backbox/backbox.dart new file mode 100644 index 00000000..0ef85fba --- /dev/null +++ b/lib/game/components/backbox/backbox.dart @@ -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 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 onLoad() async { + await super.onLoad(); + + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.backbox.marquee.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 20; + } +} diff --git a/lib/game/components/backbox/displays/displays.dart b/lib/game/components/backbox/displays/displays.dart new file mode 100644 index 00000000..194212ab --- /dev/null +++ b/lib/game/components/backbox/displays/displays.dart @@ -0,0 +1 @@ +export 'initials_input_display.dart'; diff --git a/lib/game/components/backbox/displays/initials_input_display.dart b/lib/game/components/backbox/displays/initials_input_display.dart new file mode 100644 index 00000000..fd286d62 --- /dev/null +++ b/lib/game/components/backbox/displays/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 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() + .map((prompt) => prompt.char) + .join(); + + bool _submit() { + _onSubmit?.call(initials); + return true; + } + + bool _movePrompt(bool left) { + final prompts = children.whereType().toList(); + + final current = prompts.firstWhere((prompt) => prompt.hasFocus) + ..hasFocus = false; + var index = prompts.indexOf(current) + (left ? -1 : 1); + index = min(max(0, index), prompts.length - 1); + + prompts[index].hasFocus = true; + + return false; + } +} + +class _ScoreLabelTextComponent extends TextComponent + with HasGameRef { + _ScoreLabelTextComponent() + : super( + anchor: Anchor.centerLeft, + position: Vector2(-16.9, -24), + textRenderer: _bodyTextPaint.copyWith( + (style) => style.copyWith( + color: PinballColors.red, + ), + ), + ); + + @override + Future onLoad() async { + await super.onLoad(); + text = gameRef.l10n.score; + } +} + +class _ScoreTextComponent extends TextComponent { + _ScoreTextComponent(String score) + : super( + text: score, + anchor: Anchor.centerLeft, + position: Vector2(-16.9, -20), + textRenderer: _bodyTextPaint, + ); +} + +class _NameLabelTextComponent extends TextComponent + with HasGameRef { + _NameLabelTextComponent() + : super( + anchor: Anchor.center, + position: Vector2(11.4, -24), + textRenderer: _bodyTextPaint.copyWith( + (style) => style.copyWith( + color: PinballColors.red, + ), + ), + ); + + @override + Future onLoad() async { + await super.onLoad(); + text = gameRef.l10n.name; + } +} + +class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef { + _CharacterIconSpriteComponent(String characterIconPath) + : _characterIconPath = characterIconPath, + super( + anchor: Anchor.center, + position: Vector2(8.4, -20), + ); + + final String _characterIconPath; + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite(gameRef.images.fromCache(_characterIconPath)); + this.sprite = sprite; + size = sprite.originalSize / 20; + } +} + +/// {@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 onLoad() async { + _underscore = RectangleComponent( + size: Vector2(1.9, 0.4), + anchor: Anchor.center, + position: Vector2(-0.1, 1.8), + ); + + await add(_underscore); + + _input = TextComponent( + text: 'A', + textRenderer: _bodyTextPaint, + anchor: Anchor.center, + ); + await add(_input); + + _underscoreBlinker = TimerComponent( + period: 0.6, + repeat: true, + autoStart: _hasFocus, + onTick: () { + _underscore.paint.color = (_underscore.paint.color == Colors.white) + ? Colors.transparent + : Colors.white; + }, + ); + + await add(_underscoreBlinker); + + await add( + KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowUp: () => _cycle(true), + LogicalKeyboardKey.arrowDown: () => _cycle(false), + }, + ), + ); + } + + /// Returns the current selected character + String get char => String.fromCharCode(_alphabetCode + _charIndex); + + bool _cycle(bool up) { + if (_hasFocus) { + final newCharCode = + min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength); + _input.text = String.fromCharCode(_alphabetCode + newCharCode); + _charIndex = newCharCode; + + return false; + } + return true; + } + + /// Returns if this prompt has focus on it + bool get hasFocus => _hasFocus; + + /// Updates this prompt focus + set hasFocus(bool hasFocus) { + if (hasFocus) { + _underscoreBlinker.timer.resume(); + } else { + _underscoreBlinker.timer.pause(); + } + _underscore.paint.color = Colors.white; + _hasFocus = hasFocus; + } +} + +class _DividerSpriteComponent extends SpriteComponent with HasGameRef { + _DividerSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(0, -17), + ); + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite( + gameRef.images.fromCache(Assets.images.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 { + _EnterInitialsTextComponent() + : super( + anchor: Anchor.center, + position: Vector2(0, -2.4), + textRenderer: _subtitleTextPaint, + ); + + @override + Future onLoad() async { + await super.onLoad(); + text = gameRef.l10n.enterInitials; + } +} + +class _ArrowsTextComponent extends TextComponent with HasGameRef { + _ArrowsTextComponent() + : super( + anchor: Anchor.center, + position: Vector2(-13.2, 0), + textRenderer: _subtitleTextPaint.copyWith( + (style) => style.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ); + + @override + Future onLoad() async { + await super.onLoad(); + text = gameRef.l10n.arrows; + } +} + +class _AndPressTextComponent extends TextComponent + with HasGameRef { + _AndPressTextComponent() + : super( + anchor: Anchor.center, + position: Vector2(-3.7, 0), + textRenderer: _subtitleTextPaint, + ); + + @override + Future onLoad() async { + await super.onLoad(); + text = gameRef.l10n.andPress; + } +} + +class _EnterReturnTextComponent extends TextComponent + with HasGameRef { + _EnterReturnTextComponent() + : super( + anchor: Anchor.center, + position: Vector2(10, 0), + textRenderer: _subtitleTextPaint.copyWith( + (style) => style.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ); + + @override + Future onLoad() async { + await super.onLoad(); + text = gameRef.l10n.enterReturn; + } +} + +class _ToSubmitTextComponent extends TextComponent + with HasGameRef { + _ToSubmitTextComponent() + : super( + anchor: Anchor.center, + position: Vector2(0, 2.4), + textRenderer: _subtitleTextPaint, + ); + + @override + Future onLoad() async { + await super.onLoad(); + text = gameRef.l10n.toSubmit; + } +} diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart index 8def273f..d7856e48 100644 --- a/lib/game/components/bottom_group.dart +++ b/lib/game/components/bottom_group.dart @@ -52,7 +52,8 @@ class _BottomGroupSide extends Component { final kicker = Kicker( side: _side, children: [ - ScoringBehavior(points: Points.fiveThousand)..applyTo(['bouncy_edge']), + ScoringContactBehavior(points: Points.fiveThousand) + ..applyTo(['bouncy_edge']), ], )..initialPosition = Vector2( (22.64 * direction) + centerXAdjustment, diff --git a/lib/game/components/camera_controller.dart b/lib/game/components/camera_controller.dart index a411942e..083e5745 100644 --- a/lib/game/components/camera_controller.dart +++ b/lib/game/components/camera_controller.dart @@ -3,15 +3,15 @@ import 'package:flame/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -/// Adds helpers methods to Flame's [Camera] +/// Adds helpers methods to Flame's [Camera]. extension CameraX on Camera { - /// Instantly apply the point of focus to the [Camera] + /// Instantly apply the point of focus to the [Camera]. void snapToFocus(FocusData data) { followVector2(data.position); zoom = data.zoom; } - /// Returns a [CameraZoom] that can be added to a [FlameGame] + /// Returns a [CameraZoom] that can be added to a [FlameGame]. CameraZoom focusToCameraZoom(FocusData data) { final zoom = CameraZoom(value: data.zoom); zoom.completed.then((_) { @@ -22,7 +22,7 @@ extension CameraX on Camera { } /// {@template focus_data} -/// Model class that defines a focus point of the camera +/// Model class that defines a focus point of the camera. /// {@endtemplate} class FocusData { /// {@template focus_data} @@ -31,50 +31,63 @@ class FocusData { required this.position, }); - /// The amount of zoom + /// The amount of zoom. final double zoom; - /// The position of the camera + /// The position of the camera. final Vector2 position; } /// {@template camera_controller} -/// A [Component] that controls its game camera focus +/// A [Component] that controls its game camera focus. /// {@endtemplate} class CameraController extends ComponentController { /// {@macro camera_controller} CameraController(FlameGame component) : super(component) { final gameZoom = component.size.y / 16; - final backboardZoom = component.size.y / 18; + final waitingBackboxZoom = component.size.y / 18; + final gameOverBackboxZoom = component.size.y / 10; gameFocus = FocusData( zoom: gameZoom, position: Vector2(0, -7.8), ); - backboardFocus = FocusData( - zoom: backboardZoom, - position: Vector2(0, -100.8), + waitingBackboxFocus = FocusData( + zoom: waitingBackboxZoom, + 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 ..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; - /// Holds the data for the backboard focus point - late final FocusData backboardFocus; + /// Holds the data for the waiting backbox focus point. + 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() { component.add(component.camera.focusToCameraZoom(gameFocus)); } - /// Move the camera focus to the backboard - void focusOnBackboard() { - component.add(component.camera.focusToCameraZoom(backboardFocus)); + /// Move the camera focus to the waiting backbox. + void focusOnWaitingBackbox() { + component.add(component.camera.focusToCameraZoom(waitingBackboxFocus)); + } + + /// Move the camera focus to the game over backbox. + void focusOnGameOverBackbox() { + component.add(component.camera.focusToCameraZoom(gameOverBackboxFocus)); } } diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 19784226..2b132656 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,4 +1,5 @@ export 'android_acres/android_acres.dart'; +export 'backbox/backbox.dart'; export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; diff --git a/lib/game/components/dino_desert/dino_desert.dart b/lib/game/components/dino_desert/dino_desert.dart index e415c173..9ba9c71b 100644 --- a/lib/game/components/dino_desert/dino_desert.dart +++ b/lib/game/components/dino_desert/dino_desert.dart @@ -17,7 +17,7 @@ class DinoDesert extends Component { children: [ ChromeDino( children: [ - ScoringBehavior(points: Points.twoHundredThousand) + ScoringContactBehavior(points: Points.twoHundredThousand) ..applyTo(['inside_mouth']), ], )..initialPosition = Vector2(12.6, -6.9), diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index f2b93d00..259b6bb2 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -18,25 +18,25 @@ class FlutterForest extends Component with ZIndex { children: [ Signpost( children: [ - ScoringBehavior(points: Points.fiveThousand), + ScoringContactBehavior(points: Points.fiveThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(8.35, -58.3), DashNestBumper.main( children: [ - ScoringBehavior(points: Points.twoHundredThousand), + ScoringContactBehavior(points: Points.twoHundredThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(18.55, -59.35), DashNestBumper.a( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(8.95, -51.95), DashNestBumper.b( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(22.3, -46.75), diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index edc65329..4af93610 100644 --- a/lib/game/components/game_flow_controller.dart +++ b/lib/game/components/game_flow_controller.dart @@ -1,7 +1,6 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template game_flow_controller} @@ -20,27 +19,26 @@ class GameFlowController extends ComponentController @override void onNewState(GameState state) { if (state.isGameOver) { - gameOver(); + _initialsInput(); } else { start(); } } - /// Puts the game on a game over state - void gameOver() { + /// Puts the game in the initials input state. + void _initialsInput() { // TODO(erickzanardo): implement score submission and "navigate" to the // next page - component.firstChild()?.gameOverMode( + component.descendants().whereType().first.initialsInput( score: state?.score ?? 0, characterIconPath: component.characterTheme.leaderboardIcon.keyName, ); - component.firstChild()?.focusOnBackboard(); + component.firstChild()!.focusOnGameOverBackbox(); } - /// Puts the game on a playing state + /// Puts the game in the playing state. void start() { component.audio.backgroundMusic(); - component.firstChild()?.waitingMode(); component.firstChild()?.focusOnGame(); component.overlays.remove(PinballGame.playButtonOverlay); } diff --git a/lib/game/components/google_word/google_word.dart b/lib/game/components/google_word/google_word.dart index a2f6470a..76bac244 100644 --- a/lib/game/components/google_word/google_word.dart +++ b/lib/game/components/google_word/google_word.dart @@ -16,27 +16,27 @@ class GoogleWord extends Component with ZIndex { children: [ GoogleLetter( 0, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(-13.1, 1.72), GoogleLetter( 1, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(-8.33, -0.75), GoogleLetter( 2, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(-2.88, -1.85), GoogleLetter( 3, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(2.88, -1.85), GoogleLetter( 4, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(8.33, -0.75), GoogleLetter( 5, - children: [ScoringBehavior(points: Points.fiveThousand)], + children: [ScoringContactBehavior(points: Points.fiveThousand)], )..initialPosition = position + Vector2(13.1, 1.72), GoogleWordBonusBehavior(), ], diff --git a/lib/game/components/sparky_scorch.dart b/lib/game/components/sparky_scorch.dart index 7ce83c7a..5a266b4e 100644 --- a/lib/game/components/sparky_scorch.dart +++ b/lib/game/components/sparky_scorch.dart @@ -17,19 +17,19 @@ class SparkyScorch extends Component { children: [ SparkyBumper.a( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-22.9, -41.65), SparkyBumper.b( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-21.25, -57.9), SparkyBumper.c( children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), BumperNoisyBehavior(), ], )..initialPosition = Vector2(-3.3, -52.55), @@ -51,7 +51,7 @@ class SparkyComputerSensor extends BodyComponent : super( renderBody: false, children: [ - ScoringBehavior(points: Points.twentyThousand), + ScoringContactBehavior(points: Points.twentyThousand), ], ); diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 0a9f6654..d066ce0d 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -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.c.lit.keyName), images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName), - images.load(components.Assets.images.backboard.backboardScores.keyName), - images.load(components.Assets.images.backboard.backboardGameOver.keyName), + images.load(components.Assets.images.backbox.marquee.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.dimmed.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.letter6.lit.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.dimmed.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 0cd130ca..aa963a53 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -8,6 +8,7 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -22,6 +23,7 @@ class PinballGame extends PinballForge2DGame PinballGame({ required this.characterTheme, required this.audio, + required this.l10n, }) : super(gravity: Vector2(0, 30)) { images.prefix = ''; controller = _GameBallsController(this); @@ -37,6 +39,8 @@ class PinballGame extends PinballForge2DGame final PinballAudio audio; + final AppLocalizations l10n; + late final GameFlowController gameFlowController; @override @@ -47,7 +51,7 @@ class PinballGame extends PinballForge2DGame final machine = [ BoardBackgroundSpriteComponent(), Boundaries(), - Backboard.waiting(position: Vector2(0, -88)), + Backbox(), ]; final decals = [ GoogleWord(position: Vector2(-4.25, 1.8)), @@ -77,7 +81,7 @@ class PinballGame extends PinballForge2DGame await super.onLoad(); } - BoardSide? focusedBoardSide; + final focusedBoardSide = {}; @override void onTapDown(int pointerId, TapDownInfo info) { @@ -90,9 +94,10 @@ class PinballGame extends PinballForge2DGame descendants().whereType().single.pullFor(2); } else { 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().where((flipper) { - return flipper.side == focusedBoardSide; + return flipper.side == focusedBoardSide[pointerId]; }); flippers.first.moveUp(); } @@ -103,23 +108,23 @@ class PinballGame extends PinballForge2DGame @override void onTapUp(int pointerId, TapUpInfo info) { - _moveFlippersDown(); + _moveFlippersDown(pointerId); super.onTapUp(pointerId, info); } @override void onTapCancel(int pointerId) { - _moveFlippersDown(); + _moveFlippersDown(pointerId); super.onTapCancel(pointerId); } - void _moveFlippersDown() { - if (focusedBoardSide != null) { + void _moveFlippersDown(int pointerId) { + if (focusedBoardSide[pointerId] != null) { final flippers = descendants().whereType().where((flipper) { - return flipper.side == focusedBoardSide; + return flipper.side == focusedBoardSide[pointerId]; }); flippers.first.moveDown(); - focusedBoardSide = null; + focusedBoardSide.remove(pointerId); } } } @@ -163,20 +168,27 @@ class _GameBallsController extends ComponentController } } -class DebugPinballGame extends PinballGame with FPSCounter { +class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { DebugPinballGame({ required CharacterTheme characterTheme, required PinballAudio audio, + required AppLocalizations l10n, }) : super( characterTheme: characterTheme, audio: audio, + l10n: l10n, ) { controller = _GameBallsController(this); } + Vector2? lineStart; + Vector2? lineEnd; + @override Future onLoad() async { await super.onLoad(); + await add(PreviewLine()); + await add(_DebugInformation()); } @@ -190,10 +202,57 @@ class DebugPinballGame extends PinballGame with FPSCounter { firstChild()?.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()?.add(ball); + } } -// TODO(wolfenrain): investigate this CI failure. // coverage:ignore-start +class PreviewLine extends PositionComponent with HasGameRef { + 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 { @override PositionType get positionType => PositionType.widget; diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 4557c243..3a626ba4 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; @@ -39,8 +40,16 @@ class PinballGamePage extends StatelessWidget { final pinballAudio = context.read(); final game = isDebugMode - ? DebugPinballGame(characterTheme: characterTheme, audio: audio) - : PinballGame(characterTheme: characterTheme, audio: audio); + ? DebugPinballGame( + characterTheme: characterTheme, + audio: audio, + l10n: context.l10n, + ) + : PinballGame( + characterTheme: characterTheme, + audio: audio, + l10n: context.l10n, + ); final loadables = [ ...game.preLoadAssets(), diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 5566066f..03fde0bd 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -64,49 +64,45 @@ "@gameOver": { "description": "Text displayed on the ending dialog when game finishes" }, - "leaderboard": "Leaderboard", - "@leaderboard": { - "description": "Text displayed on the ending dialog leaderboard button" + "rounds": "Ball Ct:", + "@rounds": { + "description": "Text displayed on the scoreboard widget to indicate rounds left" }, - "rank": "Rank", - "@rank": { - "description": "Text displayed on the leaderboard page header rank column" + "topPlayers": "Top Players", + "@topPlayers": { + "description": "Title text displayed on leaderboard screen" }, - "character": "Character", - "@character": { - "description": "Text displayed on the leaderboard page header character column" + "rank": "rank", + "@rank": { + "description": "Label text displayed above player's rank" }, - "username": "Username", - "@username": { - "description": "Text displayed on the leaderboard page header userName column" + "name": "name", + "@name": { + "description": "Label text displayed above player's initials" }, - "score": "Score", + "score": "score", "@score": { - "description": "Text displayed on the leaderboard page header score column" - }, - "retry": "Retry", - "@retry": { - "description": "Text displayed on the retry button leaders board page" + "description": "Label text displayed above player's score" }, - "addUser": "Add User", - "@addUser": { - "description": "Text displayed on the add user button at ending dialog" + "enterInitials": "Enter your initials using the", + "@enterInitials": { + "description": "Informational text displayed on initials input screen" }, - "error": "Error", - "@error": { - "description": "Text displayed on the ending dialog when there is any error on sending user" + "arrows": "arrows", + "@arrows": { + "description": "Text displayed on initials input screen indicating arrow keys" }, - "yourScore": "Your score is", - "@yourScore": { - "description": "Text displayed on the ending dialog when game finishes to show the final score" + "andPress": "and press", + "@andPress": { + "description": "Connecting text displayed on initials input screen informational text span" }, - "enterInitials": "Enter your initials", - "@enterInitials": { - "description": "Text displayed on the ending dialog when game finishes to ask the user for his initials" + "enterReturn": "enter/return", + "@enterReturn": { + "description": "Text displayed on initials input screen indicating return key" }, - "rounds": "Ball Ct:", - "@rounds": { - "description": "Text displayed on the scoreboard widget to indicate rounds left" + "toSubmit": "to submit", + "@toSubmit": { + "description": "Ending text displayed on initials input screen informational text span" }, "footerMadeWithText": "Made with ", "@footerMadeWithText": { diff --git a/packages/pinball_components/assets/images/backboard/backboard_game_over.png b/packages/pinball_components/assets/images/backboard/backboard_game_over.png deleted file mode 100644 index 70bd4544..00000000 Binary files a/packages/pinball_components/assets/images/backboard/backboard_game_over.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/backboard/backboard_scores.png b/packages/pinball_components/assets/images/backboard/backboard_scores.png deleted file mode 100644 index dab850d2..00000000 Binary files a/packages/pinball_components/assets/images/backboard/backboard_scores.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/backboard/display.png b/packages/pinball_components/assets/images/backboard/display.png deleted file mode 100644 index 97dbb50b..00000000 Binary files a/packages/pinball_components/assets/images/backboard/display.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/backbox/display-divider.png b/packages/pinball_components/assets/images/backbox/display-divider.png new file mode 100644 index 00000000..c7be2066 Binary files /dev/null and b/packages/pinball_components/assets/images/backbox/display-divider.png differ diff --git a/packages/pinball_components/assets/images/backbox/marquee.png b/packages/pinball_components/assets/images/backbox/marquee.png new file mode 100644 index 00000000..ee98a495 Binary files /dev/null and b/packages/pinball_components/assets/images/backbox/marquee.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 73dd4614..93273683 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -11,7 +11,7 @@ class $AssetsImagesGen { const $AssetsImagesGen(); $AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen(); - $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); + $AssetsImagesBackboxGen get backbox => const $AssetsImagesBackboxGen(); $AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); @@ -50,20 +50,16 @@ class $AssetsImagesAndroidGen { const $AssetsImagesAndroidSpaceshipGen(); } -class $AssetsImagesBackboardGen { - const $AssetsImagesBackboardGen(); +class $AssetsImagesBackboxGen { + const $AssetsImagesBackboxGen(); - /// File path: assets/images/backboard/backboard_game_over.png - AssetGenImage get backboardGameOver => - const AssetGenImage('assets/images/backboard/backboard_game_over.png'); + /// File path: assets/images/backbox/display-divider.png + AssetGenImage get displayDivider => + const AssetGenImage('assets/images/backbox/display-divider.png'); - /// File path: assets/images/backboard/backboard_scores.png - AssetGenImage get backboardScores => - const AssetGenImage('assets/images/backboard/backboard_scores.png'); - - /// File path: assets/images/backboard/display.png - AssetGenImage get display => - const AssetGenImage('assets/images/backboard/display.png'); + /// File path: assets/images/backbox/marquee.png + AssetGenImage get marquee => + const AssetGenImage('assets/images/backbox/marquee.png'); } class $AssetsImagesBallGen { diff --git a/packages/pinball_components/lib/src/components/backboard/backboard.dart b/packages/pinball_components/lib/src/components/backboard/backboard.dart deleted file mode 100644 index fe5fd37c..00000000 --- a/packages/pinball_components/lib/src/components/backboard/backboard.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:async'; - -import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; - -export 'backboard_game_over.dart'; -export 'backboard_letter_prompt.dart'; -export 'backboard_waiting.dart'; - -/// {@template backboard} -/// The [Backboard] of the pinball machine. -/// {@endtemplate} -class Backboard extends PositionComponent with HasGameRef { - /// {@macro backboard} - Backboard({ - required Vector2 position, - }) : super( - position: position, - anchor: Anchor.bottomCenter, - ); - - /// {@macro backboard} - /// - /// Returns a [Backboard] initialized in the waiting mode - factory Backboard.waiting({ - required Vector2 position, - }) { - return Backboard(position: position)..waitingMode(); - } - - /// {@macro backboard} - /// - /// Returns a [Backboard] initialized in the game over mode - factory Backboard.gameOver({ - required Vector2 position, - required String characterIconPath, - required int score, - required BackboardOnSubmit onSubmit, - }) { - return Backboard(position: position) - ..gameOverMode( - score: score, - characterIconPath: characterIconPath, - onSubmit: onSubmit, - ); - } - - /// [TextPaint] used on the [Backboard] - static final textPaint = TextPaint( - style: const TextStyle( - fontSize: 6, - color: Colors.white, - fontFamily: PinballFonts.pixeloidSans, - ), - ); - - /// Puts the Backboard in waiting mode, where the scoreboard is shown. - Future waitingMode() async { - children.removeWhere((_) => true); - await add(BackboardWaiting()); - } - - /// Puts the Backboard in game over mode, where the score input is shown. - Future gameOverMode({ - required int score, - required String characterIconPath, - BackboardOnSubmit? onSubmit, - }) async { - children.removeWhere((_) => true); - await add( - BackboardGameOver( - score: score, - characterIconPath: characterIconPath, - onSubmit: onSubmit, - ), - ); - } -} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart b/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart deleted file mode 100644 index cfea0bc6..00000000 --- a/packages/pinball_components/lib/src/components/backboard/backboard_game_over.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flame/components.dart'; -import 'package:flutter/services.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// Signature for the callback called when the used has -/// submettied their initials on the [BackboardGameOver] -typedef BackboardOnSubmit = void Function(String); - -/// {@template backboard_game_over} -/// [PositionComponent] that handles the user input on the -/// game over display view. -/// {@endtemplate} -class BackboardGameOver extends PositionComponent with HasGameRef { - /// {@macro backboard_game_over} - BackboardGameOver({ - required int score, - required String characterIconPath, - BackboardOnSubmit? onSubmit, - }) : _onSubmit = onSubmit, - super( - children: [ - _BackboardSpriteComponent(), - _BackboardDisplaySpriteComponent(), - _ScoreTextComponent(score.formatScore()), - _CharacterIconSpriteComponent(characterIconPath), - ], - ); - - final BackboardOnSubmit? _onSubmit; - - @override - Future onLoad() async { - for (var i = 0; i < 3; i++) { - await add( - BackboardLetterPrompt( - position: Vector2( - 24.3 + (4.5 * i), - -45, - ), - hasFocus: i == 0, - ), - ); - } - - await add( - KeyboardInputController( - keyUp: { - LogicalKeyboardKey.arrowLeft: () => _movePrompt(true), - LogicalKeyboardKey.arrowRight: () => _movePrompt(false), - LogicalKeyboardKey.enter: _submit, - }, - ), - ); - } - - /// Returns the current inputed initials - String get initials => children - .whereType() - .map((prompt) => prompt.char) - .join(); - - bool _submit() { - _onSubmit?.call(initials); - return true; - } - - bool _movePrompt(bool left) { - final prompts = children.whereType().toList(); - - final current = prompts.firstWhere((prompt) => prompt.hasFocus) - ..hasFocus = false; - var index = prompts.indexOf(current) + (left ? -1 : 1); - index = min(max(0, index), prompts.length - 1); - - prompts[index].hasFocus = true; - - return false; - } -} - -class _BackboardSpriteComponent extends SpriteComponent with HasGameRef { - _BackboardSpriteComponent() : super(anchor: Anchor.bottomCenter); - - @override - Future onLoad() async { - await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.backboard.backboardGameOver.keyName, - ); - this.sprite = sprite; - size = sprite.originalSize / 10; - } -} - -class _BackboardDisplaySpriteComponent extends SpriteComponent with HasGameRef { - _BackboardDisplaySpriteComponent() - : super( - anchor: Anchor.bottomCenter, - position: Vector2(0, -11.5), - ); - - @override - Future onLoad() async { - await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.backboard.display.keyName, - ); - this.sprite = sprite; - size = sprite.originalSize / 10; - } -} - -class _ScoreTextComponent extends TextComponent { - _ScoreTextComponent(String score) - : super( - text: score, - anchor: Anchor.centerLeft, - position: Vector2(-34, -45), - textRenderer: Backboard.textPaint, - ); -} - -class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef { - _CharacterIconSpriteComponent(String characterIconPath) - : _characterIconPath = characterIconPath, - super( - anchor: Anchor.center, - position: Vector2(18.4, -45), - ); - - final String _characterIconPath; - - @override - Future onLoad() async { - await super.onLoad(); - final sprite = Sprite(gameRef.images.fromCache(_characterIconPath)); - this.sprite = sprite; - size = sprite.originalSize / 10; - } -} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart b/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart deleted file mode 100644 index fe582210..00000000 --- a/packages/pinball_components/lib/src/components/backboard/backboard_letter_prompt.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template backboard_letter_prompt} -/// A [PositionComponent] that renders a letter prompt used -/// on the [BackboardGameOver] -/// {@endtemplate} -class BackboardLetterPrompt extends PositionComponent { - /// {@macro backboard_letter_prompt} - BackboardLetterPrompt({ - required Vector2 position, - bool hasFocus = false, - }) : _hasFocus = hasFocus, - super( - position: position, - ); - - static const _alphabetCode = 65; - static const _alphabetLength = 25; - var _charIndex = 0; - - bool _hasFocus; - - late RectangleComponent _underscore; - late TextComponent _input; - late TimerComponent _underscoreBlinker; - - @override - Future onLoad() async { - _underscore = RectangleComponent( - size: Vector2(3.8, 0.8), - anchor: Anchor.center, - position: Vector2(-0.3, 4), - ); - - await add(_underscore); - - _input = TextComponent( - text: 'A', - textRenderer: Backboard.textPaint, - anchor: Anchor.center, - ); - await add(_input); - - _underscoreBlinker = TimerComponent( - period: 0.6, - repeat: true, - autoStart: _hasFocus, - onTick: () { - _underscore.paint.color = (_underscore.paint.color == Colors.white) - ? Colors.transparent - : Colors.white; - }, - ); - - await add(_underscoreBlinker); - - await add( - KeyboardInputController( - keyUp: { - LogicalKeyboardKey.arrowUp: () => _cycle(true), - LogicalKeyboardKey.arrowDown: () => _cycle(false), - }, - ), - ); - } - - /// Returns the current selected character - String get char => String.fromCharCode(_alphabetCode + _charIndex); - - bool _cycle(bool up) { - if (_hasFocus) { - final newCharCode = - min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength); - _input.text = String.fromCharCode(_alphabetCode + newCharCode); - _charIndex = newCharCode; - - return false; - } - return true; - } - - /// Returns if this prompt has focus on it - bool get hasFocus => _hasFocus; - - /// Updates this prompt focus - set hasFocus(bool hasFocus) { - if (hasFocus) { - _underscoreBlinker.timer.resume(); - } else { - _underscoreBlinker.timer.pause(); - } - _underscore.paint.color = Colors.white; - _hasFocus = hasFocus; - } -} diff --git a/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart b/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart deleted file mode 100644 index f7fa84bf..00000000 --- a/packages/pinball_components/lib/src/components/backboard/backboard_waiting.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// [PositionComponent] that shows the leaderboard while the player -/// has not started the game yet. -class BackboardWaiting extends SpriteComponent with HasGameRef { - @override - Future onLoad() async { - final sprite = await gameRef.loadSprite( - Assets.images.backboard.backboardScores.keyName, - ); - - this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.bottomCenter; - } -} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index d3d4253b..2a3d5061 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,7 +1,6 @@ export 'android_animatronic.dart'; export 'android_bumper/android_bumper.dart'; export 'android_spaceship/android_spaceship.dart'; -export 'backboard/backboard.dart'; export 'ball/ball.dart'; export 'baseboard.dart'; export 'board_background_sprite_component.dart'; diff --git a/packages/pinball_components/lib/src/components/score_component.dart b/packages/pinball_components/lib/src/components/score_component.dart index 12d198cb..5f95878a 100644 --- a/packages/pinball_components/lib/src/components/score_component.dart +++ b/packages/pinball_components/lib/src/components/score_component.dart @@ -23,16 +23,20 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex { ScoreComponent({ required this.points, required Vector2 position, - }) : super( + required EffectController effectController, + }) : _effectController = effectController, + super( position: position, anchor: Anchor.center, ) { zIndex = ZIndexes.score; } + late Points points; + late final Effect _effect; - late Points points; + final EffectController _effectController; @override Future onLoad() async { @@ -46,7 +50,7 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex { await add( _effect = MoveEffect.by( Vector2(0, -5), - EffectController(duration: 1), + _effectController, ), ); } diff --git a/packages/pinball_components/lib/src/components/z_indexes.dart b/packages/pinball_components/lib/src/components/z_indexes.dart index a04402b5..b59a9a4b 100644 --- a/packages/pinball_components/lib/src/components/z_indexes.dart +++ b/packages/pinball_components/lib/src/components/z_indexes.dart @@ -114,5 +114,10 @@ abstract class ZIndexes { static const score = _above + spaceshipRampForegroundRailing; // Debug information + static const debugInfo = _above + score; + + // Backbox + + static const backbox = _below + outerBoundary; } diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 9716a526..bee6fd02 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -73,7 +73,6 @@ flutter: - assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/b/ - assets/images/sparky/bumper/c/ - - assets/images/backboard/ - assets/images/google_word/letter1/ - assets/images/google_word/letter2/ - assets/images/google_word/letter3/ @@ -88,6 +87,7 @@ flutter: - assets/images/multiplier/x5/ - assets/images/multiplier/x6/ - assets/images/score/ + - assets/images/backbox/ - assets/images/flapper/ flutter_gen: diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index cb268b41..ccb1b0bc 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -18,7 +18,6 @@ void main() { addGoogleWordStories(dashbook); addLaunchRampStories(dashbook); addScoreStories(dashbook); - addBackboardStories(dashbook); addMultiballStories(dashbook); addMultipliersStories(dashbook); diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart deleted file mode 100644 index ce14d7b6..00000000 --- a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart +++ /dev/null @@ -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 = { - 'Dash': Assets.images.dash.leaderboardIcon.keyName, - 'Sparky': Assets.images.sparky.leaderboardIcon.keyName, - 'Android': Assets.images.android.leaderboardIcon.keyName, - 'Dino': Assets.images.dino.leaderboardIcon.keyName, - }; - - final int score; - - final String character; - - @override - Future onLoad() async { - 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), - ), - ); - }, - ), - ); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart b/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart deleted file mode 100644 index 6da9206c..00000000 --- a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_waiting_game.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; - -class BackboardWaitingGame extends AssetsGame { - BackboardWaitingGame() - : super( - imagesFileNames: [], - ); - - static const description = ''' - Shows how the Backboard in waiting mode is rendered. - '''; - - @override - Future onLoad() async { - camera - ..followVector2(Vector2.zero()) - ..zoom = 5; - - await add( - Backboard.waiting(position: Vector2(0, 20)), - ); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart deleted file mode 100644 index 9e83c7c4..00000000 --- a/packages/pinball_components/sandbox/lib/stories/backboard/stories.dart +++ /dev/null @@ -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(), - ), - ), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/score/score_game.dart b/packages/pinball_components/sandbox/lib/stories/score/score_game.dart index 4bde5018..edb4fa36 100644 --- a/packages/pinball_components/sandbox/lib/stories/score/score_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/score/score_game.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:flame/effects.dart'; import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; @@ -38,6 +39,7 @@ class ScoreGame extends AssetsGame with TapDetector { ScoreComponent( points: score, position: info.eventPosition.game..multiply(Vector2(1, -1)), + effectController: EffectController(duration: 1), ), ); } diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index b8bc567c..b48770ba 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,5 +1,4 @@ export 'android_acres/stories.dart'; -export 'backboard/stories.dart'; export 'ball/stories.dart'; export 'bottom_group/stories.dart'; export 'boundaries/stories.dart'; diff --git a/packages/pinball_components/test/src/components/backboard_test.dart b/packages/pinball_components/test/src/components/backboard_test.dart deleted file mode 100644 index aee2481a..00000000 --- a/packages/pinball_components/test/src/components/backboard_test.dart +++ /dev/null @@ -1,185 +0,0 @@ -// ignore_for_file: unawaited_futures, cascade_invocations - -import 'package:flame/components.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; -import 'package:pinball_theme/pinball_theme.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('Backboard', () { - final characterIconPath = Assets.images.dash.leaderboardIcon.keyName; - final tester = FlameTester(() => KeyboardTestGame([characterIconPath])); - - group('on waitingMode', () { - tester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - game.camera.zoom = 2; - game.camera.followVector2(Vector2.zero()); - await game.ensureAdd(Backboard.waiting(position: Vector2(0, 15))); - await tester.pump(); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('golden/backboard/waiting.png'), - ); - }, - ); - }); - - group('on gameOverMode', () { - tester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - game.camera.zoom = 2; - game.camera.followVector2(Vector2.zero()); - final backboard = Backboard.gameOver( - position: Vector2(0, 15), - score: 1000, - characterIconPath: characterIconPath, - onSubmit: (_) {}, - ); - await game.ensureAdd(backboard); - }, - verify: (game, tester) async { - final prompts = - game.descendants().whereType().length; - expect(prompts, equals(3)); - - final score = game.descendants().firstWhere( - (component) => - component is TextComponent && component.text == '1,000', - ); - expect(score, isNotNull); - }, - ); - - tester.testGameWidget( - 'can change the initials', - setUp: (game, tester) async { - final backboard = Backboard.gameOver( - position: Vector2(0, 15), - score: 1000, - characterIconPath: characterIconPath, - onSubmit: (_) {}, - ); - await game.ensureAdd(backboard); - - // Focus is already on the first letter - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.pump(); - - // Move to the next an press up again - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.pump(); - - // One more time - await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.pump(); - - // Back to the previous and increase one more - await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.pump(); - }, - verify: (game, tester) async { - final backboard = game - .descendants() - .firstWhere((component) => component is BackboardGameOver) - as BackboardGameOver; - - expect(backboard.initials, equals('BCB')); - }, - ); - - String? submitedInitials; - tester.testGameWidget( - 'submits the initials', - setUp: (game, tester) async { - final backboard = Backboard.gameOver( - position: Vector2(0, 15), - score: 1000, - characterIconPath: characterIconPath, - onSubmit: (value) { - submitedInitials = value; - }, - ); - await game.ensureAdd(backboard); - - await tester.sendKeyEvent(LogicalKeyboardKey.enter); - await tester.pump(); - }, - verify: (game, tester) async { - expect(submitedInitials, equals('AAA')); - }, - ); - }); - }); - - group('BackboardLetterPrompt', () { - final tester = FlameTester(KeyboardTestGame.new); - - tester.testGameWidget( - 'cycles the char up and down when it has focus', - setUp: (game, tester) async { - await game.ensureAdd( - BackboardLetterPrompt(hasFocus: true, position: Vector2.zero()), - ); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); - await tester.pump(); - }, - verify: (game, tester) async { - final prompt = game.firstChild(); - expect(prompt?.char, equals('C')); - }, - ); - - tester.testGameWidget( - "does nothing when it doesn't have focus", - setUp: (game, tester) async { - await game.ensureAdd( - BackboardLetterPrompt(position: Vector2.zero()), - ); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.pump(); - }, - verify: (game, tester) async { - final prompt = game.firstChild(); - expect(prompt?.char, equals('A')); - }, - ); - - tester.testGameWidget( - 'blinks the prompt when it has the focus', - setUp: (game, tester) async { - await game.ensureAdd( - BackboardLetterPrompt(position: Vector2.zero(), hasFocus: true), - ); - }, - verify: (game, tester) async { - final underscore = game.descendants().whereType().first; - expect(underscore.paint.color, Colors.white); - - game.update(2); - expect(underscore.paint.color, Colors.transparent); - }, - ); - }); -} diff --git a/packages/pinball_components/test/src/components/golden/backboard/waiting.png b/packages/pinball_components/test/src/components/golden/backboard/waiting.png deleted file mode 100644 index 00164289..00000000 Binary files a/packages/pinball_components/test/src/components/golden/backboard/waiting.png and /dev/null differ diff --git a/packages/pinball_components/test/src/components/score_component_test.dart b/packages/pinball_components/test/src/components/score_component_test.dart index 69688874..f2bd52e3 100644 --- a/packages/pinball_components/test/src/components/score_component_test.dart +++ b/packages/pinball_components/test/src/components/score_component_test.dart @@ -28,6 +28,7 @@ void main() { ScoreComponent( points: Points.oneMillion, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); }, @@ -46,6 +47,7 @@ void main() { ScoreComponent( points: Points.oneMillion, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -67,6 +69,7 @@ void main() { ScoreComponent( points: Points.oneMillion, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -88,6 +91,7 @@ void main() { ScoreComponent( points: Points.fiveThousand, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -113,6 +117,7 @@ void main() { ScoreComponent( points: Points.twentyThousand, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -138,6 +143,7 @@ void main() { ScoreComponent( points: Points.twoHundredThousand, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); @@ -163,6 +169,7 @@ void main() { ScoreComponent( points: Points.oneMillion, position: Vector2.zero(), + effectController: EffectController(duration: 1), ), ); diff --git a/packages/pinball_components/test/src/extensions/score_test.dart b/packages/pinball_components/test/src/extensions/score_test.dart new file mode 100644 index 00000000..d8546ea1 --- /dev/null +++ b/packages/pinball_components/test/src/extensions/score_test.dart @@ -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'); + }); + }); +} diff --git a/packages/pinball_ui/lib/src/theme/pinball_colors.dart b/packages/pinball_ui/lib/src/theme/pinball_colors.dart index df1ddce6..d6029422 100644 --- a/packages/pinball_ui/lib/src/theme/pinball_colors.dart +++ b/packages/pinball_ui/lib/src/theme/pinball_colors.dart @@ -6,6 +6,7 @@ abstract class PinballColors { static const Color darkBlue = Color(0xFF0C32A4); static const Color yellow = Color(0xFFFFEE02); static const Color orange = Color(0xFFE5AB05); + static const Color red = Color(0xFFF03939); static const Color blue = Color(0xFF4B94F6); static const Color transparent = Color(0x00000000); static const Color loadingDarkRed = Color(0xFFE33B2D); diff --git a/packages/pinball_ui/test/src/theme/pinball_colors_test.dart b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart index 3c54c60b..469ab142 100644 --- a/packages/pinball_ui/test/src/theme/pinball_colors_test.dart +++ b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart @@ -20,6 +20,10 @@ void main() { expect(PinballColors.orange, const Color(0xFFE5AB05)); }); + test('red is 0xFFF03939', () { + expect(PinballColors.red, const Color(0xFFF03939)); + }); + test('blue is 0xFF4B94F6', () { expect(PinballColors.blue, const Color(0xFF4B94F6)); }); diff --git a/test/game/behaviors/scoring_behavior_test.dart b/test/game/behaviors/scoring_behavior_test.dart index 07c2753a..3e710641 100644 --- a/test/game/behaviors/scoring_behavior_test.dart +++ b/test/game/behaviors/scoring_behavior_test.dart @@ -34,80 +34,177 @@ void main() { Assets.images.score.oneMillion.keyName, ]; - group('ScoringBehavior', () { - group('beginContact', () { - late GameBloc bloc; - late Ball ball; - late BodyComponent parent; - - setUp(() { - ball = _MockBall(); - final ballBody = _MockBody(); - when(() => ball.body).thenReturn(ballBody); - when(() => ballBody.position).thenReturn(Vector2.all(4)); - - parent = _TestBodyComponent(); - }); - - final flameBlocTester = FlameBlocTester( - 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, - ); + late GameBloc bloc; + late Ball ball; + late BodyComponent parent; + + setUp(() { + ball = _MockBall(); + final ballBody = _MockBody(); + when(() => ball.body).thenReturn(ballBody); + when(() => ballBody.position).thenReturn(Vector2.all(4)); + + parent = _TestBodyComponent(); + }); - flameBlocTester.testGameWidget( - 'emits Scored event with points', - setUp: (game, tester) async { - const points = Points.oneMillion; - final scoringBehavior = ScoringBehavior(points: points); - await parent.add(scoringBehavior); - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); - - scoringBehavior.beginContact(ball, _MockContact()); - - verify( - () => bloc.add( - Scored(points: points.value), - ), - ).called(1); - }, + final flameBlocTester = FlameBlocTester( + 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( - "adds a ScoreComponent at Ball's position with points", - setUp: (game, tester) async { - const points = Points.oneMillion; - final scoringBehavior = ScoringBehavior(points: points); - await parent.add(scoringBehavior); - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); - - scoringBehavior.beginContact(ball, _MockContact()); - await game.ready(); - - final scoreText = game.descendants().whereType(); - expect(scoreText.length, equals(1)); - expect( - scoreText.first.points, - equals(points), - ); - expect( - scoreText.first.position, - equals(ball.body.position), - ); - }, + group('ScoringBehavior', () { + test('can be instantiated', () { + expect( + ScoringBehavior( + points: Points.fiveThousand, + position: Vector2.zero(), + ), + isA(), ); }); + + 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(), + 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(); + 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(), + 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(), + 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(); + expect( + scoreText.first.position, + equals(ball.body.position), + ); + }, + ); }); } diff --git a/test/game/components/backbox/backbox_test.dart b/test/game/components/backbox/backbox_test.dart new file mode 100644 index 00000000..341198f8 --- /dev/null +++ b/test/game/components/backbox/backbox_test.dart @@ -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(), + 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(), isNotNull); + }, + ); + }); +} diff --git a/test/game/components/backbox/displays/initials_input_display_test.dart b/test/game/components/backbox/displays/initials_input_display_test.dart new file mode 100644 index 00000000..993e0678 --- /dev/null +++ b/test/game/components/backbox/displays/initials_input_display_test.dart @@ -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().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(); + 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(); + 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().first; + expect(underscore.paint.color, Colors.white); + + game.update(2); + expect(underscore.paint.color, Colors.transparent); + }, + ); + }); + }); +} diff --git a/test/game/components/camera_controller_test.dart b/test/game/components/camera_controller_test.dart index 6af3f594..934f6340 100644 --- a/test/game/components/camera_controller_test.dart +++ b/test/game/components/camera_controller_test.dart @@ -24,15 +24,18 @@ void main() { test('correctly calculates the zooms', () async { 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 { - expect(game.camera.zoom, equals(controller.backboardFocus.zoom)); - expect(game.camera.follow, equals(controller.backboardFocus.position)); + expect(game.camera.zoom, equals(controller.waitingBackboxFocus.zoom)); + expect( + game.camera.follow, + equals(controller.waitingBackboxFocus.position), + ); }); - group('focusOnBoard', () { + group('focusOnGame', () { test('changes the zoom', () async { controller.focusOnGame(); @@ -53,22 +56,22 @@ void main() { 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 { - controller.focusOnBackboard(); + controller.focusOnWaitingBackbox(); await game.ready(); final zoom = game.firstChild(); 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 { - controller.focusOnBackboard(); + controller.focusOnWaitingBackbox(); await game.ready(); final cameraZoom = game.firstChild()!; final future = cameraZoom.completed; @@ -78,7 +81,32 @@ void main() { 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(); + 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()!; + 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)); }); }); }); diff --git a/test/game/components/dino_desert/dino_desert_test.dart b/test/game/components/dino_desert/dino_desert_test.dart index d4c39dbe..63e45e5b 100644 --- a/test/game/components/dino_desert/dino_desert_test.dart +++ b/test/game/components/dino_desert/dino_desert_test.dart @@ -68,13 +68,13 @@ void main() { group('adds', () { flameTester.test( - 'ScoringBehavior to ChromeDino', + 'ScoringContactBehavior to ChromeDino', (game) async { await game.ensureAdd(DinoDesert()); final chromeDino = game.descendants().whereType().single; expect( - chromeDino.firstChild(), + chromeDino.firstChild(), isNotNull, ); }, diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index c85d0b52..c7196057 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -5,12 +5,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_theme/pinball_theme.dart'; class _MockPinballGame extends Mock implements PinballGame {} -class _MockBackboard extends Mock implements Backboard {} +class _MockBackbox extends Mock implements Backbox {} class _MockCameraController extends Mock implements CameraController {} @@ -40,7 +39,7 @@ void main() { group('onNewState', () { late PinballGame game; - late Backboard backboard; + late Backbox backbox; late CameraController cameraController; late GameFlowController gameFlowController; late PinballAudio pinballAudio; @@ -48,26 +47,26 @@ void main() { setUp(() { game = _MockPinballGame(); - backboard = _MockBackboard(); + backbox = _MockBackbox(); cameraController = _MockCameraController(); gameFlowController = GameFlowController(game); overlays = _MockActiveOverlaysNotifier(); pinballAudio = _MockPinballAudio(); when( - () => backboard.gameOverMode( + () => backbox.initialsInput( score: any(named: 'score'), characterIconPath: any(named: 'characterIconPath'), onSubmit: any(named: 'onSubmit'), ), ).thenAnswer((_) async {}); - when(backboard.waitingMode).thenAnswer((_) async {}); - when(cameraController.focusOnBackboard).thenAnswer((_) async {}); + when(cameraController.focusOnWaitingBackbox).thenAnswer((_) async {}); when(cameraController.focusOnGame).thenAnswer((_) async {}); when(() => overlays.remove(any())).thenAnswer((_) => true); - when(game.firstChild).thenReturn(backboard); + when(() => game.descendants().whereType()) + .thenReturn([backbox]); when(game.firstChild).thenReturn(cameraController); when(() => game.overlays).thenReturn(overlays); when(() => game.characterTheme).thenReturn(DashTheme()); @@ -75,11 +74,12 @@ void main() { }); 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( GameState( - score: 10, + score: 0, multiplier: 1, rounds: 0, bonusHistory: const [], @@ -87,22 +87,21 @@ void main() { ); verify( - () => backboard.gameOverMode( + () => backbox.initialsInput( score: 0, characterIconPath: any(named: 'characterIconPath'), onSubmit: any(named: 'onSubmit'), ), ).called(1); - verify(cameraController.focusOnBackboard).called(1); + verify(cameraController.focusOnGameOverBackbox).called(1); }, ); 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()); - verify(backboard.waitingMode).called(1); verify(cameraController.focusOnGame).called(1); verify(() => overlays.remove(PinballGame.playButtonOverlay)) .called(1); diff --git a/test/game/components/golden/backbox.png b/test/game/components/golden/backbox.png new file mode 100644 index 00000000..962573ab Binary files /dev/null and b/test/game/components/golden/backbox.png differ diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index ac562aec..2c5a47d9 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -26,6 +26,12 @@ class _MockTapUpDetails extends Mock implements TapUpDetails {} 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() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -35,9 +41,8 @@ void main() { Assets.images.android.bumper.b.dimmed.keyName, Assets.images.android.bumper.cow.lit.keyName, Assets.images.android.bumper.cow.dimmed.keyName, - Assets.images.backboard.backboardScores.keyName, - Assets.images.backboard.backboardGameOver.keyName, - Assets.images.backboard.display.keyName, + Assets.images.backbox.marquee.keyName, + Assets.images.backbox.displayDivider.keyName, Assets.images.boardBackground.keyName, theme.Assets.images.android.ball.keyName, theme.Assets.images.dash.ball.keyName, @@ -414,6 +419,51 @@ void main() { 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(); + 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', () { @@ -442,8 +492,9 @@ void main() { }); group('DebugPinballGame', () { + final debugAssets = [Assets.images.ball.flameEffect.keyName, ...assets]; final debugModeFlameTester = FlameTester( - () => DebugPinballTestGame(assets: assets), + () => DebugPinballTestGame(assets: debugAssets), ); 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().toList(); + + game.onPanEnd(_MockDragEndInfo()); + await game.ready(); + + expect( + game.descendants().whereType().length, + equals(previousBalls.length + 1), + ); + }, + ); }); } diff --git a/test/game/view/widgets/bonus_animation_test.dart b/test/game/view/widgets/bonus_animation_test.dart index 2284ca8d..52c1b3d8 100644 --- a/test/game/view/widgets/bonus_animation_test.dart +++ b/test/game/view/widgets/bonus_animation_test.dart @@ -1,9 +1,5 @@ // 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_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -13,8 +9,6 @@ import 'package:pinball_flame/pinball_flame.dart'; import '../../../helpers/helpers.dart'; -class _MockImages extends Mock implements Images {} - class _MockCallback extends Mock { void call(); } @@ -24,13 +18,7 @@ void main() { const animationDuration = 6; setUp(() async { - // TODO(arturplaczek): need to find for a better solution for loading 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; + await mockFlameImages(); }); group('loads SpriteAnimationWidget correctly for', () { diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart index f8be70c2..75fa7439 100644 --- a/test/game/view/widgets/game_hud_test.dart +++ b/test/game/view/widgets/game_hud_test.dart @@ -1,11 +1,8 @@ // ignore_for_file: prefer_const_constructors import 'dart:async'; -import 'dart:typed_data'; 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_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -18,8 +15,6 @@ import 'package:pinball_ui/pinball_ui.dart'; import '../../../helpers/helpers.dart'; -class _MockImages extends Mock implements Images {} - class _MockGameBloc extends Mock implements GameBloc {} void main() { @@ -34,15 +29,9 @@ void main() { ); setUp(() async { - gameBloc = _MockGameBloc(); + await mockFlameImages(); - // TODO(arturplaczek): need to find for a better solution for loading - // 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; + gameBloc = _MockGameBloc(); whenListen( gameBloc, diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 1d7070e0..843592c3 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,10 +1,8 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/flame.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/select_character/select_character.dart'; -import 'package:pinball_theme/pinball_theme.dart'; import '../../../helpers/helpers.dart'; @@ -21,14 +19,12 @@ void main() { late CharacterThemeCubit characterThemeCubit; setUp(() async { - Flame.images.prefix = ''; - 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); + await mockFlameImages(); + game = _MockPinballGame(); gameFlowController = _MockGameFlowController(); characterThemeCubit = _MockCharacterThemeCubit(); + whenListen( characterThemeCubit, const Stream.empty(), diff --git a/test/helpers/fakes.dart b/test/helpers/fakes.dart index d782ede4..706733a1 100644 --- a/test/helpers/fakes.dart +++ b/test/helpers/fakes.dart @@ -5,70 +5,3 @@ import 'package:pinball/game/game.dart'; class FakeContact extends Fake implements Contact {} class FakeGameEvent extends Fake implements GameEvent {} - -const fakeImage = [ - 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, -]; diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index febf8d36..6621abcc 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -2,6 +2,7 @@ export 'builders.dart'; export 'fakes.dart'; export 'forge2d.dart'; export 'key_testers.dart'; +export 'mock_flame_images.dart'; export 'pump_app.dart'; export 'test_games.dart'; export 'text_span.dart'; diff --git a/test/helpers/mock_flame_images.dart b/test/helpers/mock_flame_images.dart new file mode 100644 index 00000000..48e4d40e --- /dev/null +++ b/test/helpers/mock_flame_images.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 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 = [ + 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, +]; diff --git a/test/helpers/test_games.dart b/test/helpers/test_games.dart index 5e67532a..aa1ef777 100644 --- a/test/helpers/test_games.dart +++ b/test/helpers/test_games.dart @@ -2,15 +2,19 @@ import 'dart:async'; +import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_theme/pinball_theme.dart'; class _MockPinballAudio extends Mock implements PinballAudio {} +class _MockAppLocalizations extends Mock implements AppLocalizations {} + class TestGame extends Forge2DGame with FlameBloc { TestGame() { images.prefix = ''; @@ -22,10 +26,12 @@ class PinballTestGame extends PinballGame { List? assets, PinballAudio? audio, CharacterTheme? theme, + AppLocalizations? l10n, }) : _assets = assets, super( audio: audio ?? _MockPinballAudio(), characterTheme: theme ?? const DashTheme(), + l10n: l10n ?? _MockAppLocalizations(), ); final List? _assets; @@ -43,10 +49,12 @@ class DebugPinballTestGame extends DebugPinballGame { List? assets, PinballAudio? audio, CharacterTheme? theme, + AppLocalizations? l10n, }) : _assets = assets, super( audio: audio ?? _MockPinballAudio(), characterTheme: theme ?? const DashTheme(), + l10n: l10n ?? _MockAppLocalizations(), ); final List? _assets; @@ -65,10 +73,34 @@ class EmptyPinballTestGame extends PinballTestGame { List? assets, PinballAudio? audio, CharacterTheme? theme, + AppLocalizations? l10n, + }) : super( + assets: assets, + audio: audio, + theme: theme, + l10n: l10n ?? _MockAppLocalizations(), + ); + + @override + Future onLoad() async { + if (_assets != null) { + await images.loadAll(_assets!); + } + } +} + +class EmptyKeyboardPinballTestGame extends PinballTestGame + with HasKeyboardHandlerComponents { + EmptyKeyboardPinballTestGame({ + List? assets, + PinballAudio? audio, + CharacterTheme? theme, + AppLocalizations? l10n, }) : super( assets: assets, audio: audio, theme: theme, + l10n: l10n ?? _MockAppLocalizations(), ); @override diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index 28033030..7d64dd39 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -1,5 +1,4 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/flame.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -17,11 +16,8 @@ void main() { late CharacterThemeCubit characterThemeCubit; setUp(() async { - Flame.images.prefix = ''; - 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); + await mockFlameImages(); + characterThemeCubit = _MockCharacterThemeCubit(); whenListen( characterThemeCubit,