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/firebase.json b/firebase.json index 80e2ae69..1338aeba 100644 --- a/firebase.json +++ b/firebase.json @@ -1,11 +1,33 @@ { + "firestore": { + "rules": "firestore.rules" + }, "hosting": { "public": "build/web", "site": "ashehwkdkdjruejdnensjsjdne", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "headers": [ + { + "source": "**/*.@(jpg|jpeg|gif|png)", + "headers": [ + { + "key": "Cache-Control", + "value": "max-age=3600" + } + ] + }, + { + "source": "**", + "headers": [ + { + "key": "Cache-Control", + "value": "no-cache, no-store, must-revalidate" + } + ] + } ] + }, + "storage": { + "rules": "storage.rules" } } diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 00000000..fbff78f0 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,29 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /leaderboard/{userId} { + + function prohibited(initials) { + let prohibitedInitials = get(/databases/$(database)/documents/prohibitedInitials/list).data.prohibitedInitials; + return initials in prohibitedInitials; + } + + function inCharLimit(initials) { + return initials.size() < 4; + } + + function isAuthedUser(auth) { + return request.auth.uid != null && auth.token.firebase.sign_in_provider == "anonymous" + } + + // Leaderboard can be read if it doesn't contain any prohibited initials + allow read: if !prohibited(resource.data.playerInitials); + + // A leaderboard entry can be created if the user is authenticated, + // it's 3 characters long, and not a prohibited combination. + allow create: if isAuthedUser(request.auth) && + inCharLimit(request.resource.data.playerInitials) && + !prohibited(request.resource.data.playerInitials); + } + } +} \ No newline at end of file diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index d778b55b..de46512b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -8,6 +8,7 @@ import 'package:leaderboard_repository/leaderboard_repository.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'; import 'package:pinball_ui/pinball_ui.dart'; @@ -34,8 +35,11 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _pinballAudio), ], - child: BlocProvider( - create: (context) => CharacterThemeCubit(), + child: MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => CharacterThemeCubit()), + BlocProvider(create: (_) => StartGameBloc()), + ], child: MaterialApp( title: 'I/O Pinball', theme: PinballTheme.standard, diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 49f40d1f..43d6005b 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -17,12 +17,13 @@ class GameBloc extends Bloc { } void _onRoundLost(RoundLost event, Emitter emit) { - final score = state.score * state.multiplier; + final score = state.totalScore + state.roundScore * state.multiplier; final roundsLeft = math.max(state.rounds - 1, 0); emit( state.copyWith( - score: score, + totalScore: score, + roundScore: 0, multiplier: 1, rounds: roundsLeft, ), @@ -32,7 +33,7 @@ class GameBloc extends Bloc { void _onScored(Scored event, Emitter emit) { if (!state.isGameOver) { emit( - state.copyWith(score: state.score + event.points), + state.copyWith(roundScore: state.roundScore + event.points), ); } } diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 4ce9042d..2ccb4405 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -26,22 +26,32 @@ enum GameBonus { class GameState extends Equatable { /// {@macro game_state} const GameState({ - required this.score, + required this.totalScore, + required this.roundScore, required this.multiplier, required this.rounds, required this.bonusHistory, - }) : assert(score >= 0, "Score can't be negative"), + }) : assert(totalScore >= 0, "TotalScore can't be negative"), + assert(roundScore >= 0, "Round score can't be negative"), assert(multiplier > 0, 'Multiplier must be greater than zero'), assert(rounds >= 0, "Number of rounds can't be negative"); const GameState.initial() - : score = 0, + : totalScore = 0, + roundScore = 0, multiplier = 1, rounds = 3, bonusHistory = const []; - /// The current score of the game. - final int score; + /// The score for the current round of the game. + /// + /// Multipliers are only applied to the score for the current round once is + /// lost. Then the [roundScore] is added to the [totalScore] and reset to 0 + /// for the next round. + final int roundScore; + + /// The total score of the game. + final int totalScore; /// The current multiplier for the score. final int multiplier; @@ -58,20 +68,25 @@ class GameState extends Equatable { /// Determines when the game is over. bool get isGameOver => rounds == 0; + /// The score displayed at the game. + int get displayScore => roundScore + totalScore; + GameState copyWith({ - int? score, + int? totalScore, + int? roundScore, int? multiplier, int? balls, int? rounds, List? bonusHistory, }) { assert( - score == null || score >= this.score, - "Score can't be decreased", + totalScore == null || totalScore >= this.totalScore, + "Total score can't be decreased", ); return GameState( - score: score ?? this.score, + totalScore: totalScore ?? this.totalScore, + roundScore: roundScore ?? this.roundScore, multiplier: multiplier ?? this.multiplier, rounds: rounds ?? this.rounds, bonusHistory: bonusHistory ?? this.bonusHistory, @@ -80,7 +95,8 @@ class GameState extends Equatable { @override List get props => [ - score, + totalScore, + roundScore, multiplier, rounds, bonusHistory, diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index 82b71741..649ef196 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -15,7 +15,16 @@ class AndroidAcres extends Component { AndroidAcres() : super( children: [ - SpaceshipRamp(), + SpaceshipRamp( + children: [ + RampShotBehavior( + points: Points.fiveThousand, + ), + RampBonusBehavior( + points: Points.oneMillion, + ), + ], + ), SpaceshipRail(), AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidAnimatronic( diff --git a/lib/game/components/android_acres/behaviors/behaviors.dart b/lib/game/components/android_acres/behaviors/behaviors.dart index e4ac5981..91b1e132 100644 --- a/lib/game/components/android_acres/behaviors/behaviors.dart +++ b/lib/game/components/android_acres/behaviors/behaviors.dart @@ -1 +1,3 @@ export 'android_spaceship_bonus_behavior.dart'; +export 'ramp_bonus_behavior.dart'; +export 'ramp_shot_behavior.dart'; diff --git a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart new file mode 100644 index 00000000..218ad8b4 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_bonus_behavior} +/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp]. +/// {@endtemplate} +class RampBonusBehavior extends Component + with ParentIsA, HasGameRef { + /// {@macro ramp_bonus_behavior} + RampBonusBehavior({ + required Points points, + }) : _points = points, + super(); + + /// Creates a [RampBonusBehavior]. + /// + /// This can be used for testing [RampBonusBehavior] in isolation. + @visibleForTesting + RampBonusBehavior.test({ + required Points points, + required this.subscription, + }) : _points = points, + super(); + + final Points _points; + + /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. + @visibleForTesting + StreamSubscription? subscription; + + @override + void onMount() { + super.onMount(); + + subscription = subscription ?? + parent.bloc.stream.listen((state) { + final achievedOneMillionPoints = state.hits % 10 == 0; + + if (achievedOneMillionPoints) { + parent.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -60), + duration: 2, + ), + ); + } + }); + } + + @override + void onRemove() { + subscription?.cancel(); + super.onRemove(); + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart new file mode 100644 index 00000000..8a9c1a9c --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart @@ -0,0 +1,63 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_shot_behavior} +/// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. +/// {@endtemplate} +class RampShotBehavior extends Component + with ParentIsA, HasGameRef { + /// {@macro ramp_shot_behavior} + RampShotBehavior({ + required Points points, + }) : _points = points, + super(); + + /// Creates a [RampShotBehavior]. + /// + /// This can be used for testing [RampShotBehavior] in isolation. + @visibleForTesting + RampShotBehavior.test({ + required Points points, + required this.subscription, + }) : _points = points, + super(); + + final Points _points; + + /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. + @visibleForTesting + StreamSubscription? subscription; + + @override + void onMount() { + super.onMount(); + + subscription = subscription ?? + parent.bloc.stream.listen((state) { + final achievedOneMillionPoints = state.hits % 10 == 0; + + if (!achievedOneMillionPoints) { + gameRef.read().add(const MultiplierIncreased()); + + parent.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -45), + ), + ); + } + }); + } + + @override + void onRemove() { + subscription?.cancel(); + super.onRemove(); + } +} 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/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/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 9dc81135..132639d4 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,7 +1,6 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -17,7 +16,7 @@ class ControlledBall extends Ball with Controls { /// A [Ball] that launches from the [Plunger]. ControlledBall.launch({ required CharacterTheme characterTheme, - }) : super(baseColor: characterTheme.ballColor) { + }) : super(assetPath: characterTheme.ball.keyName) { controller = BallController(this); layer = Layer.launcher; zIndex = ZIndexes.ballOnLaunchRamp; @@ -28,13 +27,13 @@ class ControlledBall extends Ball with Controls { /// {@endtemplate} ControlledBall.bonus({ required CharacterTheme characterTheme, - }) : super(baseColor: characterTheme.ballColor) { + }) : super(assetPath: characterTheme.ball.keyName) { controller = BallController(this); zIndex = ZIndexes.ballOnBoard; } /// [Ball] used in [DebugPinballGame]. - ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { + ControlledBall.debug() : super() { controller = BallController(this); zIndex = ZIndexes.ballOnBoard; } diff --git a/lib/game/components/dino_desert/dino_desert.dart b/lib/game/components/dino_desert/dino_desert.dart index 9ba9c71b..5f01979f 100644 --- a/lib/game/components/dino_desert/dino_desert.dart +++ b/lib/game/components/dino_desert/dino_desert.dart @@ -36,12 +36,14 @@ class DinoDesert extends Component { } class _BarrierBehindDino extends BodyComponent { + _BarrierBehindDino() : super(renderBody: false); + @override Body createBody() { final shape = EdgeShape() ..set( - Vector2(25, -14.2), - Vector2(25, -7.7), + Vector2(25.3, -14.2), + Vector2(25.3, -7.7), ); return world.createBody(BodyDef())..createFixtureFromShape(shape); diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index edc65329..1299e6eb 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( - score: state?.score ?? 0, + component.descendants().whereType().first.initialsInput( + score: state?.displayScore ?? 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/game_assets.dart b/lib/game/game_assets.dart index 22c1c2d6..d066ce0d 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -13,7 +13,6 @@ extension PinballGameAssetsX on PinballGame { return [ images.load(components.Assets.images.boardBackground.keyName), - images.load(components.Assets.images.ball.ball.keyName), images.load(components.Assets.images.ball.flameEffect.keyName), images.load(components.Assets.images.signpost.inactive.keyName), images.load(components.Assets.images.signpost.active1.keyName), @@ -99,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), @@ -113,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), @@ -137,6 +135,10 @@ extension PinballGameAssetsX on PinballGame { images.load(sparkyTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName), images.load(dinoTheme.leaderboardIcon.keyName), + images.load(androidTheme.ball.keyName), + images.load(dashTheme.ball.keyName), + images.load(dinoTheme.ball.keyName), + images.load(sparkyTheme.ball.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..21b9ac5c 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(), @@ -51,7 +60,6 @@ class PinballGamePage extends StatelessWidget { return MultiBlocProvider( providers: [ - BlocProvider(create: (_) => StartGameBloc(game: game)), BlocProvider(create: (_) => GameBloc()), BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()), ], @@ -96,36 +104,43 @@ class PinballGameLoadedView extends StatelessWidget { @override Widget build(BuildContext context) { + final isPlaying = context.select( + (StartGameBloc bloc) => bloc.state.status == StartGameStatus.play, + ); final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; final screenWidth = MediaQuery.of(context).size.width; final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); - return Stack( - children: [ - Positioned.fill( - child: GameWidget( - game: game, - initialActiveOverlays: const [PinballGame.playButtonOverlay], - overlayBuilderMap: { - PinballGame.playButtonOverlay: (context, game) { - return Positioned( - bottom: 20, - right: 0, - left: 0, - child: PlayButtonOverlay(game: game), - ); + return StartGameListener( + game: game, + child: Stack( + children: [ + Positioned.fill( + child: GameWidget( + game: game, + initialActiveOverlays: const [PinballGame.playButtonOverlay], + overlayBuilderMap: { + PinballGame.playButtonOverlay: (context, game) { + return const Positioned( + bottom: 20, + right: 0, + left: 0, + child: PlayButtonOverlay(), + ); + }, }, - }, + ), ), - ), - // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc - // status - Positioned( - top: 16, - left: leftMargin, - child: const GameHud(), - ), - ], + Positioned( + top: 16, + left: leftMargin, + child: Visibility( + visible: isPlaying, + child: const GameHud(), + ), + ), + ], + ), ); } } diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 605bceb4..b40536aa 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -7,8 +7,8 @@ import 'package:pinball_ui/pinball_ui.dart'; /// {@template game_hud} /// Overlay on the [PinballGame]. /// -/// Displays the current [GameState.score], [GameState.rounds] and animates when -/// the player gets a [GameBonus]. +/// Displays the current [GameState.displayScore], [GameState.rounds] and +/// animates when the player gets a [GameBonus]. /// {@endtemplate} class GameHud extends StatefulWidget { /// {@macro game_hud} diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index 1d4a10fb..7a954c77 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:pinball/game/pinball_game.dart'; +import 'package:flutter_bloc/flutter_bloc.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_ui/pinball_ui.dart'; /// {@template play_button_overlay} @@ -9,13 +9,7 @@ import 'package:pinball_ui/pinball_ui.dart'; /// {@endtemplate} class PlayButtonOverlay extends StatelessWidget { /// {@macro play_button_overlay} - const PlayButtonOverlay({ - Key? key, - required PinballGame game, - }) : _game = game, - super(key: key); - - final PinballGame _game; + const PlayButtonOverlay({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -23,9 +17,8 @@ class PlayButtonOverlay extends StatelessWidget { return PinballButton( text: l10n.play, - onTap: () async { - _game.gameFlowController.start(); - await showCharacterSelectionDialog(context); + onTap: () { + context.read().add(const PlayTapped()); }, ); } diff --git a/lib/game/view/widgets/score_view.dart b/lib/game/view/widgets/score_view.dart index 1fe57eb1..76ab9fa4 100644 --- a/lib/game/view/widgets/score_view.dart +++ b/lib/game/view/widgets/score_view.dart @@ -69,7 +69,7 @@ class _ScoreText extends StatelessWidget { @override Widget build(BuildContext context) { - final score = context.select((GameBloc bloc) => bloc.state.score); + final score = context.select((GameBloc bloc) => bloc.state.displayScore); return Text( score.formatScore(), diff --git a/lib/how_to_play/widgets/how_to_play_dialog.dart b/lib/how_to_play/widgets/how_to_play_dialog.dart index e91698f5..426fcbe5 100644 --- a/lib/how_to_play/widgets/how_to_play_dialog.dart +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -3,10 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/gen/gen.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_ui/pinball_ui.dart'; import 'package:platform_helper/platform_helper.dart'; @@ -51,24 +49,16 @@ extension on Control { } } -Future showHowToPlayDialog(BuildContext context) { - final audio = context.read(); - return showDialog( - context: context, - builder: (_) => HowToPlayDialog(), - ).then((_) { - audio.ioPinballVoiceOver(); - }); -} - class HowToPlayDialog extends StatefulWidget { HowToPlayDialog({ Key? key, + required this.onDismissCallback, @visibleForTesting PlatformHelper? platformHelper, }) : platformHelper = platformHelper ?? PlatformHelper(), super(key: key); final PlatformHelper platformHelper; + final VoidCallback onDismissCallback; @override State createState() => _HowToPlayDialogState(); @@ -82,6 +72,7 @@ class _HowToPlayDialogState extends State { closeTimer = Timer(const Duration(seconds: 3), () { if (mounted) { Navigator.of(context).pop(); + widget.onDismissCallback.call(); } }); } @@ -96,10 +87,17 @@ class _HowToPlayDialogState extends State { Widget build(BuildContext context) { final isMobile = widget.platformHelper.isMobile; final l10n = context.l10n; - return PinballDialog( - title: l10n.howToPlay, - subtitle: l10n.tipsForFlips, - child: isMobile ? const _MobileBody() : const _DesktopBody(), + + return WillPopScope( + onWillPop: () { + widget.onDismissCallback.call(); + return Future.value(true); + }, + child: PinballDialog( + title: l10n.howToPlay, + subtitle: l10n.tipsForFlips, + child: isMobile ? const _MobileBody() : const _DesktopBody(), + ), ); } } 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/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 3b00829b..1f7b0374 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -1,20 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pinball/how_to_play/how_to_play.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_theme/pinball_theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; -/// Inflates [CharacterSelectionDialog] using [showDialog]. -Future showCharacterSelectionDialog(BuildContext context) { - return showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const CharacterSelectionDialog(), - ); -} - /// {@template character_selection_dialog} /// Dialog used to select the playing character of the game. /// {@endtemplate character_selection_dialog} @@ -59,7 +50,7 @@ class _SelectCharacterButton extends StatelessWidget { return PinballButton( onTap: () async { Navigator.of(context).pop(); - await showHowToPlayDialog(context); + context.read().add(const CharacterSelected()); }, text: l10n.select, ); @@ -74,36 +65,40 @@ class _CharacterGrid extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Column( - children: [ - _Character( - key: const Key('sparky_character_selection'), - character: const SparkyTheme(), - isSelected: state.isSparkySelected, - ), - const SizedBox(height: 6), - _Character( - key: const Key('android_character_selection'), - character: const AndroidTheme(), - isSelected: state.isAndroidSelected, - ), - ], + Expanded( + child: Column( + children: [ + _Character( + key: const Key('sparky_character_selection'), + character: const SparkyTheme(), + isSelected: state.isSparkySelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('android_character_selection'), + character: const AndroidTheme(), + isSelected: state.isAndroidSelected, + ), + ], + ), ), const SizedBox(width: 6), - Column( - children: [ - _Character( - key: const Key('dash_character_selection'), - character: const DashTheme(), - isSelected: state.isDashSelected, - ), - const SizedBox(height: 6), - _Character( - key: const Key('dino_character_selection'), - character: const DinoTheme(), - isSelected: state.isDinoSelected, - ), - ], + Expanded( + child: Column( + children: [ + _Character( + key: const Key('dash_character_selection'), + character: const DashTheme(), + isSelected: state.isDashSelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('dino_character_selection'), + character: const DinoTheme(), + isSelected: state.isDinoSelected, + ), + ], + ), ), ], ); diff --git a/lib/start_game/bloc/start_game_bloc.dart b/lib/start_game/bloc/start_game_bloc.dart index ba44d88c..3a96b57b 100644 --- a/lib/start_game/bloc/start_game_bloc.dart +++ b/lib/start_game/bloc/start_game_bloc.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:pinball/game/game.dart'; part 'start_game_event.dart'; part 'start_game_state.dart'; @@ -10,23 +9,16 @@ part 'start_game_state.dart'; /// {@endtemplate} class StartGameBloc extends Bloc { /// {@macro start_game_bloc} - StartGameBloc({ - required PinballGame game, - }) : _game = game, - super(const StartGameState.initial()) { + StartGameBloc() : super(const StartGameState.initial()) { on(_onPlayTapped); on(_onCharacterSelected); on(_onHowToPlayFinished); } - final PinballGame _game; - void _onPlayTapped( PlayTapped event, Emitter emit, ) { - _game.gameFlowController.start(); - emit( state.copyWith( status: StartGameStatus.selectCharacter, diff --git a/lib/start_game/start_game.dart b/lib/start_game/start_game.dart index 7171c66d..9e63b170 100644 --- a/lib/start_game/start_game.dart +++ b/lib/start_game/start_game.dart @@ -1 +1,2 @@ export 'bloc/start_game_bloc.dart'; +export 'widgets/start_game_listener.dart'; diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart new file mode 100644 index 00000000..df34b324 --- /dev/null +++ b/lib/start_game/widgets/start_game_listener.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template start_game_listener} +/// Listener that manages the display of dialogs for [StartGameStatus]. +/// +/// It's responsible for starting the game after pressing play button +/// and playing a sound after the 'how to play' dialog. +/// {@endtemplate} +class StartGameListener extends StatelessWidget { + /// {@macro start_game_listener} + const StartGameListener({ + Key? key, + required Widget child, + required PinballGame game, + }) : _child = child, + _game = game, + super(key: key); + + final Widget _child; + final PinballGame _game; + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + switch (state.status) { + case StartGameStatus.initial: + break; + case StartGameStatus.selectCharacter: + _onSelectCharacter(context); + _game.gameFlowController.start(); + break; + case StartGameStatus.howToPlay: + _onHowToPlay(context); + break; + case StartGameStatus.play: + break; + } + }, + child: _child, + ); + } + + void _onSelectCharacter(BuildContext context) { + _showPinballDialog( + context: context, + child: const CharacterSelectionDialog(), + barrierDismissible: false, + ); + } + + void _onHowToPlay(BuildContext context) { + final audio = context.read(); + + _showPinballDialog( + context: context, + child: HowToPlayDialog( + onDismissCallback: () { + context.read().add(const HowToPlayFinished()); + audio.ioPinballVoiceOver(); + }, + ), + ); + } + + void _showPinballDialog({ + required BuildContext context, + required Widget child, + bool barrierDismissible = true, + }) { + final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; + + showDialog( + context: context, + barrierColor: PinballColors.transparent, + barrierDismissible: barrierDismissible, + builder: (_) { + return Center( + child: SizedBox( + height: gameWidgetWidth * 0.87, + width: gameWidgetWidth, + child: child, + ), + ); + }, + ); + } +} 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/assets/images/ball/ball.png b/packages/pinball_components/assets/images/ball/ball.png deleted file mode 100644 index 43332c9a..00000000 Binary files a/packages/pinball_components/assets/images/ball/ball.png and /dev/null 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/ball/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart index 9234e69c..e8cea997 100644 --- a/packages/pinball_components/lib/src/components/ball/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -2,9 +2,10 @@ import 'dart:async'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; export 'behaviors/behaviors.dart'; @@ -14,11 +15,11 @@ export 'behaviors/behaviors.dart'; class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// {@macro ball} Ball({ - required this.baseColor, + String? assetPath, }) : super( renderBody: false, children: [ - _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), + _BallSpriteComponent(assetPath: assetPath), BallScalingBehavior(), BallGravitatingBehavior(), ], @@ -35,7 +36,7 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// /// This can be used for testing [Ball]'s behaviors in isolation. @visibleForTesting - Ball.test({required this.baseColor}) + Ball.test() : super( children: [_BallSpriteComponent()], ); @@ -43,9 +44,6 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// The size of the [Ball]. static final Vector2 size = Vector2.all(4.13); - /// The base [Color] used to tint this [Ball]. - final Color baseColor; - @override Body createBody() { final shape = CircleShape()..radius = size.x / 2; @@ -79,14 +77,22 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { } class _BallSpriteComponent extends SpriteComponent with HasGameRef { + _BallSpriteComponent({ + this.assetPath, + }) : super( + anchor: Anchor.center, + ); + + final String? assetPath; + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.ball.ball.keyName, + final sprite = Sprite( + gameRef.images + .fromCache(assetPath ?? theme.Assets.images.dash.ball.keyName), ); this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.center; + size = sprite.originalSize / 12.5; } } diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 6a724334..5eef3538 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'; @@ -32,7 +31,7 @@ export 'shapes/shapes.dart'; export 'signpost/signpost.dart'; export 'slingshot.dart'; export 'spaceship_rail.dart'; -export 'spaceship_ramp.dart'; +export 'spaceship_ramp/spaceship_ramp.dart'; export 'sparky_animatronic.dart'; export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_computer.dart'; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart new file mode 100644 index 00000000..1f9b6284 --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart @@ -0,0 +1 @@ +export 'ramp_ball_ascending_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart new file mode 100644 index 00000000..2d0aad7c --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart @@ -0,0 +1,24 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_ball_ascending_contact_behavior} +/// Detects an ascending [Ball] that enters into the [SpaceshipRamp]. +/// +/// The [Ball] can hit with sensor to recognize if a [Ball] goes into or out of +/// the [SpaceshipRamp]. +/// {@endtemplate} +class RampBallAscendingContactBehavior + extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + if (other.body.linearVelocity.y < 0) { + parent.parent.bloc.onAscendingBallEntered(); + } + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart new file mode 100644 index 00000000..d27a7a2c --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart @@ -0,0 +1,16 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'spaceship_ramp_state.dart'; + +class SpaceshipRampCubit extends Cubit { + SpaceshipRampCubit() : super(const SpaceshipRampState.initial()); + + void onAscendingBallEntered() { + emit( + state.copyWith(hits: state.hits + 1), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart new file mode 100644 index 00000000..7fae894f --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart @@ -0,0 +1,24 @@ +// ignore_for_file: public_member_api_docs + +part of 'spaceship_ramp_cubit.dart'; + +class SpaceshipRampState extends Equatable { + const SpaceshipRampState({ + required this.hits, + }) : assert(hits >= 0, "Hits can't be negative"); + + const SpaceshipRampState.initial() : this(hits: 0); + + final int hits; + + SpaceshipRampState copyWith({ + int? hits, + }) { + return SpaceshipRampState( + hits: hits ?? this.hits, + ); + } + + @override + List get props => [hits]; +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart similarity index 77% rename from packages/pinball_components/lib/src/components/spaceship_ramp.dart rename to packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart index c1be0943..0b407517 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart @@ -5,16 +5,35 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'cubit/spaceship_ramp_cubit.dart'; + /// {@template spaceship_ramp} /// Ramp leading into the [AndroidSpaceship]. /// {@endtemplate} class SpaceshipRamp extends Component { /// {@macro spaceship_ramp} - SpaceshipRamp() - : super( + SpaceshipRamp({ + Iterable? children, + }) : this._( + children: children, + bloc: SpaceshipRampCubit(), + ); + + SpaceshipRamp._({ + Iterable? children, + required this.bloc, + }) : super( children: [ + // TODO(ruimiguel): refactor RampScoringSensor and + // _SpaceshipRampOpening to be in only one sensor if possible. + RampScoringSensor( + children: [ + RampBallAscendingContactBehavior(), + ], + )..initialPosition = Vector2(1.7, -20.4), _SpaceshipRampOpening( outsidePriority: ZIndexes.ballOnBoard, rotation: math.pi, @@ -34,60 +53,30 @@ class SpaceshipRamp extends Component { _SpaceshipRampForegroundRailing(), _SpaceshipRampBase()..initialPosition = Vector2(1.7, -20), _SpaceshipRampBackgroundRailingSpriteComponent(), - _SpaceshipRampArrowSpriteComponent(), + SpaceshipRampArrowSpriteComponent( + current: bloc.state.hits, + ), + ...?children, ], ); - /// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. + /// Creates a [SpaceshipRamp] without any children. /// - /// If the current state is the last one it cycles back to the initial state. - void progress() => - firstChild<_SpaceshipRampArrowSpriteComponent>()?.progress(); -} - -/// Indicates the state of the arrow on the [SpaceshipRamp]. -@visibleForTesting -enum SpaceshipRampArrowSpriteState { - /// Arrow with no dashes lit up. - inactive, - - /// Arrow with 1 light lit up. - active1, + /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation. + @visibleForTesting + SpaceshipRamp.test({ + required this.bloc, + }) : super(); - /// Arrow with 2 lights lit up. - active2, - - /// Arrow with 3 lights lit up. - active3, - - /// Arrow with 4 lights lit up. - active4, - - /// Arrow with all 5 lights lit up. - active5, -} - -extension on SpaceshipRampArrowSpriteState { - String get path { - switch (this) { - case SpaceshipRampArrowSpriteState.inactive: - return Assets.images.android.ramp.arrow.inactive.keyName; - case SpaceshipRampArrowSpriteState.active1: - return Assets.images.android.ramp.arrow.active1.keyName; - case SpaceshipRampArrowSpriteState.active2: - return Assets.images.android.ramp.arrow.active2.keyName; - case SpaceshipRampArrowSpriteState.active3: - return Assets.images.android.ramp.arrow.active3.keyName; - case SpaceshipRampArrowSpriteState.active4: - return Assets.images.android.ramp.arrow.active4.keyName; - case SpaceshipRampArrowSpriteState.active5: - return Assets.images.android.ramp.arrow.active5.keyName; - } - } + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final SpaceshipRampCubit bloc; - SpaceshipRampArrowSpriteState get next { - return SpaceshipRampArrowSpriteState - .values[(index + 1) % SpaceshipRampArrowSpriteState.values.length]; + @override + void onRemove() { + bloc.close(); + super.onRemove(); } } @@ -194,37 +183,81 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent /// /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// {@endtemplate} -class _SpaceshipRampArrowSpriteComponent - extends SpriteGroupComponent - with HasGameRef, ZIndex { +@visibleForTesting +class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent + with HasGameRef, ParentIsA, ZIndex { /// {@macro spaceship_ramp_arrow_sprite_component} - _SpaceshipRampArrowSpriteComponent() - : super( + SpaceshipRampArrowSpriteComponent({ + required int current, + }) : super( anchor: Anchor.center, position: Vector2(-3.9, -56.5), + current: current, ) { zIndex = ZIndexes.spaceshipRampArrow; } - /// Changes arrow image to the next [Sprite]. - void progress() => current = current?.next; - @override Future onLoad() async { await super.onLoad(); - final sprites = {}; + parent.bloc.stream.listen((state) { + current = state.hits % SpaceshipRampArrowSpriteState.values.length; + }); + + final sprites = {}; this.sprites = sprites; for (final spriteState in SpaceshipRampArrowSpriteState.values) { - sprites[spriteState] = Sprite( + sprites[spriteState.index] = Sprite( gameRef.images.fromCache(spriteState.path), ); } - current = SpaceshipRampArrowSpriteState.inactive; + current = 0; size = sprites[current]!.originalSize / 10; } } +/// Indicates the state of the arrow on the [SpaceshipRamp]. +@visibleForTesting +enum SpaceshipRampArrowSpriteState { + /// Arrow with no dashes lit up. + inactive, + + /// Arrow with 1 light lit up. + active1, + + /// Arrow with 2 lights lit up. + active2, + + /// Arrow with 3 lights lit up. + active3, + + /// Arrow with 4 lights lit up. + active4, + + /// Arrow with all 5 lights lit up. + active5, +} + +extension on SpaceshipRampArrowSpriteState { + String get path { + switch (this) { + case SpaceshipRampArrowSpriteState.inactive: + return Assets.images.android.ramp.arrow.inactive.keyName; + case SpaceshipRampArrowSpriteState.active1: + return Assets.images.android.ramp.arrow.active1.keyName; + case SpaceshipRampArrowSpriteState.active2: + return Assets.images.android.ramp.arrow.active2.keyName; + case SpaceshipRampArrowSpriteState.active3: + return Assets.images.android.ramp.arrow.active3.keyName; + case SpaceshipRampArrowSpriteState.active4: + return Assets.images.android.ramp.arrow.active4.keyName; + case SpaceshipRampArrowSpriteState.active5: + return Assets.images.android.ramp.arrow.active5.keyName; + } + } +} + class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent with HasGameRef, ZIndex { _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) { @@ -373,3 +406,47 @@ class _SpaceshipRampOpening extends LayerSensor { ); } } + +/// {@template ramp_scoring_sensor} +/// Small sensor body used to detect when a ball has entered the +/// [SpaceshipRamp]. +/// {@endtemplate} +class RampScoringSensor extends BodyComponent + with ParentIsA, InitialPosition, Layered { + /// {@macro ramp_scoring_sensor} + RampScoringSensor({ + Iterable? children, + }) : super( + children: children, + renderBody: false, + ) { + layer = Layer.spaceshipEntranceRamp; + } + + /// Creates a [RampScoringSensor] without any children. + /// + @visibleForTesting + RampScoringSensor.test(); + + @override + Body createBody() { + final shape = PolygonShape() + ..setAsBox( + 2.6, + .5, + initialPosition, + -5 * math.pi / 180, + ); + + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} 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/common/games.dart b/packages/pinball_components/sandbox/lib/common/games.dart index 89d16450..bee6a280 100644 --- a/packages/pinball_components/sandbox/lib/common/games.dart +++ b/packages/pinball_components/sandbox/lib/common/games.dart @@ -24,6 +24,14 @@ abstract class AssetsGame extends Forge2DGame { } abstract class LineGame extends AssetsGame with PanDetector { + LineGame({ + List? imagesFileNames, + }) : super( + imagesFileNames: [ + if (imagesFileNames != null) ...imagesFileNames, + ], + ); + Vector2? _lineEnd; @override 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/android_acres/android_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart index 32638c2d..78cebd95 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart @@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; class AndroidBumperAGame extends BallGame { AndroidBumperAGame() : super( - color: const Color(0xFF0000FF), imagesFileNames: [ Assets.images.android.bumper.a.lit.keyName, Assets.images.android.bumper.a.dimmed.keyName, diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart index bfd4206c..9bd2caff 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart @@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; class AndroidBumperBGame extends BallGame { AndroidBumperBGame() : super( - color: const Color(0xFF0000FF), imagesFileNames: [ Assets.images.android.bumper.b.lit.keyName, Assets.images.android.bumper.b.dimmed.keyName, diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart index dee83e26..4093ad33 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart @@ -1,14 +1,12 @@ import 'dart:async'; import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRailGame extends BallGame { SpaceshipRailGame() : super( - color: Colors.blue, ballPriority: ZIndexes.ballOnSpaceshipRail, ballLayer: Layer.spaceshipExitRail, imagesFileNames: [ diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart index cabe4d54..fe4e6dae 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart @@ -9,7 +9,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRampGame extends BallGame with KeyboardEvents { SpaceshipRampGame() : super( - color: Colors.blue, ballPriority: ZIndexes.ballOnSpaceshipRamp, ballLayer: Layer.spaceshipEntranceRamp, imagesFileNames: [ @@ -54,7 +53,7 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { ) { if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.space) { - _spaceshipRamp.progress(); + _spaceshipRamp.bloc.onAscendingBallEntered(); return KeyEventResult.handled; } 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 423dde98..00000000 --- a/packages/pinball_components/sandbox/lib/stories/backboard/backboard_game_over_game.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flame/effects.dart'; -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), - effectController: EffectController(duration: 1), - ), - ); - }, - ), - ); - } -} 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/ball/ball_booster_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart index a66459a6..ac0989e2 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/ball_booster_game.dart @@ -1,9 +1,20 @@ import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:sandbox/common/common.dart'; class BallBoosterGame extends LineGame { + BallBoosterGame() + : super( + imagesFileNames: [ + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, + Assets.images.ball.flameEffect.keyName, + ], + ); + static const description = ''' Shows how a Ball with a boost works. @@ -12,7 +23,7 @@ class BallBoosterGame extends LineGame { @override void onLine(Vector2 line) { - final ball = Ball(baseColor: Colors.transparent); + final ball = Ball(); final impulse = line * -1 * 20; ball.add(BallTurboChargingBehavior(impulse: impulse)); diff --git a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart index e57a0322..f3ba50f3 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart @@ -1,17 +1,20 @@ import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:sandbox/common/common.dart'; class BallGame extends AssetsGame with TapDetector, Traceable { BallGame({ - this.color = Colors.blue, this.ballPriority = 0, this.ballLayer = Layer.all, + this.character, List? imagesFileNames, }) : super( imagesFileNames: [ - Assets.images.ball.ball.keyName, + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, if (imagesFileNames != null) ...imagesFileNames, ], ); @@ -22,14 +25,23 @@ class BallGame extends AssetsGame with TapDetector, Traceable { - Tap anywhere on the screen to spawn a ball into the game. '''; - final Color color; + static final characterBallPaths = { + 'Dash': theme.Assets.images.dash.ball.keyName, + 'Sparky': theme.Assets.images.sparky.ball.keyName, + 'Android': theme.Assets.images.android.ball.keyName, + 'Dino': theme.Assets.images.dino.ball.keyName, + }; + final int ballPriority; final Layer ballLayer; + final String? character; @override void onTapUp(TapUpInfo info) { add( - Ball(baseColor: color) + Ball( + assetPath: characterBallPaths[character], + ) ..initialPosition = info.eventPosition.game ..layer = ballLayer ..priority = ballPriority, diff --git a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart index eb472282..146ebcda 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart @@ -1,5 +1,4 @@ import 'package:dashbook/dashbook.dart'; -import 'package:flutter/material.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/ball_booster_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; @@ -7,10 +6,14 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart'; void addBallStories(Dashbook dashbook) { dashbook.storiesOf('Ball') ..addGame( - title: 'Colored', + title: 'Themed', description: BallGame.description, gameBuilder: (context) => BallGame( - color: context.colorProperty('color', Colors.blue), + character: context.listProperty( + 'Character', + BallGame.characterBallPaths.keys.first, + BallGame.characterBallPaths.keys.toList(), + ), ), ) ..addGame( diff --git a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart index bc537de2..94389f60 100644 --- a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart @@ -1,14 +1,10 @@ -import 'dart:ui'; - import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class GoogleLetterGame extends BallGame { GoogleLetterGame() : super( - color: const Color(0xFF009900), imagesFileNames: [ Assets.images.googleWord.letter1.lit.keyName, Assets.images.googleWord.letter1.dimmed.keyName, diff --git a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart index ea3bd4db..b6955a26 100644 --- a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart @@ -1,14 +1,12 @@ import 'dart:async'; import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class LaunchRampGame extends BallGame { LaunchRampGame() : super( - color: Colors.blue, ballPriority: ZIndexes.ballOnLaunchRamp, ballLayer: Layer.launcher, ); diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart index 0f1ec2e4..0ee58cc9 100644 --- a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -6,8 +6,6 @@ import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class PlungerGame extends BallGame with KeyboardEvents, Traceable { - PlungerGame() : super(color: const Color(0xFFFF0000)); - static const description = ''' Shows how Plunger is rendered. 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/ball/ball_test.dart b/packages/pinball_components/test/src/components/ball/ball_test.dart index 655836a0..9195e0b2 100644 --- a/packages/pinball_components/test/src/components/ball/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball/ball_test.dart @@ -2,31 +2,36 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, + ]; - group('Ball', () { - const baseColor = Color(0xFFFFFFFF); + final flameTester = FlameTester(() => TestGame(assets)); + group('Ball', () { test( 'can be instantiated', () { - expect(Ball(baseColor: baseColor), isA()); - expect(Ball.test(baseColor: baseColor), isA()); + expect(Ball(), isA()); + expect(Ball.test(), isA()); }, ); flameTester.test( 'loads correctly', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ready(); await game.ensureAdd(ball); @@ -36,7 +41,7 @@ void main() { group('adds', () { flameTester.test('a BallScalingBehavior', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect( ball.descendants().whereType().length, @@ -45,7 +50,7 @@ void main() { }); flameTester.test('a BallGravitatingBehavior', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect( ball.descendants().whereType().length, @@ -58,7 +63,7 @@ void main() { flameTester.test( 'is dynamic', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect(ball.body.bodyType, equals(BodyType.dynamic)); @@ -67,7 +72,7 @@ void main() { group('can be moved', () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); game.update(1); @@ -75,7 +80,7 @@ void main() { }); flameTester.test('by applying velocity', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.body.gravityScale = Vector2.zero(); @@ -90,7 +95,7 @@ void main() { flameTester.test( 'exists', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); expect(ball.body.fixtures[0], isA()); @@ -100,7 +105,7 @@ void main() { flameTester.test( 'is dense', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -111,7 +116,7 @@ void main() { flameTester.test( 'shape is circular', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -123,7 +128,7 @@ void main() { flameTester.test( 'has Layer.all as default filter maskBits', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ready(); await game.ensureAdd(ball); await game.ready(); @@ -137,7 +142,7 @@ void main() { group('stop', () { group("can't be moved", () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.stop(); @@ -152,7 +157,7 @@ void main() { flameTester.test( 'by its weight when previously stopped', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.stop(); ball.resume(); @@ -165,7 +170,7 @@ void main() { flameTester.test( 'by applying velocity when previously stopped', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); ball.stop(); ball.resume(); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart index d78df37a..ce193dc8 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_gravitating_behavior_test.dart @@ -1,21 +1,19 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final asset = Assets.images.ball.ball.keyName; + final asset = theme.Assets.images.dash.ball.keyName; final flameTester = FlameTester(() => TestGame([asset])); group('BallGravitatingBehavior', () { - const baseColor = Color(0xFFFFFFFF); test('can be instantiated', () { expect( BallGravitatingBehavior(), @@ -24,7 +22,7 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); final behavior = BallGravitatingBehavior(); await ball.add(behavior); await game.ensureAdd(ball); @@ -37,12 +35,10 @@ void main() { flameTester.test( "overrides the body's horizontal gravity symmetrically", (game) async { - final ball1 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(10, 0); + final ball1 = Ball.test()..initialPosition = Vector2(10, 0); await ball1.add(BallGravitatingBehavior()); - final ball2 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(-10, 0); + final ball2 = Ball.test()..initialPosition = Vector2(-10, 0); await ball2.add(BallGravitatingBehavior()); await game.ensureAddAll([ball1, ball2]); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart index 0aeeda98..bd0cca49 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -1,21 +1,19 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final asset = Assets.images.ball.ball.keyName; + final asset = theme.Assets.images.dash.ball.keyName; final flameTester = FlameTester(() => TestGame([asset])); group('BallScalingBehavior', () { - const baseColor = Color(0xFFFFFFFF); test('can be instantiated', () { expect( BallScalingBehavior(), @@ -24,7 +22,7 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); final behavior = BallScalingBehavior(); await ball.add(behavior); await game.ensureAdd(ball); @@ -35,12 +33,10 @@ void main() { }); flameTester.test('scales the shape radius', (game) async { - final ball1 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, 10); + final ball1 = Ball.test()..initialPosition = Vector2(0, 10); await ball1.add(BallScalingBehavior()); - final ball2 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, -10); + final ball2 = Ball.test()..initialPosition = Vector2(0, -10); await ball2.add(BallScalingBehavior()); await game.ensureAddAll([ball1, ball2]); @@ -57,12 +53,10 @@ void main() { flameTester.test( 'scales the sprite', (game) async { - final ball1 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, 10); + final ball1 = Ball.test()..initialPosition = Vector2(0, 10); await ball1.add(BallScalingBehavior()); - final ball2 = Ball.test(baseColor: baseColor) - ..initialPosition = Vector2(0, -10); + final ball2 = Ball.test()..initialPosition = Vector2(0, -10); await ball2.add(BallScalingBehavior()); await game.ensureAddAll([ball1, ball2]); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart index 00f34832..79eb030e 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_turbo_charging_behavior_test.dart @@ -1,12 +1,10 @@ // ignore_for_file: cascade_invocations -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; @@ -16,9 +14,8 @@ void main() { group( 'BallTurboChargingBehavior', () { - final assets = [Assets.images.ball.ball.keyName]; - final flameTester = FlameTester(() => TestGame(assets)); - const baseColor = Color(0xFFFFFFFF); + final asset = theme.Assets.images.dash.ball.keyName; + final flameTester = FlameTester(() => TestGame([asset])); test('can be instantiated', () { expect( @@ -28,7 +25,7 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); await ball.add(behavior); await game.ensureAdd(ball); @@ -41,7 +38,7 @@ void main() { flameTester.test( 'impulses the ball velocity when loaded', (game) async { - final ball = Ball.test(baseColor: baseColor); + final ball = Ball.test(); await game.ensureAdd(ball); final impulse = Vector2.all(1); final behavior = BallTurboChargingBehavior(impulse: impulse); @@ -59,7 +56,7 @@ void main() { ); flameTester.test('adds sprite', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); await ball.ensureAdd( @@ -73,7 +70,7 @@ void main() { }); flameTester.test('removes sprite after it finishes', (game) async { - final ball = Ball(baseColor: baseColor); + final ball = Ball(); await game.ensureAdd(ball); final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart index 8d052fab..dfc33967 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart @@ -4,11 +4,11 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; @@ -20,7 +20,10 @@ class _MockFixture extends Mock implements Fixture {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + theme.Assets.images.dash.ball.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group( 'ChromeDinoChompingBehavior', @@ -35,7 +38,7 @@ void main() { flameTester.test( 'beginContact sets ball sprite to be invisible and calls onChomp', (game) async { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); final behavior = ChromeDinoChompingBehavior(); final bloc = _MockChromeDinoCubit(); whenListen( diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart index 1d0a55b4..8c2cbe57 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart @@ -5,11 +5,11 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; @@ -17,7 +17,10 @@ class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + theme.Assets.images.dash.ball.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group( 'ChromeDinoSpittingBehavior', @@ -33,7 +36,7 @@ void main() { flameTester.test( 'sets ball sprite to visible and sets a linear velocity', (game) async { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); final behavior = ChromeDinoSpittingBehavior(); final bloc = _MockChromeDinoCubit(); final streamController = StreamController(); @@ -71,7 +74,7 @@ void main() { flameTester.test( 'calls onSpit', (game) async { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); final behavior = ChromeDinoSpittingBehavior(); final bloc = _MockChromeDinoCubit(); final streamController = StreamController(); diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart index 5b31be74..79375a6e 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart @@ -1,5 +1,4 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -7,7 +6,7 @@ void main() { group( 'ChromeDinoCubit', () { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); blocTest( 'onOpenMouth emits true', diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart index d067674b..442d544b 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: prefer_const_constructors -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -61,7 +60,7 @@ void main() { 'copies correctly ' 'when all arguments specified', () { - final ball = Ball(baseColor: Colors.red); + final ball = Ball(); const chromeDinoState = ChromeDinoState( status: ChromeDinoStatus.chomping, isMouthOpen: true, diff --git a/packages/pinball_components/test/src/components/flipper_test.dart b/packages/pinball_components/test/src/components/flipper_test.dart index c34d0d1c..314b1f77 100644 --- a/packages/pinball_components/test/src/components/flipper_test.dart +++ b/packages/pinball_components/test/src/components/flipper_test.dart @@ -2,9 +2,9 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../helpers/helpers.dart'; @@ -13,6 +13,7 @@ void main() { final assets = [ Assets.images.flipper.left.keyName, Assets.images.flipper.right.keyName, + theme.Assets.images.dash.ball.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); @@ -89,7 +90,7 @@ void main() { 'has greater mass than Ball', (game) async { final flipper = Flipper(side: BoardSide.left); - final ball = Ball(baseColor: Colors.white); + final ball = Ball(); await game.ready(); await game.ensureAddAll([flipper, ball]); 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/golden/ball/android.png b/packages/pinball_components/test/src/components/golden/ball/android.png new file mode 100644 index 00000000..2a659092 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/ball/android.png differ diff --git a/packages/pinball_components/test/src/components/golden/ball/dash.png b/packages/pinball_components/test/src/components/golden/ball/dash.png new file mode 100644 index 00000000..c95afc88 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/ball/dash.png differ diff --git a/packages/pinball_components/test/src/components/golden/ball/dino.png b/packages/pinball_components/test/src/components/golden/ball/dino.png new file mode 100644 index 00000000..8ea10758 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/ball/dino.png differ diff --git a/packages/pinball_components/test/src/components/golden/ball/sparky.png b/packages/pinball_components/test/src/components/golden/ball/sparky.png new file mode 100644 index 00000000..afdeb263 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/ball/sparky.png differ diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart new file mode 100644 index 00000000..ea37550a --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart @@ -0,0 +1,117 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _MockBall extends Mock implements Ball {} + +class _MockBody extends Mock implements Body {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + ]; + + final flameTester = FlameTester(() => TestGame(assets)); + + group( + 'RampBallAscendingContactBehavior', + () { + test('can be instantiated', () { + expect( + RampBallAscendingContactBehavior(), + isA(), + ); + }); + + group('beginContact', () { + late Ball ball; + late Body body; + + setUp(() { + ball = _MockBall(); + body = _MockBody(); + + when(() => ball.body).thenReturn(body); + }); + + flameTester.test( + "calls 'onAscendingBallEntered' when a ball enters into the ramp", + (game) async { + final behavior = RampBallAscendingContactBehavior(); + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SpaceshipRampState.initial(), + ); + + final rampSensor = RampScoringSensor.test(); + final spaceshipRamp = SpaceshipRamp.test( + bloc: bloc, + ); + + when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); + + await spaceshipRamp.add(rampSensor); + await game.ensureAddAll([spaceshipRamp, ball]); + await rampSensor.add(behavior); + + behavior.beginContact(ball, _MockContact()); + + verify(bloc.onAscendingBallEntered).called(1); + }, + ); + + flameTester.test( + "doesn't call 'onAscendingBallEntered' when a ball goes out the ramp", + (game) async { + final behavior = RampBallAscendingContactBehavior(); + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SpaceshipRampState.initial(), + ); + + final rampSensor = RampScoringSensor.test(); + final spaceshipRamp = SpaceshipRamp.test( + bloc: bloc, + ); + + when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); + + await spaceshipRamp.add(rampSensor); + await game.ensureAddAll([spaceshipRamp, ball]); + await rampSensor.add(behavior); + + behavior.beginContact(ball, _MockContact()); + + verifyNever(bloc.onAscendingBallEntered); + }, + ); + }); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart new file mode 100644 index 00000000..b7e899fe --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart @@ -0,0 +1,25 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('SpaceshipRampCubit', () { + group('onAscendingBallEntered', () { + blocTest( + 'emits hits incremented and arrow goes to the next value', + build: SpaceshipRampCubit.new, + act: (bloc) => bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(), + expect: () => [ + SpaceshipRampState(hits: 1), + SpaceshipRampState(hits: 2), + SpaceshipRampState(hits: 3), + ], + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart new file mode 100644 index 00000000..536f4e8e --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart @@ -0,0 +1,78 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/components/components.dart'; + +void main() { + group('SpaceshipRampState', () { + test('supports value equality', () { + expect( + SpaceshipRampState(hits: 0), + equals( + SpaceshipRampState(hits: 0), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + SpaceshipRampState(hits: 0), + isNotNull, + ); + }); + }); + + test( + 'throws AssertionError ' + 'when hits is negative', + () { + expect( + () => SpaceshipRampState(hits: -1), + throwsAssertionError, + ); + }, + ); + + group('copyWith', () { + test( + 'throws AssertionError ' + 'when hits is decreased', + () { + const rampState = SpaceshipRampState(hits: 0); + expect( + () => rampState.copyWith(hits: rampState.hits - 1), + throwsAssertionError, + ); + }, + ); + + test( + 'copies correctly ' + 'when no argument specified', + () { + const rampState = SpaceshipRampState(hits: 0); + expect( + rampState.copyWith(), + equals(rampState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + const rampState = SpaceshipRampState(hits: 0); + final otherRampState = SpaceshipRampState(hits: rampState.hits + 1); + expect(rampState, isNot(equals(otherRampState))); + + expect( + rampState.copyWith(hits: rampState.hits + 1), + equals(otherRampState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart similarity index 53% rename from packages/pinball_components/test/src/components/spaceship_ramp_test.dart rename to packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart index 0f2ce13a..b74cfb88 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart @@ -1,12 +1,16 @@ // ignore_for_file: cascade_invocations +import 'package:bloc_test/bloc_test.dart'; 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_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -25,28 +29,35 @@ void main() { final flameTester = FlameTester(() => TestGame(assets)); group('SpaceshipRamp', () { - flameTester.test('loads correctly', (game) async { - final component = SpaceshipRamp(); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); - }); + flameTester.test( + 'loads correctly', + (game) async { + final spaceshipRamp = SpaceshipRamp(); + await game.ensureAdd(spaceshipRamp); + expect(game.children, contains(spaceshipRamp)); + }, + ); group('renders correctly', () { - const goldenFilePath = 'golden/spaceship_ramp/'; + const goldenFilePath = '../golden/spaceship_ramp/'; final centerForSpaceshipRamp = Vector2(-13, -55); flameTester.testGameWidget( 'inactive sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.inactive, ); @@ -64,15 +75,21 @@ void main() { 'active1 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component.progress(); + ramp.bloc.onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active1, ); @@ -90,17 +107,23 @@ void main() { 'active2 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active2, ); @@ -118,18 +141,24 @@ void main() { 'active3 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active3, ); @@ -147,19 +176,25 @@ void main() { 'active4 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress() - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active4, ); @@ -177,20 +212,26 @@ void main() { 'active5 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final component = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [component]); + final ramp = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [ramp]); await game.ensureAdd(canvas); - component - ..progress() - ..progress() - ..progress() - ..progress() - ..progress(); + ramp.bloc + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered() + ..onAscendingBallEntered(); + + await game.ready(); await tester.pump(); + final index = ramp.children + .whereType() + .first + .current; expect( - component.children.whereType().first.current, + SpaceshipRampArrowSpriteState.values[index!], SpaceshipRampArrowSpriteState.active5, ); @@ -204,5 +245,34 @@ void main() { }, ); }); + + flameTester.test('closes bloc when removed', (game) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final ramp = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ramp); + game.remove(ramp); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final ramp = SpaceshipRamp(children: [component]); + await game.ensureAdd(ramp); + expect(ramp.children, contains(component)); + }); + }); }); } 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_theme/assets/images/android/ball.png b/packages/pinball_theme/assets/images/android/ball.png new file mode 100644 index 00000000..b5cfbc3f Binary files /dev/null and b/packages/pinball_theme/assets/images/android/ball.png differ diff --git a/packages/pinball_theme/assets/images/dash/ball.png b/packages/pinball_theme/assets/images/dash/ball.png new file mode 100644 index 00000000..fa754cbc Binary files /dev/null and b/packages/pinball_theme/assets/images/dash/ball.png differ diff --git a/packages/pinball_theme/assets/images/dino/ball.png b/packages/pinball_theme/assets/images/dino/ball.png new file mode 100644 index 00000000..02b99c43 Binary files /dev/null and b/packages/pinball_theme/assets/images/dino/ball.png differ diff --git a/packages/pinball_theme/assets/images/sparky/ball.png b/packages/pinball_theme/assets/images/sparky/ball.png new file mode 100644 index 00000000..95e5a10b Binary files /dev/null and b/packages/pinball_theme/assets/images/sparky/ball.png differ diff --git a/packages/pinball_theme/lib/src/generated/assets.gen.dart b/packages/pinball_theme/lib/src/generated/assets.gen.dart index 3feeecce..545f514b 100644 --- a/packages/pinball_theme/lib/src/generated/assets.gen.dart +++ b/packages/pinball_theme/lib/src/generated/assets.gen.dart @@ -36,9 +36,9 @@ class $AssetsImagesAndroidGen { AssetGenImage get background => const AssetGenImage('assets/images/android/background.png'); - /// File path: assets/images/android/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/android/character.png'); + /// File path: assets/images/android/ball.png + AssetGenImage get ball => + const AssetGenImage('assets/images/android/ball.png'); /// File path: assets/images/android/icon.png AssetGenImage get icon => @@ -60,9 +60,8 @@ class $AssetsImagesDashGen { AssetGenImage get background => const AssetGenImage('assets/images/dash/background.png'); - /// File path: assets/images/dash/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/dash/character.png'); + /// File path: assets/images/dash/ball.png + AssetGenImage get ball => const AssetGenImage('assets/images/dash/ball.png'); /// File path: assets/images/dash/icon.png AssetGenImage get icon => const AssetGenImage('assets/images/dash/icon.png'); @@ -83,9 +82,8 @@ class $AssetsImagesDinoGen { AssetGenImage get background => const AssetGenImage('assets/images/dino/background.png'); - /// File path: assets/images/dino/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/dino/character.png'); + /// File path: assets/images/dino/ball.png + AssetGenImage get ball => const AssetGenImage('assets/images/dino/ball.png'); /// File path: assets/images/dino/icon.png AssetGenImage get icon => const AssetGenImage('assets/images/dino/icon.png'); @@ -106,9 +104,9 @@ class $AssetsImagesSparkyGen { AssetGenImage get background => const AssetGenImage('assets/images/sparky/background.png'); - /// File path: assets/images/sparky/character.png - AssetGenImage get character => - const AssetGenImage('assets/images/sparky/character.png'); + /// File path: assets/images/sparky/ball.png + AssetGenImage get ball => + const AssetGenImage('assets/images/sparky/ball.png'); /// File path: assets/images/sparky/icon.png AssetGenImage get icon => diff --git a/packages/pinball_theme/lib/src/themes/android_theme.dart b/packages/pinball_theme/lib/src/themes/android_theme.dart index 8989c717..6e7d76b2 100644 --- a/packages/pinball_theme/lib/src/themes/android_theme.dart +++ b/packages/pinball_theme/lib/src/themes/android_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template android_theme} @@ -12,7 +11,7 @@ class AndroidTheme extends CharacterTheme { String get name => 'Android'; @override - Color get ballColor => Colors.green; + AssetGenImage get ball => Assets.images.android.ball; @override AssetGenImage get background => Assets.images.android.background; diff --git a/packages/pinball_theme/lib/src/themes/character_theme.dart b/packages/pinball_theme/lib/src/themes/character_theme.dart index 072c917f..596f41a0 100644 --- a/packages/pinball_theme/lib/src/themes/character_theme.dart +++ b/packages/pinball_theme/lib/src/themes/character_theme.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template character_theme} @@ -15,8 +14,8 @@ abstract class CharacterTheme extends Equatable { /// Name of character. String get name; - /// Ball color for this theme. - Color get ballColor; + /// Asset for the ball. + AssetGenImage get ball; /// Asset for the background. AssetGenImage get background; @@ -33,7 +32,7 @@ abstract class CharacterTheme extends Equatable { @override List get props => [ name, - ballColor, + ball, background, icon, leaderboardIcon, diff --git a/packages/pinball_theme/lib/src/themes/dash_theme.dart b/packages/pinball_theme/lib/src/themes/dash_theme.dart index 7584c8ed..be3a8873 100644 --- a/packages/pinball_theme/lib/src/themes/dash_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dash_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dash_theme} @@ -12,7 +11,7 @@ class DashTheme extends CharacterTheme { String get name => 'Dash'; @override - Color get ballColor => Colors.blue; + AssetGenImage get ball => Assets.images.dash.ball; @override AssetGenImage get background => Assets.images.dash.background; diff --git a/packages/pinball_theme/lib/src/themes/dino_theme.dart b/packages/pinball_theme/lib/src/themes/dino_theme.dart index 3baf466c..1de42d41 100644 --- a/packages/pinball_theme/lib/src/themes/dino_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dino_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template dino_theme} @@ -12,7 +11,7 @@ class DinoTheme extends CharacterTheme { String get name => 'Dino'; @override - Color get ballColor => Colors.grey; + AssetGenImage get ball => Assets.images.dino.ball; @override AssetGenImage get background => Assets.images.dino.background; diff --git a/packages/pinball_theme/lib/src/themes/sparky_theme.dart b/packages/pinball_theme/lib/src/themes/sparky_theme.dart index 7884a22f..1699f3ae 100644 --- a/packages/pinball_theme/lib/src/themes/sparky_theme.dart +++ b/packages/pinball_theme/lib/src/themes/sparky_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:pinball_theme/pinball_theme.dart'; /// {@template sparky_theme} @@ -9,7 +8,7 @@ class SparkyTheme extends CharacterTheme { const SparkyTheme(); @override - Color get ballColor => Colors.orange; + AssetGenImage get ball => Assets.images.sparky.ball; @override String get name => 'Sparky'; 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 3e710641..47903d8a 100644 --- a/test/game/behaviors/scoring_behavior_test.dart +++ b/test/game/behaviors/scoring_behavior_test.dart @@ -52,7 +52,8 @@ void main() { blocBuilder: () { bloc = _MockGameBloc(); const state = GameState( - score: 0, + totalScore: 0, + roundScore: 0, multiplier: 1, rounds: 3, bonusHistory: [], diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 3711105e..5927291b 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -6,7 +6,7 @@ void main() { group('GameBloc', () { test('initial state has 3 rounds and empty score', () { final gameBloc = GameBloc(); - expect(gameBloc.state.score, equals(0)); + expect(gameBloc.state.roundScore, equals(0)); expect(gameBloc.state.rounds, equals(3)); }); @@ -19,21 +19,17 @@ void main() { bloc.add(const RoundLost()); }, expect: () => [ - const GameState( - score: 0, - multiplier: 1, - rounds: 2, - bonusHistory: [], - ), + isA()..having((state) => state.rounds, 'rounds', 2), ], ); blocTest( - 'apply multiplier to score ' + 'apply multiplier to roundScore and add it to totalScore ' 'when round is lost', build: GameBloc.new, seed: () => const GameState( - score: 5, + totalScore: 10, + roundScore: 5, multiplier: 3, rounds: 2, bonusHistory: [], @@ -43,8 +39,8 @@ void main() { }, expect: () => [ isA() - ..having((state) => state.score, 'score', 15) - ..having((state) => state.rounds, 'rounds', 1), + ..having((state) => state.totalScore, 'totalScore', 25) + ..having((state) => state.roundScore, 'roundScore', 0) ], ); @@ -53,7 +49,8 @@ void main() { 'when round is lost', build: GameBloc.new, seed: () => const GameState( - score: 5, + totalScore: 10, + roundScore: 5, multiplier: 3, rounds: 2, bonusHistory: [], @@ -62,9 +59,7 @@ void main() { bloc.add(const RoundLost()); }, expect: () => [ - isA() - ..having((state) => state.multiplier, 'multiplier', 1) - ..having((state) => state.rounds, 'rounds', 1), + isA()..having((state) => state.multiplier, 'multiplier', 1) ], ); }); @@ -79,10 +74,10 @@ void main() { ..add(const Scored(points: 3)), expect: () => [ isA() - ..having((state) => state.score, 'score', 2) + ..having((state) => state.roundScore, 'roundScore', 2) ..having((state) => state.isGameOver, 'isGameOver', false), isA() - ..having((state) => state.score, 'score', 5) + ..having((state) => state.roundScore, 'roundScore', 5) ..having((state) => state.isGameOver, 'isGameOver', false), ], ); @@ -99,15 +94,15 @@ void main() { }, expect: () => [ isA() - ..having((state) => state.score, 'score', 0) + ..having((state) => state.roundScore, 'roundScore', 0) ..having((state) => state.rounds, 'rounds', 2) ..having((state) => state.isGameOver, 'isGameOver', false), isA() - ..having((state) => state.score, 'score', 0) + ..having((state) => state.roundScore, 'roundScore', 0) ..having((state) => state.rounds, 'rounds', 1) ..having((state) => state.isGameOver, 'isGameOver', false), isA() - ..having((state) => state.score, 'score', 0) + ..having((state) => state.roundScore, 'roundScore', 0) ..having((state) => state.rounds, 'rounds', 0) ..having((state) => state.isGameOver, 'isGameOver', true), ], @@ -124,11 +119,9 @@ void main() { ..add(const MultiplierIncreased()), expect: () => [ isA() - ..having((state) => state.score, 'score', 0) ..having((state) => state.multiplier, 'multiplier', 2) ..having((state) => state.isGameOver, 'isGameOver', false), isA() - ..having((state) => state.score, 'score', 0) ..having((state) => state.multiplier, 'multiplier', 3) ..having((state) => state.isGameOver, 'isGameOver', false), ], @@ -139,7 +132,8 @@ void main() { 'when multiplier is 6 and game is not over', build: GameBloc.new, seed: () => const GameState( - score: 0, + totalScore: 10, + roundScore: 0, multiplier: 6, rounds: 3, bonusHistory: [], @@ -160,15 +154,12 @@ void main() { }, expect: () => [ isA() - ..having((state) => state.score, 'score', 0) ..having((state) => state.multiplier, 'multiplier', 1) ..having((state) => state.isGameOver, 'isGameOver', false), isA() - ..having((state) => state.score, 'score', 0) ..having((state) => state.multiplier, 'multiplier', 1) ..having((state) => state.isGameOver, 'isGameOver', false), isA() - ..having((state) => state.score, 'score', 0) ..having((state) => state.multiplier, 'multiplier', 1) ..having((state) => state.isGameOver, 'isGameOver', true), ], diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index add25e05..b59115a3 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -8,14 +8,16 @@ void main() { test('supports value equality', () { expect( GameState( - score: 0, + totalScore: 0, + roundScore: 0, multiplier: 1, rounds: 3, bonusHistory: const [], ), equals( const GameState( - score: 0, + totalScore: 0, + roundScore: 0, multiplier: 1, rounds: 3, bonusHistory: [], @@ -28,7 +30,8 @@ void main() { test('can be instantiated', () { expect( const GameState( - score: 0, + totalScore: 0, + roundScore: 0, multiplier: 1, rounds: 3, bonusHistory: [], @@ -40,11 +43,29 @@ void main() { test( 'throws AssertionError ' - 'when score is negative', + 'when totalScore is negative', () { expect( () => GameState( - score: -1, + totalScore: -1, + roundScore: 0, + multiplier: 1, + rounds: 3, + bonusHistory: const [], + ), + throwsAssertionError, + ); + }, + ); + + test( + 'throws AssertionError ' + 'when roundScore is negative', + () { + expect( + () => GameState( + totalScore: 0, + roundScore: -1, multiplier: 1, rounds: 3, bonusHistory: const [], @@ -60,7 +81,8 @@ void main() { () { expect( () => GameState( - score: 1, + totalScore: 0, + roundScore: 1, multiplier: 0, rounds: 3, bonusHistory: const [], @@ -76,7 +98,8 @@ void main() { () { expect( () => GameState( - score: 1, + totalScore: 0, + roundScore: 1, multiplier: 1, rounds: -1, bonusHistory: const [], @@ -91,7 +114,8 @@ void main() { 'is true ' 'when no rounds are left', () { const gameState = GameState( - score: 0, + totalScore: 0, + roundScore: 0, multiplier: 1, rounds: 0, bonusHistory: [], @@ -103,7 +127,8 @@ void main() { 'is false ' 'when one 1 round left', () { const gameState = GameState( - score: 0, + totalScore: 0, + roundScore: 0, multiplier: 1, rounds: 1, bonusHistory: [], @@ -115,16 +140,17 @@ void main() { group('copyWith', () { test( 'throws AssertionError ' - 'when scored is decreased', + 'when totalScore is decreased', () { const gameState = GameState( - score: 2, + totalScore: 2, + roundScore: 2, multiplier: 1, rounds: 3, bonusHistory: [], ); expect( - () => gameState.copyWith(score: gameState.score - 1), + () => gameState.copyWith(totalScore: gameState.totalScore - 1), throwsAssertionError, ); }, @@ -135,7 +161,8 @@ void main() { 'when no argument specified', () { const gameState = GameState( - score: 2, + totalScore: 0, + roundScore: 2, multiplier: 1, rounds: 3, bonusHistory: [], @@ -152,13 +179,15 @@ void main() { 'when all arguments specified', () { const gameState = GameState( - score: 2, + totalScore: 0, + roundScore: 2, multiplier: 1, rounds: 3, bonusHistory: [], ); final otherGameState = GameState( - score: gameState.score + 1, + totalScore: gameState.totalScore + 1, + roundScore: gameState.roundScore + 1, multiplier: gameState.multiplier + 1, rounds: gameState.rounds + 1, bonusHistory: const [GameBonus.googleWord], @@ -167,7 +196,8 @@ void main() { expect( gameState.copyWith( - score: otherGameState.score, + totalScore: otherGameState.totalScore, + roundScore: otherGameState.roundScore, multiplier: otherGameState.multiplier, rounds: otherGameState.rounds, bonusHistory: otherGameState.bonusHistory, diff --git a/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart new file mode 100644 index 00000000..acd17717 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart @@ -0,0 +1,152 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _MockStreamSubscription extends Mock + implements StreamSubscription {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.score.oneMillion.keyName, + ]; + + group('RampBonusBehavior', () { + const shotPoints = Points.oneMillion; + + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + flameBlocTester.testGameWidget( + 'when hits are multiples of 10 times adds a ScoringBehavior', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState(hits: 9), + ); + final behavior = RampBonusBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 10)); + + final scores = game.descendants().whereType(); + await game.ready(); + + expect(scores.length, 1); + }, + ); + + flameBlocTester.testGameWidget( + "when hits are not multiple of 10 times doesn't add any ScoringBehavior", + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState.initial(), + ); + final behavior = RampBonusBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 1)); + + final scores = game.descendants().whereType(); + await game.ready(); + + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'closes subscription when removed', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final subscription = _MockStreamSubscription(); + when(subscription.cancel).thenAnswer((_) async {}); + + final behavior = RampBonusBehavior.test( + points: shotPoints, + subscription: subscription, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + parent.remove(behavior); + await game.ready(); + + verify(subscription.cancel).called(1); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart new file mode 100644 index 00000000..23f02220 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart @@ -0,0 +1,156 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _MockStreamSubscription extends Mock + implements StreamSubscription {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.score.fiveThousand.keyName, + ]; + + group('RampShotBehavior', () { + const shotPoints = Points.fiveThousand; + + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + flameBlocTester.testGameWidget( + 'when hits are not multiple of 10 times ' + 'increases multiplier and adds a ScoringBehavior', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState.initial(), + ); + final behavior = RampShotBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 1)); + + final scores = game.descendants().whereType(); + await game.ready(); + + verify(() => gameBloc.add(MultiplierIncreased())).called(1); + expect(scores.length, 1); + }, + ); + + flameBlocTester.testGameWidget( + 'when hits multiple of 10 times ' + "doesn't increase multiplier, neither ScoringBehavior", + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState(hits: 9), + ); + final behavior = RampShotBehavior( + points: shotPoints, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + streamController.add(SpaceshipRampState(hits: 10)); + + final scores = game.children.whereType(); + await game.ready(); + + verifyNever(() => gameBloc.add(MultiplierIncreased())); + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'closes subscription when removed', + setUp: (game, tester) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final subscription = _MockStreamSubscription(); + when(subscription.cancel).thenAnswer((_) async {}); + + final behavior = RampShotBehavior.test( + points: shotPoints, + subscription: subscription, + ); + final parent = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAdd(behavior); + + parent.remove(behavior); + await game.ready(); + + verify(subscription.cancel).called(1); + }, + ); + }); +} 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/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index d8d31b4e..dc142ffd 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -2,13 +2,12 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; -import 'package:flame/extensions.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; - import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../helpers/helpers.dart'; @@ -33,13 +32,16 @@ class _MockBall extends Mock implements Ball {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + theme.Assets.images.dash.ball.keyName, + ]; group('BallController', () { late Ball ball; late GameBloc gameBloc; setUp(() { - ball = Ball(baseColor: const Color(0xFF00FFFF)); + ball = Ball(); gameBloc = _MockGameBloc(); whenListen( gameBloc, @@ -51,6 +53,7 @@ void main() { final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, + assets: assets, ); test('can be instantiated', () { @@ -68,7 +71,7 @@ void main() { await ball.add(controller); await game.ensureAdd(ball); - final otherBall = Ball(baseColor: const Color(0xFF00FFFF)); + final otherBall = Ball(); final otherController = BallController(otherBall); await otherBall.add(otherController); await game.ensureAdd(otherBall); @@ -106,6 +109,7 @@ void main() { flameBlocTester.testGameWidget( 'adds TurboChargeActivated', setUp: (game, tester) async { + await game.images.loadAll(assets); final controller = BallController(ball); await ball.add(controller); await game.ensureAdd(ball); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index e8b7aaf3..f9561b60 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -27,7 +27,8 @@ void main() { blocBuilder: () { final bloc = _MockGameBloc(); const state = GameState( - score: 0, + totalScore: 0, + roundScore: 0, multiplier: 1, rounds: 0, bonusHistory: [], diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index c832e24a..4d9c9c74 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -22,7 +22,8 @@ void main() { blocBuilder: () { final bloc = _MockGameBloc(); const state = GameState( - score: 0, + totalScore: 0, + roundScore: 0, multiplier: 1, rounds: 0, bonusHistory: [], diff --git a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart index f9e2988d..71b41029 100644 --- a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart +++ b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart @@ -10,15 +10,21 @@ import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart' import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; class _MockGameBloc extends Mock implements GameBloc {} void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('FlutterForestBonusBehavior', () { late GameBloc gameBloc; - final assets = [Assets.images.dash.animatronic.keyName]; + final assets = [ + Assets.images.dash.animatronic.keyName, + theme.Assets.images.dash.ball.keyName, + ]; setUp(() { gameBloc = _MockGameBloc(); @@ -32,6 +38,7 @@ void main() { final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, + assets: assets, ); void _contactedBumper(DashNestBumper bumper) => diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index c85d0b52..e403396d 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 {} @@ -24,7 +23,8 @@ void main() { group('listenWhen', () { test('is true when the game over state has changed', () { final state = GameState( - score: 10, + totalScore: 0, + roundScore: 10, multiplier: 1, rounds: 0, bonusHistory: const [], @@ -40,7 +40,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 +48,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 +75,13 @@ 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, + totalScore: 0, + roundScore: 10, multiplier: 1, rounds: 0, bonusHistory: const [], @@ -87,22 +89,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/components/multiballs/behaviors/multiballs_behavior_test.dart b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart index 5f8b1400..b294d350 100644 --- a/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart +++ b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart @@ -74,7 +74,8 @@ void main() { test('is false when the bonusHistory state is the same', () { final previous = GameState.initial(); final state = GameState( - score: 10, + totalScore: 0, + roundScore: 10, multiplier: 1, rounds: 0, bonusHistory: const [], diff --git a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart index 40a952f1..ca3c5921 100644 --- a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart +++ b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart @@ -56,7 +56,8 @@ void main() { group('listenWhen', () { test('is true when the multiplier has changed', () { final state = GameState( - score: 10, + totalScore: 0, + roundScore: 10, multiplier: 2, rounds: 0, bonusHistory: const [], @@ -71,7 +72,8 @@ void main() { test('is false when the multiplier state is the same', () { final state = GameState( - score: 10, + totalScore: 0, + roundScore: 10, multiplier: 1, rounds: 0, bonusHistory: const [], diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 98aac670..b75b3147 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../helpers/helpers.dart'; @@ -25,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 = [ @@ -34,11 +41,13 @@ 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, - Assets.images.ball.ball.keyName, + theme.Assets.images.android.ball.keyName, + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + theme.Assets.images.sparky.ball.keyName, Assets.images.ball.flameEffect.keyName, Assets.images.baseboard.left.keyName, Assets.images.baseboard.right.keyName, @@ -410,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', () { @@ -438,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( @@ -462,6 +517,72 @@ void main() { game.onTapUp(0, tapUpEvent); await game.ready(); + final currentBalls = + game.descendants().whereType().toList(); + + expect( + currentBalls.length, + equals(previousBalls.length + 1), + ); + }, + ); + + 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/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 0ed6e744..90d1b194 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -200,12 +200,11 @@ void main() { find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, ); - // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc - // status - // expect( - // find.byType(GameHud), - // findsNothing, - // ); + + expect( + find.byType(GameHud), + findsNothing, + ); }); testWidgets('renders a hud on play state', (tester) async { 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..19c860da 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() { @@ -27,22 +22,17 @@ void main() { late GameBloc gameBloc; const initialState = GameState( - score: 1000, + totalScore: 0, + roundScore: 1000, multiplier: 1, rounds: 1, bonusHistory: [], ); 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, @@ -81,7 +71,10 @@ void main() { gameBloc: gameBloc, ); - expect(find.text(initialState.score.formatScore()), findsOneWidget); + expect( + find.text(initialState.roundScore.formatScore()), + findsOneWidget, + ); }, ); diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 1d7070e0..f10c5f5b 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,69 +1,45 @@ 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 'package:pinball/start_game/bloc/start_game_bloc.dart'; import '../../../helpers/helpers.dart'; -class _MockPinballGame extends Mock implements PinballGame {} - -class _MockGameFlowController extends Mock implements GameFlowController {} - -class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} +class _MockStartGameBloc extends Mock implements StartGameBloc {} void main() { group('PlayButtonOverlay', () { - late PinballGame game; - late GameFlowController gameFlowController; - late CharacterThemeCubit characterThemeCubit; + late StartGameBloc startGameBloc; 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); - game = _MockPinballGame(); - gameFlowController = _MockGameFlowController(); - characterThemeCubit = _MockCharacterThemeCubit(); + await mockFlameImages(); + startGameBloc = _MockStartGameBloc(); + whenListen( - characterThemeCubit, - const Stream.empty(), - initialState: const CharacterThemeState.initial(), + startGameBloc, + Stream.value(const StartGameState.initial()), + initialState: const StartGameState.initial(), ); - when(() => characterThemeCubit.state) - .thenReturn(const CharacterThemeState.initial()); - when(() => game.gameFlowController).thenReturn(gameFlowController); - when(gameFlowController.start).thenAnswer((_) {}); }); testWidgets('renders correctly', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - expect(find.text('Play'), findsOneWidget); - }); + await tester.pumpApp(const PlayButtonOverlay()); - testWidgets('calls gameFlowController.start when tapped', (tester) async { - await tester.pumpApp( - PlayButtonOverlay(game: game), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('Play')); - await tester.pump(); - verify(gameFlowController.start).called(1); + expect(find.text('Play'), findsOneWidget); }); - testWidgets('displays CharacterSelectionDialog when tapped', + testWidgets('adds PlayTapped event to StartGameBloc when taped', (tester) async { await tester.pumpApp( - PlayButtonOverlay(game: game), - characterThemeCubit: characterThemeCubit, + const PlayButtonOverlay(), + startGameBloc: startGameBloc, ); + await tester.tap(find.text('Play')); await tester.pump(); - expect(find.byType(CharacterSelectionDialog), findsOneWidget); + + verify(() => startGameBloc.add(const PlayTapped())).called(1); }); }); } diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart index e3a4b887..049aba95 100644 --- a/test/game/view/widgets/round_count_display_test.dart +++ b/test/game/view/widgets/round_count_display_test.dart @@ -13,7 +13,8 @@ void main() { group('RoundCountDisplay renders', () { late GameBloc gameBloc; const initialState = GameState( - score: 0, + totalScore: 0, + roundScore: 0, multiplier: 1, rounds: 3, bonusHistory: [], diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart index 695dc6e1..0e4acafc 100644 --- a/test/game/view/widgets/score_view_test.dart +++ b/test/game/view/widgets/score_view_test.dart @@ -15,9 +15,11 @@ class _MockGameBloc extends Mock implements GameBloc {} void main() { late GameBloc gameBloc; late StreamController stateController; - const score = 123456789; + const totalScore = 123456789; + const roundScore = 1234; const initialState = GameState( - score: score, + totalScore: totalScore, + roundScore: roundScore, multiplier: 1, rounds: 1, bonusHistory: [], @@ -42,7 +44,10 @@ void main() { ); await tester.pump(); - expect(find.text(score.formatScore()), findsOneWidget); + expect( + find.text(initialState.displayScore.formatScore()), + findsOneWidget, + ); }); testWidgets('renders game over', (tester) async { @@ -69,17 +74,23 @@ void main() { gameBloc: gameBloc, ); - expect(find.text(score.formatScore()), findsOneWidget); + expect( + find.text(initialState.displayScore.formatScore()), + findsOneWidget, + ); final newState = initialState.copyWith( - score: 987654321, + roundScore: 5678, ); stateController.add(newState); await tester.pump(); - expect(find.text(newState.score.formatScore()), findsOneWidget); + expect( + find.text(newState.displayScore.formatScore()), + findsOneWidget, + ); }); }); } 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/how_to_play/how_to_play_dialog_test.dart b/test/how_to_play/how_to_play_dialog_test.dart index 232aa1d5..c6e60d73 100644 --- a/test/how_to_play/how_to_play_dialog_test.dart +++ b/test/how_to_play/how_to_play_dialog_test.dart @@ -3,13 +3,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball_audio/pinball_audio.dart'; import 'package:platform_helper/platform_helper.dart'; import '../helpers/helpers.dart'; -class _MockPinballAudio extends Mock implements PinballAudio {} - class _MockPlatformHelper extends Mock implements PlatformHelper {} void main() { @@ -25,7 +22,11 @@ void main() { testWidgets( 'can be instantiated without passing in a platform helper', (tester) async { - await tester.pumpApp(HowToPlayDialog()); + await tester.pumpApp( + HowToPlayDialog( + onDismissCallback: () {}, + ), + ); expect(find.byType(HowToPlayDialog), findsOneWidget); }, ); @@ -35,6 +36,7 @@ void main() { await tester.pumpApp( HowToPlayDialog( platformHelper: platformHelper, + onDismissCallback: () {}, ), ); expect(find.text(l10n.howToPlay), findsOneWidget); @@ -49,6 +51,7 @@ void main() { await tester.pumpApp( HowToPlayDialog( platformHelper: platformHelper, + onDismissCallback: () {}, ), ); expect(find.text(l10n.howToPlay), findsOneWidget); @@ -62,7 +65,12 @@ void main() { Builder( builder: (context) { return TextButton( - onPressed: () => showHowToPlayDialog(context), + onPressed: () => showDialog( + context: context, + builder: (_) => HowToPlayDialog( + onDismissCallback: () {}, + ), + ), child: const Text('test'), ); }, @@ -82,7 +90,12 @@ void main() { Builder( builder: (context) { return TextButton( - onPressed: () => showHowToPlayDialog(context), + onPressed: () => showDialog( + context: context, + builder: (_) => HowToPlayDialog( + onDismissCallback: () {}, + ), + ), child: const Text('test'), ); }, @@ -96,30 +109,5 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(HowToPlayDialog), findsNothing); }); - - testWidgets( - 'plays the I/O Pinball voice over audio on dismiss', - (tester) async { - final audio = _MockPinballAudio(); - await tester.pumpApp( - Builder( - builder: (context) { - return TextButton( - onPressed: () => showHowToPlayDialog(context), - child: const Text('test'), - ); - }, - ), - pinballAudio: audio, - ); - expect(find.byType(HowToPlayDialog), findsNothing); - await tester.tap(find.text('test')); - await tester.pumpAndSettle(); - - await tester.tapAt(Offset.zero); - await tester.pumpAndSettle(); - verify(audio.ioPinballVoiceOver).called(1); - }, - ); }); } diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index 28033030..a9c0c7ef 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -1,10 +1,9 @@ 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'; -import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -12,17 +11,19 @@ import '../../helpers/helpers.dart'; class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} +class _MockStartGameBloc extends Mock implements StartGameBloc {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); late CharacterThemeCubit characterThemeCubit; + late StartGameBloc startGameBloc; 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(); + startGameBloc = _MockStartGameBloc(); + whenListen( characterThemeCubit, const Stream.empty(), @@ -33,25 +34,6 @@ void main() { }); group('CharacterSelectionDialog', () { - group('showCharacterSelectionDialog', () { - testWidgets('inflates the dialog', (tester) async { - await tester.pumpApp( - Builder( - builder: (context) { - return TextButton( - onPressed: () => showCharacterSelectionDialog(context), - child: const Text('test'), - ); - }, - ), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('test')); - await tester.pump(); - expect(find.byType(CharacterSelectionDialog), findsOneWidget); - }); - }); - testWidgets('selecting a new character calls characterSelected on cubit', (tester) async { await tester.pumpApp( @@ -67,15 +49,22 @@ void main() { testWidgets( 'tapping the select button dismisses the character ' - 'dialog and shows the how to play dialog', (tester) async { + 'dialog and calls CharacterSelected event to the bloc', (tester) async { + whenListen( + startGameBloc, + const Stream.empty(), + initialState: const StartGameState.initial(), + ); + await tester.pumpApp( const CharacterSelectionDialog(), characterThemeCubit: characterThemeCubit, + startGameBloc: startGameBloc, ); await tester.tap(find.byType(PinballButton)); await tester.pumpAndSettle(); expect(find.byType(CharacterSelectionDialog), findsNothing); - expect(find.byType(HowToPlayDialog), findsOneWidget); + verify(() => startGameBloc.add(const CharacterSelected())).called(1); }); testWidgets('updating the selected character updates the preview', diff --git a/test/start_game/bloc/start_game_bloc_test.dart b/test/start_game/bloc/start_game_bloc_test.dart index 6663ff4e..ac548a93 100644 --- a/test/start_game/bloc/start_game_bloc_test.dart +++ b/test/start_game/bloc/start_game_bloc_test.dart @@ -24,9 +24,7 @@ void main() { group('StartGameBloc', () { blocTest( 'on PlayTapped changes status to selectCharacter', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const PlayTapped()), expect: () => [ const StartGameState( @@ -37,9 +35,7 @@ void main() { blocTest( 'on CharacterSelected changes status to howToPlay', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const CharacterSelected()), expect: () => [ const StartGameState( @@ -50,9 +46,7 @@ void main() { blocTest( 'on HowToPlayFinished changes status to play', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const HowToPlayFinished()), expect: () => [ const StartGameState( diff --git a/test/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart new file mode 100644 index 00000000..ca646bc9 --- /dev/null +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -0,0 +1,269 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; + +import '../../helpers/helpers.dart'; + +class _MockStartGameBloc extends Mock implements StartGameBloc {} + +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} + +class _MockPinballGame extends Mock implements PinballGame {} + +class _MockGameFlowController extends Mock implements GameFlowController {} + +class _MockPinballAudio extends Mock implements PinballAudio {} + +void main() { + late StartGameBloc startGameBloc; + late PinballGame pinballGame; + late PinballAudio pinballAudio; + late CharacterThemeCubit characterThemeCubit; + + group('StartGameListener', () { + setUp(() async { + await mockFlameImages(); + + startGameBloc = _MockStartGameBloc(); + pinballGame = _MockPinballGame(); + pinballAudio = _MockPinballAudio(); + characterThemeCubit = _MockCharacterThemeCubit(); + }); + + group('on selectCharacter status', () { + testWidgets( + 'calls start on the game controller', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.selectCharacter), + ), + initialState: const StartGameState.initial(), + ); + final gameController = _MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((_) => gameController); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + verify(gameController.start).called(1); + }, + ); + + testWidgets( + 'shows SelectCharacter dialog', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.selectCharacter), + ), + initialState: const StartGameState.initial(), + ); + whenListen( + characterThemeCubit, + Stream.value(const CharacterThemeState.initial()), + initialState: const CharacterThemeState.initial(), + ); + final gameController = _MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((_) => gameController); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + characterThemeCubit: characterThemeCubit, + ); + + await tester.pump(kThemeAnimationDuration); + + expect( + find.byType(CharacterSelectionDialog), + findsOneWidget, + ); + }, + ); + }); + + testWidgets( + 'on howToPlay status shows HowToPlay dialog', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.howToPlay), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'do nothing on play status', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.play), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + expect( + find.byType(CharacterSelectionDialog), + findsNothing, + ); + }, + ); + + testWidgets( + 'do nothing on initial status', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.initial), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + expect( + find.byType(CharacterSelectionDialog), + findsNothing, + ); + }, + ); + + group('on dismiss HowToPlayDialog', () { + setUp(() { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.howToPlay), + ), + initialState: const StartGameState.initial(), + ); + }); + + testWidgets( + 'adds HowToPlayFinished event', + (tester) async { + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + await tester.tapAt(const Offset(1, 1)); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + await tester.pumpAndSettle(); + + verify( + () => startGameBloc.add(const HowToPlayFinished()), + ).called(1); + }, + ); + + testWidgets( + 'plays the I/O Pinball voice over audio', + (tester) async { + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + pinballAudio: pinballAudio, + ); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + await tester.tapAt(const Offset(1, 1)); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + await tester.pumpAndSettle(); + + verify(pinballAudio.ioPinballVoiceOver).called(1); + }, + ); + }); + }); +}