diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index de46512b..a44d2e33 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -17,15 +17,15 @@ class App extends StatelessWidget { Key? key, required AuthenticationRepository authenticationRepository, required LeaderboardRepository leaderboardRepository, - required PinballAudio pinballAudio, + required PinballPlayer pinballPlayer, }) : _authenticationRepository = authenticationRepository, _leaderboardRepository = leaderboardRepository, - _pinballAudio = pinballAudio, + _pinballPlayer = pinballPlayer, super(key: key); final AuthenticationRepository _authenticationRepository; final LeaderboardRepository _leaderboardRepository; - final PinballAudio _pinballAudio; + final PinballPlayer _pinballPlayer; @override Widget build(BuildContext context) { @@ -33,7 +33,7 @@ class App extends StatelessWidget { providers: [ RepositoryProvider.value(value: _authenticationRepository), RepositoryProvider.value(value: _leaderboardRepository), - RepositoryProvider.value(value: _pinballAudio), + RepositoryProvider.value(value: _pinballPlayer), ], child: MultiBlocProvider( providers: [ diff --git a/lib/game/behaviors/ball_spawning_behavior.dart b/lib/game/behaviors/ball_spawning_behavior.dart new file mode 100644 index 00000000..c074fe52 --- /dev/null +++ b/lib/game/behaviors/ball_spawning_behavior.dart @@ -0,0 +1,35 @@ +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'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// Spawns a new [Ball] into the game when all balls are lost and still +/// [GameStatus.playing]. +class BallSpawningBehavior extends Component + with FlameBlocListenable, HasGameRef { + @override + bool listenWhen(GameState? previousState, GameState newState) { + if (!newState.status.isPlaying) return false; + + final startedGame = previousState?.status.isWaiting ?? true; + final lostRound = + (previousState?.rounds ?? newState.rounds + 1) > newState.rounds; + return startedGame || lostRound; + } + + @override + void onNewState(GameState state) { + final plunger = gameRef.descendants().whereType().single; + final canvas = gameRef.descendants().whereType().single; + final characterTheme = readProvider(); + final ball = ControlledBall.launch(characterTheme: characterTheme) + ..initialPosition = Vector2( + plunger.body.position.x, + plunger.body.position.y - Ball.size.y, + ); + + canvas.add(ball); + } +} diff --git a/lib/game/behaviors/behaviors.dart b/lib/game/behaviors/behaviors.dart index ae51fc09..44cce1df 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -1,2 +1,4 @@ -export 'bumper_noisy_behavior.dart'; +export 'ball_spawning_behavior.dart'; +export 'bumper_noise_behavior.dart'; +export 'camera_focusing_behavior.dart'; export 'scoring_behavior.dart'; diff --git a/lib/game/behaviors/bumper_noisy_behavior.dart b/lib/game/behaviors/bumper_noise_behavior.dart similarity index 56% rename from lib/game/behaviors/bumper_noisy_behavior.dart rename to lib/game/behaviors/bumper_noise_behavior.dart index c837c8c5..9c5da701 100644 --- a/lib/game/behaviors/bumper_noisy_behavior.dart +++ b/lib/game/behaviors/bumper_noise_behavior.dart @@ -1,14 +1,13 @@ // ignore_for_file: public_member_api_docs -import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/pinball_game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_flame/pinball_flame.dart'; -class BumperNoisyBehavior extends ContactBehavior with HasGameRef { +class BumperNoiseBehavior extends ContactBehavior { @override void beginContact(Object other, Contact contact) { super.beginContact(other, contact); - gameRef.audio.bumper(); + readProvider().play(PinballAudio.bumper); } } diff --git a/lib/game/behaviors/camera_focusing_behavior.dart b/lib/game/behaviors/camera_focusing_behavior.dart new file mode 100644 index 00000000..8a13821d --- /dev/null +++ b/lib/game/behaviors/camera_focusing_behavior.dart @@ -0,0 +1,83 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template focus_data} +/// Defines a [Camera] focus point. +/// {@endtemplate} +class FocusData { + /// {@template focus_data} + FocusData({ + required this.zoom, + required this.position, + }); + + /// The amount of zoom. + final double zoom; + + /// The position of the camera. + final Vector2 position; +} + +/// Changes the game focus when the [GameBloc] status changes. +class CameraFocusingBehavior extends Component + with FlameBlocListenable, HasGameRef { + late final Map _foci; + + @override + bool listenWhen(GameState? previousState, GameState newState) { + return previousState?.status != newState.status; + } + + @override + void onNewState(GameState state) { + switch (state.status) { + case GameStatus.waiting: + break; + case GameStatus.playing: + _zoom(_foci['game']!); + break; + case GameStatus.gameOver: + _zoom(_foci['backbox']!); + break; + } + } + + @override + Future onLoad() async { + await super.onLoad(); + _foci = { + 'game': FocusData( + zoom: gameRef.size.y / 16, + position: Vector2(0, -7.8), + ), + 'waiting': FocusData( + zoom: gameRef.size.y / 18, + position: Vector2(0, -112), + ), + 'backbox': FocusData( + zoom: gameRef.size.y / 10, + position: Vector2(0, -111), + ), + }; + + _snap(_foci['waiting']!); + } + + void _snap(FocusData data) { + gameRef.camera + ..speed = 100 + ..followVector2(data.position) + ..zoom = data.zoom; + } + + void _zoom(FocusData data) { + final zoom = CameraZoom(value: data.zoom); + zoom.completed.then((_) { + gameRef.camera.moveTo(data.position); + }); + add(zoom); + } +} diff --git a/lib/game/behaviors/scoring_behavior.dart b/lib/game/behaviors/scoring_behavior.dart index eddcb580..8b403d1e 100644 --- a/lib/game/behaviors/scoring_behavior.dart +++ b/lib/game/behaviors/scoring_behavior.dart @@ -2,6 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/effects.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -12,7 +13,8 @@ import 'package:pinball_flame/pinball_flame.dart'; /// /// The behavior removes itself after the duration. /// {@endtemplate} -class ScoringBehavior extends Component with HasGameRef { +class ScoringBehavior extends Component + with HasGameRef, FlameBlocReader { /// {@macto scoring_behavior} ScoringBehavior({ required Points points, @@ -39,7 +41,8 @@ class ScoringBehavior extends Component with HasGameRef { @override Future onLoad() async { - gameRef.read().add(Scored(points: _points.value)); + await super.onLoad(); + bloc.add(Scored(points: _points.value)); final canvas = gameRef.descendants().whereType().single; await canvas.add( ScoreComponent( @@ -54,8 +57,7 @@ class ScoringBehavior extends Component with HasGameRef { /// {@template scoring_contact_behavior} /// Adds points to the score when the [Ball] contacts the [parent]. /// {@endtemplate} -class ScoringContactBehavior extends ContactBehavior - with HasGameRef { +class ScoringContactBehavior extends ContactBehavior { /// {@macro scoring_contact_behavior} ScoringContactBehavior({ required Points points, diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 43d6005b..b22baa14 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -14,6 +14,16 @@ class GameBloc extends Bloc { on(_onIncreasedMultiplier); on(_onBonusActivated); on(_onSparkyTurboChargeActivated); + on(_onGameOver); + on(_onGameStarted); + } + + void _onGameStarted(GameStarted _, Emitter emit) { + emit(state.copyWith(status: GameStatus.playing)); + } + + void _onGameOver(GameOver _, Emitter emit) { + emit(state.copyWith(status: GameStatus.gameOver)); } void _onRoundLost(RoundLost event, Emitter emit) { @@ -26,12 +36,13 @@ class GameBloc extends Bloc { roundScore: 0, multiplier: 1, rounds: roundsLeft, + status: roundsLeft == 0 ? GameStatus.gameOver : state.status, ), ); } void _onScored(Scored event, Emitter emit) { - if (!state.isGameOver) { + if (state.status.isPlaying) { emit( state.copyWith(roundScore: state.roundScore + event.points), ); @@ -39,7 +50,7 @@ class GameBloc extends Bloc { } void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) { - if (!state.isGameOver) { + if (state.status.isPlaying) { emit( state.copyWith( multiplier: math.min(state.multiplier + 1, 6), diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index c81ce526..6dba8056 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -59,3 +59,17 @@ class MultiplierIncreased extends GameEvent { @override List get props => []; } + +class GameStarted extends GameEvent { + const GameStarted(); + + @override + List get props => []; +} + +class GameOver extends GameEvent { + const GameOver(); + + @override + List get props => []; +} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 2ccb4405..d0311442 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -20,6 +20,18 @@ enum GameBonus { androidSpaceship, } +enum GameStatus { + waiting, + playing, + gameOver, +} + +extension GameStatusX on GameStatus { + bool get isWaiting => this == GameStatus.waiting; + bool get isPlaying => this == GameStatus.playing; + bool get isGameOver => this == GameStatus.gameOver; +} + /// {@template game_state} /// Represents the state of the pinball game. /// {@endtemplate} @@ -31,13 +43,15 @@ class GameState extends Equatable { required this.multiplier, required this.rounds, required this.bonusHistory, + required this.status, }) : 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() - : totalScore = 0, + : status = GameStatus.waiting, + totalScore = 0, roundScore = 0, multiplier = 1, rounds = 3, @@ -65,8 +79,7 @@ class GameState extends Equatable { /// PinballGame. final List bonusHistory; - /// Determines when the game is over. - bool get isGameOver => rounds == 0; + final GameStatus status; /// The score displayed at the game. int get displayScore => roundScore + totalScore; @@ -78,6 +91,7 @@ class GameState extends Equatable { int? balls, int? rounds, List? bonusHistory, + GameStatus? status, }) { assert( totalScore == null || totalScore >= this.totalScore, @@ -90,6 +104,7 @@ class GameState extends Equatable { multiplier: multiplier ?? this.multiplier, rounds: rounds ?? this.rounds, bonusHistory: bonusHistory ?? this.bonusHistory, + status: status ?? this.status, ); } @@ -100,5 +115,6 @@ class GameState extends Equatable { multiplier, rounds, bonusHistory, + status, ]; } diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index 649ef196..7f9fff13 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -35,21 +35,21 @@ class AndroidAcres extends Component { AndroidBumper.a( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], - )..initialPosition = Vector2(-25, 1.3), + )..initialPosition = Vector2(-25.2, 1.5), AndroidBumper.b( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], - )..initialPosition = Vector2(-32.8, -9.2), + )..initialPosition = Vector2(-32.9, -9.3), AndroidBumper.cow( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], - )..initialPosition = Vector2(-20.5, -13.8), + )..initialPosition = Vector2(-20.7, -13), AndroidSpaceshipBonusBehavior(), ], ); diff --git a/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart index 833ac8e4..da181f9e 100644 --- a/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart +++ b/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart @@ -1,11 +1,12 @@ 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'; /// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus. class AndroidSpaceshipBonusBehavior extends Component - with HasGameRef, ParentIsA { + with ParentIsA, FlameBlocReader { @override void onMount() { super.onMount(); @@ -18,9 +19,7 @@ class AndroidSpaceshipBonusBehavior extends Component final listenWhen = state == AndroidSpaceshipState.withBonus; if (!listenWhen) return; - gameRef - .read() - .add(const BonusActivated(GameBonus.androidSpaceship)); + bloc.add(const BonusActivated(GameBonus.androidSpaceship)); androidSpaceship.bloc.onBonusAwarded(); }); } diff --git a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart index 218ad8b4..bc28650f 100644 --- a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart +++ b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart @@ -3,15 +3,13 @@ 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 { +class RampBonusBehavior extends Component with ParentIsA { /// {@macro ramp_bonus_behavior} RampBonusBehavior({ required Points points, diff --git a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart index 8a9c1a9c..b15f5e30 100644 --- a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart +++ b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/cupertino.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; @@ -11,7 +12,7 @@ import 'package:pinball_flame/pinball_flame.dart'; /// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. /// {@endtemplate} class RampShotBehavior extends Component - with ParentIsA, HasGameRef { + with ParentIsA, FlameBlocReader { /// {@macro ramp_shot_behavior} RampShotBehavior({ required Points points, @@ -43,7 +44,7 @@ class RampShotBehavior extends Component final achievedOneMillionPoints = state.hits % 10 == 0; if (!achievedOneMillionPoints) { - gameRef.read().add(const MultiplierIncreased()); + bloc.add(const MultiplierIncreased()); parent.add( ScoringBehavior( diff --git a/lib/game/components/backbox/backbox.dart b/lib/game/components/backbox/backbox.dart index 693ad8c0..ded61f5b 100644 --- a/lib/game/components/backbox/backbox.dart +++ b/lib/game/components/backbox/backbox.dart @@ -1,38 +1,89 @@ import 'dart:async'; import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart'; import 'package:pinball/game/components/backbox/displays/displays.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' hide Assets; /// {@template backbox} /// The [Backbox] of the pinball machine. /// {@endtemplate} -class Backbox extends PositionComponent with HasGameRef, ZIndex { +class Backbox extends PositionComponent with ZIndex { /// {@macro backbox} - Backbox() - : super( - position: Vector2(0, -87), - anchor: Anchor.bottomCenter, - children: [ - _BackboxSpriteComponent(), - ], - ) { + Backbox({ + required LeaderboardRepository leaderboardRepository, + }) : _bloc = BackboxBloc(leaderboardRepository: leaderboardRepository); + + /// {@macro backbox} + @visibleForTesting + Backbox.test({ + required BackboxBloc bloc, + }) : _bloc = bloc; + + late final Component _display; + final BackboxBloc _bloc; + late StreamSubscription _subscription; + + @override + Future onLoad() async { + position = Vector2(0, -87); + anchor = Anchor.bottomCenter; zIndex = ZIndexes.backbox; + + await add(_BackboxSpriteComponent()); + await add(_display = Component()); + + _subscription = _bloc.stream.listen((state) { + _display.children.removeWhere((_) => true); + _build(state); + }); + } + + @override + void onRemove() { + super.onRemove(); + _subscription.cancel(); + } + + void _build(BackboxState state) { + if (state is LoadingState) { + _display.add(LoadingDisplay()); + } else if (state is InitialsFormState) { + _display.add( + InitialsInputDisplay( + score: state.score, + characterIconPath: state.character.leaderboardIcon.keyName, + onSubmit: (initials) { + _bloc.add( + PlayerInitialsSubmitted( + score: state.score, + initials: initials, + character: state.character, + ), + ); + }, + ), + ); + } else if (state is InitialsSuccessState) { + _display.add(InitialsSubmissionSuccessDisplay()); + } else if (state is InitialsFailureState) { + _display.add(InitialsSubmissionFailureDisplay()); + } } /// Puts [InitialsInputDisplay] on the [Backbox]. - Future initialsInput({ + void requestInitials({ required int score, - required String characterIconPath, - InitialsOnSubmit? onSubmit, - }) async { - removeAll(children.where((child) => child is! _BackboxSpriteComponent)); - await add( - InitialsInputDisplay( + required CharacterTheme character, + }) { + _bloc.add( + PlayerInitialsRequested( score: score, - characterIconPath: characterIconPath, - onSubmit: onSubmit, + character: character, ), ); } diff --git a/lib/game/components/backbox/bloc/backbox_bloc.dart b/lib/game/components/backbox/bloc/backbox_bloc.dart new file mode 100644 index 00000000..f315189e --- /dev/null +++ b/lib/game/components/backbox/bloc/backbox_bloc.dart @@ -0,0 +1,56 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:pinball/leaderboard/models/leader_board_entry.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +part 'backbox_event.dart'; +part 'backbox_state.dart'; + +/// {@template backbox_bloc} +/// Bloc which manages the Backbox display. +/// {@endtemplate} +class BackboxBloc extends Bloc { + /// {@macro backbox_bloc} + BackboxBloc({ + required LeaderboardRepository leaderboardRepository, + }) : _leaderboardRepository = leaderboardRepository, + super(LoadingState()) { + on(_onPlayerInitialsRequested); + on(_onPlayerInitialsSubmitted); + } + + final LeaderboardRepository _leaderboardRepository; + + void _onPlayerInitialsRequested( + PlayerInitialsRequested event, + Emitter emit, + ) { + emit( + InitialsFormState( + score: event.score, + character: event.character, + ), + ); + } + + Future _onPlayerInitialsSubmitted( + PlayerInitialsSubmitted event, + Emitter emit, + ) async { + try { + emit(LoadingState()); + await _leaderboardRepository.addLeaderboardEntry( + LeaderboardEntryData( + playerInitials: event.initials, + score: event.score, + character: event.character.toType, + ), + ); + emit(InitialsSuccessState()); + } catch (error, stackTrace) { + addError(error, stackTrace); + emit(InitialsFailureState()); + } + } +} diff --git a/lib/game/components/backbox/bloc/backbox_event.dart b/lib/game/components/backbox/bloc/backbox_event.dart new file mode 100644 index 00000000..42203cdc --- /dev/null +++ b/lib/game/components/backbox/bloc/backbox_event.dart @@ -0,0 +1,53 @@ +part of 'backbox_bloc.dart'; + +/// {@template backbox_event} +/// Base class for backbox events. +/// {@endtemplate} +abstract class BackboxEvent extends Equatable { + /// {@macro backbox_event} + const BackboxEvent(); +} + +/// {@template player_initials_requested} +/// Event that triggers the user initials display. +/// {@endtemplate} +class PlayerInitialsRequested extends BackboxEvent { + /// {@macro player_initials_requested} + const PlayerInitialsRequested({ + required this.score, + required this.character, + }); + + /// Player's score. + final int score; + + /// Player's character. + final CharacterTheme character; + + @override + List get props => [score, character]; +} + +/// {@template player_initials_submitted} +/// Event that submits the user score and initials. +/// {@endtemplate} +class PlayerInitialsSubmitted extends BackboxEvent { + /// {@macro player_initials_submitted} + const PlayerInitialsSubmitted({ + required this.score, + required this.initials, + required this.character, + }); + + /// Player's score. + final int score; + + /// Player's initials. + final String initials; + + /// Player's character. + final CharacterTheme character; + + @override + List get props => [score, initials, character]; +} diff --git a/lib/game/components/backbox/bloc/backbox_state.dart b/lib/game/components/backbox/bloc/backbox_state.dart new file mode 100644 index 00000000..e1f2c801 --- /dev/null +++ b/lib/game/components/backbox/bloc/backbox_state.dart @@ -0,0 +1,59 @@ +part of 'backbox_bloc.dart'; + +/// {@template backbox_state} +/// The base state for all [BackboxState]. +/// {@endtemplate backbox_state} +abstract class BackboxState extends Equatable { + /// {@macro backbox_state} + const BackboxState(); +} + +/// Loading state for the backbox. +class LoadingState extends BackboxState { + @override + List get props => []; +} + +/// State when the leaderboard was successfully loaded. +class LeaderboardSuccessState extends BackboxState { + @override + List get props => []; +} + +/// State when the leaderboard failed to load. +class LeaderboardFailureState extends BackboxState { + @override + List get props => []; +} + +/// {@template initials_form_state} +/// State when the user is inputting their initials. +/// {@endtemplate} +class InitialsFormState extends BackboxState { + /// {@macro initials_form_state} + const InitialsFormState({ + required this.score, + required this.character, + }) : super(); + + /// Player's score. + final int score; + + /// Player's character. + final CharacterTheme character; + + @override + List get props => [score, character]; +} + +/// State when the leaderboard was successfully loaded. +class InitialsSuccessState extends BackboxState { + @override + List get props => []; +} + +/// State when the initials submission failed. +class InitialsFailureState extends BackboxState { + @override + List get props => []; +} diff --git a/lib/game/components/backbox/displays/displays.dart b/lib/game/components/backbox/displays/displays.dart index f5b76ba0..7d5582df 100644 --- a/lib/game/components/backbox/displays/displays.dart +++ b/lib/game/components/backbox/displays/displays.dart @@ -1,2 +1,5 @@ -export 'initials_input_display.dart'; export 'info_display.dart'; +export 'initials_input_display.dart'; +export 'initials_submission_failure_display.dart'; +export 'initials_submission_success_display.dart'; +export 'loading_display.dart'; diff --git a/lib/game/components/backbox/displays/initials_input_display.dart b/lib/game/components/backbox/displays/initials_input_display.dart index fd286d62..244a3e5b 100644 --- a/lib/game/components/backbox/displays/initials_input_display.dart +++ b/lib/game/components/backbox/displays/initials_input_display.dart @@ -4,7 +4,7 @@ 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/l10n/l10n.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -59,7 +59,7 @@ class InitialsInputDisplay extends Component with HasGameRef { await add( InitialsLetterPrompt( position: Vector2( - 11.4 + (2.3 * i), + 10.8 + (2.5 * i), -20, ), hasFocus: i == 0, @@ -103,8 +103,7 @@ class InitialsInputDisplay extends Component with HasGameRef { } } -class _ScoreLabelTextComponent extends TextComponent - with HasGameRef { +class _ScoreLabelTextComponent extends TextComponent { _ScoreLabelTextComponent() : super( anchor: Anchor.centerLeft, @@ -119,7 +118,7 @@ class _ScoreLabelTextComponent extends TextComponent @override Future onLoad() async { await super.onLoad(); - text = gameRef.l10n.score; + text = readProvider().score; } } @@ -133,12 +132,11 @@ class _ScoreTextComponent extends TextComponent { ); } -class _NameLabelTextComponent extends TextComponent - with HasGameRef { +class _NameLabelTextComponent extends TextComponent { _NameLabelTextComponent() : super( anchor: Anchor.center, - position: Vector2(11.4, -24), + position: Vector2(10.8, -24), textRenderer: _bodyTextPaint.copyWith( (style) => style.copyWith( color: PinballColors.red, @@ -149,7 +147,7 @@ class _NameLabelTextComponent extends TextComponent @override Future onLoad() async { await super.onLoad(); - text = gameRef.l10n.name; + text = readProvider().name; } } @@ -158,7 +156,7 @@ class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef { : _characterIconPath = characterIconPath, super( anchor: Anchor.center, - position: Vector2(8.4, -20), + position: Vector2(7.6, -20), ); final String _characterIconPath; @@ -241,8 +239,9 @@ class InitialsLetterPrompt extends PositionComponent { bool _cycle(bool up) { if (_hasFocus) { - final newCharCode = - min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength); + var newCharCode = _charIndex + (up ? -1 : 1); + if (newCharCode < 0) newCharCode = _alphabetLength; + if (newCharCode > _alphabetLength) newCharCode = 0; _input.text = String.fromCharCode(_alphabetCode + newCharCode); _charIndex = newCharCode; @@ -299,8 +298,7 @@ class _InstructionsComponent extends PositionComponent with HasGameRef { ); } -class _EnterInitialsTextComponent extends TextComponent - with HasGameRef { +class _EnterInitialsTextComponent extends TextComponent { _EnterInitialsTextComponent() : super( anchor: Anchor.center, @@ -311,11 +309,11 @@ class _EnterInitialsTextComponent extends TextComponent @override Future onLoad() async { await super.onLoad(); - text = gameRef.l10n.enterInitials; + text = readProvider().enterInitials; } } -class _ArrowsTextComponent extends TextComponent with HasGameRef { +class _ArrowsTextComponent extends TextComponent { _ArrowsTextComponent() : super( anchor: Anchor.center, @@ -330,12 +328,11 @@ class _ArrowsTextComponent extends TextComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - text = gameRef.l10n.arrows; + text = readProvider().arrows; } } -class _AndPressTextComponent extends TextComponent - with HasGameRef { +class _AndPressTextComponent extends TextComponent { _AndPressTextComponent() : super( anchor: Anchor.center, @@ -346,12 +343,11 @@ class _AndPressTextComponent extends TextComponent @override Future onLoad() async { await super.onLoad(); - text = gameRef.l10n.andPress; + text = readProvider().andPress; } } -class _EnterReturnTextComponent extends TextComponent - with HasGameRef { +class _EnterReturnTextComponent extends TextComponent { _EnterReturnTextComponent() : super( anchor: Anchor.center, @@ -366,12 +362,11 @@ class _EnterReturnTextComponent extends TextComponent @override Future onLoad() async { await super.onLoad(); - text = gameRef.l10n.enterReturn; + text = readProvider().enterReturn; } } -class _ToSubmitTextComponent extends TextComponent - with HasGameRef { +class _ToSubmitTextComponent extends TextComponent { _ToSubmitTextComponent() : super( anchor: Anchor.center, @@ -382,6 +377,6 @@ class _ToSubmitTextComponent extends TextComponent @override Future onLoad() async { await super.onLoad(); - text = gameRef.l10n.toSubmit; + text = readProvider().toSubmit; } } diff --git a/lib/game/components/backbox/displays/initials_submission_failure_display.dart b/lib/game/components/backbox/displays/initials_submission_failure_display.dart new file mode 100644 index 00000000..4cc5a9f5 --- /dev/null +++ b/lib/game/components/backbox/displays/initials_submission_failure_display.dart @@ -0,0 +1,27 @@ +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_ui/pinball_ui.dart'; + +final _bodyTextPaint = TextPaint( + style: const TextStyle( + fontSize: 3, + color: PinballColors.white, + fontFamily: PinballFonts.pixeloidSans, + ), +); + +/// {@template initials_submission_failure_display} +/// [Backbox] display for when a failure occurs during initials submission. +/// {@endtemplate} +class InitialsSubmissionFailureDisplay extends TextComponent { + @override + Future onLoad() async { + await super.onLoad(); + position = Vector2(0, -10); + anchor = Anchor.center; + text = 'Failure!'; + textRenderer = _bodyTextPaint; + } +} diff --git a/lib/game/components/backbox/displays/initials_submission_success_display.dart b/lib/game/components/backbox/displays/initials_submission_success_display.dart new file mode 100644 index 00000000..c963a660 --- /dev/null +++ b/lib/game/components/backbox/displays/initials_submission_success_display.dart @@ -0,0 +1,27 @@ +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_ui/pinball_ui.dart'; + +final _bodyTextPaint = TextPaint( + style: const TextStyle( + fontSize: 3, + color: PinballColors.white, + fontFamily: PinballFonts.pixeloidSans, + ), +); + +/// {@template initials_submission_success_display} +/// [Backbox] display for initials successfully submitted. +/// {@endtemplate} +class InitialsSubmissionSuccessDisplay extends TextComponent { + @override + Future onLoad() async { + await super.onLoad(); + position = Vector2(0, -10); + anchor = Anchor.center; + text = 'Success!'; + textRenderer = _bodyTextPaint; + } +} diff --git a/lib/game/components/backbox/displays/loading_display.dart b/lib/game/components/backbox/displays/loading_display.dart new file mode 100644 index 00000000..6178b940 --- /dev/null +++ b/lib/game/components/backbox/displays/loading_display.dart @@ -0,0 +1,49 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +final _bodyTextPaint = TextPaint( + style: const TextStyle( + fontSize: 3, + color: PinballColors.white, + fontFamily: PinballFonts.pixeloidSans, + ), +); + +/// {@template loading_display} +/// Display used to show the loading animation. +/// {@endtemplate} +class LoadingDisplay extends TextComponent { + /// {@template loading_display} + LoadingDisplay(); + + late final String _label; + + @override + Future onLoad() async { + await super.onLoad(); + + position = Vector2(0, -10); + anchor = Anchor.center; + text = _label = readProvider().loading; + textRenderer = _bodyTextPaint; + + await add( + TimerComponent( + period: 1, + repeat: true, + onTick: () { + final index = text.indexOf('.'); + if (index != -1 && text.substring(index).length == 3) { + text = _label; + } else { + text = '$text.'; + } + }, + ), + ); + } +} diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart index d7856e48..13bef589 100644 --- a/lib/game/components/bottom_group.dart +++ b/lib/game/components/bottom_group.dart @@ -39,14 +39,14 @@ class _BottomGroupSide extends Component { @override Future onLoad() async { final direction = _side.direction; - final centerXAdjustment = _side.isLeft ? 0 : -6.66; + final centerXAdjustment = _side.isLeft ? -0.45 : -6.8; final flipper = ControlledFlipper( side: _side, - )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); + )..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6); final baseboard = Baseboard(side: _side) ..initialPosition = Vector2( - (25.58 * direction) + centerXAdjustment, + (25.38 * direction) + centerXAdjustment, 28.71, ); final kicker = Kicker( @@ -56,7 +56,7 @@ class _BottomGroupSide extends Component { ..applyTo(['bouncy_edge']), ], )..initialPosition = Vector2( - (22.64 * direction) + centerXAdjustment, + (22.44 * direction) + centerXAdjustment, 25.1, ); diff --git a/lib/game/components/camera_controller.dart b/lib/game/components/camera_controller.dart deleted file mode 100644 index 083e5745..00000000 --- a/lib/game/components/camera_controller.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flame/components.dart'; -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]. -extension CameraX on 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]. - CameraZoom focusToCameraZoom(FocusData data) { - final zoom = CameraZoom(value: data.zoom); - zoom.completed.then((_) { - moveTo(data.position); - }); - return zoom; - } -} - -/// {@template focus_data} -/// Model class that defines a focus point of the camera. -/// {@endtemplate} -class FocusData { - /// {@template focus_data} - FocusData({ - required this.zoom, - required this.position, - }); - - /// The amount of zoom. - final double zoom; - - /// The position of the camera. - final Vector2 position; -} - -/// {@template camera_controller} -/// 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 waitingBackboxZoom = component.size.y / 18; - final gameOverBackboxZoom = component.size.y / 10; - - gameFocus = FocusData( - zoom: gameZoom, - position: Vector2(0, -7.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 [Backbox]. - component.camera - ..speed = 100 - ..snapToFocus(waitingBackboxFocus); - } - - /// Holds the data for the game focus point. - late final FocusData gameFocus; - - /// 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. - void focusOnGame() { - component.add(component.camera.focusToCameraZoom(gameFocus)); - } - - /// 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 2b132656..b96b6a65 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,14 +1,13 @@ export 'android_acres/android_acres.dart'; export 'backbox/backbox.dart'; export 'bottom_group.dart'; -export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; export 'controlled_plunger.dart'; export 'dino_desert/dino_desert.dart'; -export 'drain.dart'; +export 'drain/drain.dart'; export 'flutter_forest/flutter_forest.dart'; -export 'game_flow_controller.dart'; +export 'game_bloc_status_listener.dart'; export 'google_word/google_word.dart'; export 'launcher.dart'; export 'multiballs/multiballs.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 132639d4..241465dd 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_renaming_method_parameters 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'; @@ -22,9 +23,7 @@ class ControlledBall extends Ball with Controls { zIndex = ZIndexes.ballOnLaunchRamp; } - /// {@template bonus_ball} /// {@macro controlled_ball} - /// {@endtemplate} ControlledBall.bonus({ required CharacterTheme characterTheme, }) : super(assetPath: characterTheme.ball.keyName) { @@ -43,20 +42,14 @@ class ControlledBall extends Ball with Controls { /// Controller attached to a [Ball] that handles its game related logic. /// {@endtemplate} class BallController extends ComponentController - with HasGameRef { + with FlameBlocReader { /// {@macro ball_controller} BallController(Ball ball) : super(ball); - /// Event triggered when the ball is lost. - // TODO(alestiago): Refactor using behaviors. - void lost() { - component.shouldRemove = true; - } - /// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge /// sequence runs, then boosts the ball out of the computer. Future turboCharge() async { - gameRef.read().add(const SparkyTurboChargeActivated()); + bloc.add(const SparkyTurboChargeActivated()); component.stop(); // TODO(alestiago): Refactor this hard coded duration once the following is @@ -70,13 +63,4 @@ class BallController extends ComponentController BallTurboChargingBehavior(impulse: Vector2(40, 110)), ); } - - @override - void onRemove() { - super.onRemove(); - final noBallsLeft = gameRef.descendants().whereType().isEmpty; - if (noBallsLeft) { - gameRef.read().add(const RoundLost()); - } - } } diff --git a/lib/game/components/controlled_flipper.dart b/lib/game/components/controlled_flipper.dart index 3c82e719..1d5502c6 100644 --- a/lib/game/components/controlled_flipper.dart +++ b/lib/game/components/controlled_flipper.dart @@ -21,7 +21,7 @@ class ControlledFlipper extends Flipper with Controls { /// A [ComponentController] that controls a [Flipper]s movement. /// {@endtemplate} class FlipperController extends ComponentController - with KeyboardHandler, BlocComponent { + with KeyboardHandler, FlameBlocReader { /// {@macro flipper_controller} FlipperController(Flipper flipper) : _keys = flipper.side.flipperKeys, @@ -37,7 +37,7 @@ class FlipperController extends ComponentController RawKeyEvent event, Set keysPressed, ) { - if (state?.isGameOver ?? false) return true; + if (!bloc.state.status.isPlaying) return true; if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { diff --git a/lib/game/components/controlled_plunger.dart b/lib/game/components/controlled_plunger.dart index d6c622f7..c8cb90fb 100644 --- a/lib/game/components/controlled_plunger.dart +++ b/lib/game/components/controlled_plunger.dart @@ -2,6 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/services.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -14,13 +15,36 @@ class ControlledPlunger extends Plunger with Controls { : super(compressionDistance: compressionDistance) { controller = PlungerController(this); } + + @override + void release() { + super.release(); + + add(PlungerNoiseBehavior()); + } +} + +/// A behavior attached to the plunger when it launches the ball which plays the +/// related sound effects. +class PlungerNoiseBehavior extends Component { + @override + Future onLoad() async { + await super.onLoad(); + readProvider().play(PinballAudio.launcher); + } + + @override + void update(double dt) { + super.update(dt); + removeFromParent(); + } } /// {@template plunger_controller} /// A [ComponentController] that controls a [Plunger]s movement. /// {@endtemplate} class PlungerController extends ComponentController - with KeyboardHandler, BlocComponent { + with KeyboardHandler, FlameBlocReader { /// {@macro plunger_controller} PlungerController(Plunger plunger) : super(plunger); @@ -38,7 +62,7 @@ class PlungerController extends ComponentController RawKeyEvent event, Set keysPressed, ) { - if (state?.isGameOver ?? false) return true; + if (bloc.state.status.isGameOver) return true; if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { diff --git a/lib/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior.dart b/lib/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior.dart index e4d69f9c..f1e4f53d 100644 --- a/lib/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior.dart +++ b/lib/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior.dart @@ -1,11 +1,12 @@ 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'; /// Adds a [GameBonus.dinoChomp] when a [Ball] is chomped by the [ChromeDino]. class ChromeDinoBonusBehavior extends Component - with HasGameRef, ParentIsA { + with ParentIsA, FlameBlocReader { @override void onMount() { super.onMount(); @@ -18,7 +19,7 @@ class ChromeDinoBonusBehavior extends Component final listenWhen = state.status == ChromeDinoStatus.chomping; if (!listenWhen) return; - gameRef.read().add(const BonusActivated(GameBonus.dinoChomp)); + bloc.add(const BonusActivated(GameBonus.dinoChomp)); }); } } diff --git a/lib/game/components/dino_desert/dino_desert.dart b/lib/game/components/dino_desert/dino_desert.dart index 5f01979f..1d7b9072 100644 --- a/lib/game/components/dino_desert/dino_desert.dart +++ b/lib/game/components/dino_desert/dino_desert.dart @@ -20,7 +20,7 @@ class DinoDesert extends Component { ScoringContactBehavior(points: Points.twoHundredThousand) ..applyTo(['inside_mouth']), ], - )..initialPosition = Vector2(12.6, -6.9), + )..initialPosition = Vector2(12.2, -6.9), _BarrierBehindDino(), DinoWalls(), Slingshots(), diff --git a/lib/game/components/drain.dart b/lib/game/components/drain.dart deleted file mode 100644 index 1dc3e211..00000000 --- a/lib/game/components/drain.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flame/extensions.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template drain} -/// Area located at the bottom of the board to detect when a [Ball] is lost. -/// {@endtemplate} -// TODO(allisonryan0002): move to components package when possible. -class Drain extends BodyComponent with ContactCallbacks { - /// {@macro drain} - Drain() : super(renderBody: false); - - @override - Body createBody() { - final shape = EdgeShape() - ..set( - BoardDimensions.bounds.bottomLeft.toVector2(), - BoardDimensions.bounds.bottomRight.toVector2(), - ); - final fixtureDef = FixtureDef(shape, isSensor: true); - final bodyDef = BodyDef(userData: this); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - -// TODO(allisonryan0002): move this to ball.dart when BallLost is removed. - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is! ControlledBall) return; - other.controller.lost(); - } -} diff --git a/lib/game/components/drain/behaviors/behaviors.dart b/lib/game/components/drain/behaviors/behaviors.dart new file mode 100644 index 00000000..a7c2a401 --- /dev/null +++ b/lib/game/components/drain/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'draining_behavior.dart'; diff --git a/lib/game/components/drain/behaviors/draining_behavior.dart b/lib/game/components/drain/behaviors/draining_behavior.dart new file mode 100644 index 00000000..630d04af --- /dev/null +++ b/lib/game/components/drain/behaviors/draining_behavior.dart @@ -0,0 +1,25 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Handles removing a [Ball] from the game. +class DrainingBehavior extends ContactBehavior with HasGameRef { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + other.removeFromParent(); + final ballsLeft = gameRef.descendants().whereType().length; + if (ballsLeft - 1 == 0) { + ancestors() + .whereType>() + .first + .bloc + .add(const RoundLost()); + } + } +} diff --git a/lib/game/components/drain/drain.dart b/lib/game/components/drain/drain.dart new file mode 100644 index 00000000..aaf09023 --- /dev/null +++ b/lib/game/components/drain/drain.dart @@ -0,0 +1,36 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/foundation.dart'; +import 'package:pinball/game/components/drain/behaviors/behaviors.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template drain} +/// Area located at the bottom of the board. +/// +/// Its [DrainingBehavior] handles removing a [Ball] from the game. +/// {@endtemplate} +class Drain extends BodyComponent with ContactCallbacks { + /// {@macro drain} + Drain() + : super( + renderBody: false, + children: [DrainingBehavior()], + ); + + /// Creates a [Drain] without any children. + /// + /// This can be used for testing a [Drain]'s behaviors in isolation. + @visibleForTesting + Drain.test(); + + @override + Body createBody() { + final shape = EdgeShape() + ..set( + BoardDimensions.bounds.bottomLeft.toVector2(), + BoardDimensions.bounds.bottomRight.toVector2(), + ); + final fixtureDef = FixtureDef(shape, isSensor: true); + return world.createBody(BodyDef())..createFixture(fixtureDef); + } +} diff --git a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart index c06e6f87..a4931f90 100644 --- a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -1,7 +1,9 @@ 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'; +import 'package:pinball_theme/pinball_theme.dart'; /// Bonus obtained at the [FlutterForest]. /// @@ -9,7 +11,10 @@ import 'package:pinball_flame/pinball_flame.dart'; /// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest] /// is awarded, and the [DashNestBumper.main] releases a new [Ball]. class FlutterForestBonusBehavior extends Component - with ParentIsA, HasGameRef { + with + ParentIsA, + HasGameRef, + FlameBlocReader { @override void onMount() { super.onMount(); @@ -35,12 +40,11 @@ class FlutterForestBonusBehavior extends Component } if (signpost.bloc.isFullyProgressed()) { - gameRef - .read() - .add(const BonusActivated(GameBonus.dashNest)); + bloc.add(const BonusActivated(GameBonus.dashNest)); canvas.add( - ControlledBall.bonus(characterTheme: gameRef.characterTheme) - ..initialPosition = Vector2(29.5, -24.5), + ControlledBall.bonus( + characterTheme: readProvider(), + )..initialPosition = Vector2(29.2, -24.5), ); animatronic.playing = true; signpost.bloc.onProgressed(); diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index 259b6bb2..1cc055ae 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -19,27 +19,27 @@ class FlutterForest extends Component with ZIndex { Signpost( children: [ ScoringContactBehavior(points: Points.fiveThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], - )..initialPosition = Vector2(8.35, -58.3), + )..initialPosition = Vector2(7.95, -58.35), DashNestBumper.main( children: [ ScoringContactBehavior(points: Points.twoHundredThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(18.55, -59.35), DashNestBumper.a( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(8.95, -51.95), DashNestBumper.b( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], - )..initialPosition = Vector2(22.3, -46.75), + )..initialPosition = Vector2(21.8, -46.75), DashAnimatronic()..position = Vector2(20, -66), FlutterForestBonusBehavior(), ], diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart new file mode 100644 index 00000000..6e11f3d6 --- /dev/null +++ b/lib/game/components/game_bloc_status_listener.dart @@ -0,0 +1,34 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// Listens to the [GameBloc] and updates the game accordingly. +class GameBlocStatusListener extends Component + with FlameBlocListenable, HasGameRef { + @override + bool listenWhen(GameState? previousState, GameState newState) { + return previousState?.status != newState.status; + } + + @override + void onNewState(GameState state) { + switch (state.status) { + case GameStatus.waiting: + break; + case GameStatus.playing: + readProvider().play(PinballAudio.backgroundMusic); + gameRef.overlays.remove(PinballGame.playButtonOverlay); + break; + case GameStatus.gameOver: + readProvider().play(PinballAudio.gameOverVoiceOver); + gameRef.descendants().whereType().first.requestInitials( + score: state.displayScore, + character: readProvider(), + ); + break; + } + } +} diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart deleted file mode 100644 index e6657627..00000000 --- a/lib/game/components/game_flow_controller.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template game_flow_controller} -/// A [Component] that controls the game over and game restart logic -/// {@endtemplate} -class GameFlowController extends ComponentController - with BlocComponent { - /// {@macro game_flow_controller} - GameFlowController(PinballGame component) : super(component); - - @override - bool listenWhen(GameState? previousState, GameState newState) { - return previousState?.isGameOver != newState.isGameOver; - } - - @override - void onNewState(GameState state) { - print("START $state"); - if (state.isGameOver) { - _initialsInput(); - } else { - start(); - } - } - - /// Puts the game in the initials input state. - void _initialsInput() { - // TODO(erickzanardo): implement score submission and "navigate" to the - // next page - component.descendants().whereType().first.infoScreen( - score: state?.displayScore ?? 0, - characterIconPath: component.characterTheme.leaderboardIcon.keyName, - ); - component.firstChild()!.focusOnGameOverBackbox(); - } - - /// Puts the game in the playing state. - void start() { - _initialsInput(); - /* - component.audio.backgroundMusic(); - component.firstChild()?.focusOnGame(); - component.overlays.remove(PinballGame.playButtonOverlay);*/ - } -} diff --git a/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart index cb9ad308..e49d4537 100644 --- a/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart +++ b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart @@ -1,11 +1,13 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated. class GoogleWordBonusBehavior extends Component - with HasGameRef, ParentIsA { + with ParentIsA, FlameBlocReader { @override void onMount() { super.onMount(); @@ -20,10 +22,8 @@ class GoogleWordBonusBehavior extends Component .every((letter) => letter.bloc.state == GoogleLetterState.lit); if (achievedBonus) { - gameRef.audio.googleBonus(); - gameRef - .read() - .add(const BonusActivated(GameBonus.googleWord)); + readProvider().play(PinballAudio.google); + bloc.add(const BonusActivated(GameBonus.googleWord)); for (final letter in googleLetters) { letter.bloc.onReset(); } diff --git a/lib/game/components/launcher.dart b/lib/game/components/launcher.dart index da1a3569..4729515a 100644 --- a/lib/game/components/launcher.dart +++ b/lib/game/components/launcher.dart @@ -14,8 +14,8 @@ class Launcher extends Component { LaunchRamp(), Flapper(), ControlledPlunger(compressionDistance: 9.2) - ..initialPosition = Vector2(41.2, 43.7), - RocketSpriteComponent()..position = Vector2(43, 62.3), + ..initialPosition = Vector2(41, 43.7), + RocketSpriteComponent()..position = Vector2(42.8, 62.3), ], ); } diff --git a/lib/game/components/multiballs/behaviors/multiballs_behavior.dart b/lib/game/components/multiballs/behaviors/multiballs_behavior.dart index 8b323ff4..b01c32e1 100644 --- a/lib/game/components/multiballs/behaviors/multiballs_behavior.dart +++ b/lib/game/components/multiballs/behaviors/multiballs_behavior.dart @@ -6,10 +6,7 @@ import 'package:pinball_flame/pinball_flame.dart'; /// Toggle each [Multiball] when there is a bonus ball. class MultiballsBehavior extends Component - with - HasGameRef, - ParentIsA, - BlocComponent { + with ParentIsA, FlameBlocListenable { @override bool listenWhen(GameState? previousState, GameState newState) { final hasChanged = previousState?.bonusHistory != newState.bonusHistory; diff --git a/lib/game/components/multipliers/behaviors/multipliers_behavior.dart b/lib/game/components/multipliers/behaviors/multipliers_behavior.dart index 33a59a08..ce58a8eb 100644 --- a/lib/game/components/multipliers/behaviors/multipliers_behavior.dart +++ b/lib/game/components/multipliers/behaviors/multipliers_behavior.dart @@ -6,10 +6,7 @@ import 'package:pinball_flame/pinball_flame.dart'; /// Toggle each [Multiplier] when GameState.multiplier changes. class MultipliersBehavior extends Component - with - HasGameRef, - ParentIsA, - BlocComponent { + with ParentIsA, FlameBlocListenable { @override bool listenWhen(GameState? previousState, GameState newState) { return previousState?.multiplier != newState.multiplier; diff --git a/lib/game/components/multipliers/multipliers.dart b/lib/game/components/multipliers/multipliers.dart index 8e9df1ff..8af4d4ff 100644 --- a/lib/game/components/multipliers/multipliers.dart +++ b/lib/game/components/multipliers/multipliers.dart @@ -14,23 +14,23 @@ class Multipliers extends Component with ZIndex { : super( children: [ Multiplier.x2( - position: Vector2(-19.5, -2), + position: Vector2(-19.6, -2), angle: -15 * math.pi / 180, ), Multiplier.x3( - position: Vector2(13, -9.4), + position: Vector2(12.8, -9.4), angle: 15 * math.pi / 180, ), Multiplier.x4( - position: Vector2(0, -21.2), - angle: 0, + position: Vector2(-0.3, -21.2), + angle: 3 * math.pi / 180, ), Multiplier.x5( - position: Vector2(-8.5, -28), + position: Vector2(-8.9, -28), angle: -3 * math.pi / 180, ), Multiplier.x6( - position: Vector2(10, -30.7), + position: Vector2(9.8, -30.7), angle: 8 * math.pi / 180, ), MultipliersBehavior(), diff --git a/lib/game/components/sparky_scorch.dart b/lib/game/components/sparky_scorch.dart index 5a266b4e..b820e89d 100644 --- a/lib/game/components/sparky_scorch.dart +++ b/lib/game/components/sparky_scorch.dart @@ -18,23 +18,23 @@ class SparkyScorch extends Component { SparkyBumper.a( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(-22.9, -41.65), SparkyBumper.b( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(-21.25, -57.9), SparkyBumper.c( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(-3.3, -52.55), - SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9), - SparkyAnimatronic()..position = Vector2(-13.8, -58.2), + SparkyComputerSensor()..initialPosition = Vector2(-13.2, -49.9), + SparkyAnimatronic()..position = Vector2(-14, -58.2), SparkyComputer(), ], ); diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 907687c9..b4886e4c 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -7,6 +7,7 @@ import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; @@ -16,18 +17,21 @@ import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart'; class PinballGame extends PinballForge2DGame - with - FlameBloc, - HasKeyboardHandlerComponents, - Controls<_GameBallsController>, - MultiTouchTapDetector { + with HasKeyboardHandlerComponents, MultiTouchTapDetector { PinballGame({ - required this.characterTheme, - required this.audio, - required this.l10n, - }) : super(gravity: Vector2(0, 30)) { + required CharacterTheme characterTheme, + required this.leaderboardRepository, + required GameBloc gameBloc, + required AppLocalizations l10n, + required PinballPlayer player, + }) : _gameBloc = gameBloc, + _player = player, + _characterTheme = characterTheme, + _l10n = l10n, + super( + gravity: Vector2(0, 30), + ) { images.prefix = ''; - controller = _GameBallsController(this); } /// Identifier of the play button overlay @@ -36,57 +40,64 @@ class PinballGame extends PinballForge2DGame @override Color backgroundColor() => Colors.transparent; - final CharacterTheme characterTheme; + final CharacterTheme _characterTheme; + + final PinballPlayer _player; - final PinballAudio audio; + final LeaderboardRepository leaderboardRepository; - final AppLocalizations l10n; + final AppLocalizations _l10n; - late final GameFlowController gameFlowController; + final GameBloc _gameBloc; @override Future onLoad() async { - await add(gameFlowController = GameFlowController(this)); - await add(CameraController(this)); - - final machine = [ - BoardBackgroundSpriteComponent(), - Boundaries(), - Backbox(), - ]; - final decals = [ - GoogleWord(position: Vector2(-4.25, 1.8)), - Multipliers(), - Multiballs(), - SkillShot( - children: [ - ScoringContactBehavior(points: Points.oneMillion), - ], - ), - ]; - final characterAreas = [ - AndroidAcres(), - DinoDesert(), - FlutterForest(), - SparkyScorch(), - ]; - await add( - CanvasComponent( - onSpritePainted: (paint) { - if (paint.filterQuality != FilterQuality.medium) { - paint.filterQuality = FilterQuality.medium; - } - }, + FlameBlocProvider.value( + value: _gameBloc, children: [ - ZCanvasComponent( + MultiFlameProvider( + providers: [ + FlameProvider.value(_player), + FlameProvider.value(_characterTheme), + FlameProvider.value(leaderboardRepository), + FlameProvider.value(_l10n), + ], children: [ - ...machine, - ...decals, - ...characterAreas, - Drain(), - BottomGroup(), - Launcher(), + GameBlocStatusListener(), + BallSpawningBehavior(), + CameraFocusingBehavior(), + CanvasComponent( + onSpritePainted: (paint) { + if (paint.filterQuality != FilterQuality.medium) { + paint.filterQuality = FilterQuality.medium; + } + }, + children: [ + ZCanvasComponent( + children: [ + BoardBackgroundSpriteComponent(), + Boundaries(), + Backbox(leaderboardRepository: leaderboardRepository), + GoogleWord(position: Vector2(-4.45, 1.8)), + Multipliers(), + Multiballs(), + SkillShot( + children: [ + ScoringContactBehavior(points: Points.oneMillion), + ], + ), + AndroidAcres(), + DinoDesert(), + FlutterForest(), + SparkyScorch(), + Drain(), + BottomGroup(), + Launcher(), + ], + ), + ], + ), ], ), ], @@ -144,57 +155,20 @@ class PinballGame extends PinballForge2DGame } } -class _GameBallsController extends ComponentController - with BlocComponent { - _GameBallsController(PinballGame game) : super(game); - - @override - bool listenWhen(GameState? previousState, GameState newState) { - final noBallsLeft = component.descendants().whereType().isEmpty; - final notGameOver = !newState.isGameOver; - - return noBallsLeft && notGameOver; - } - - @override - void onNewState(GameState state) { - super.onNewState(state); - spawnBall(); - } - - @override - Future onLoad() async { - await super.onLoad(); - spawnBall(); - } - - void spawnBall() { - // TODO(alestiago): Refactor with behavioural pattern. - component.ready().whenComplete(() { - final plunger = parent!.descendants().whereType().single; - final ball = ControlledBall.launch( - characterTheme: component.characterTheme, - )..initialPosition = Vector2( - plunger.body.position.x, - plunger.body.position.y - Ball.size.y, - ); - component.descendants().whereType().single.add(ball); - }); - } -} - class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { DebugPinballGame({ required CharacterTheme characterTheme, - required PinballAudio audio, + required LeaderboardRepository leaderboardRepository, required AppLocalizations l10n, + required PinballPlayer player, + required GameBloc gameBloc, }) : super( characterTheme: characterTheme, - audio: audio, + player: player, + leaderboardRepository: leaderboardRepository, l10n: l10n, - ) { - controller = _GameBallsController(this); - } + gameBloc: gameBloc, + ); Vector2? lineStart; Vector2? lineEnd; diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 21b9ac5c..be6615f1 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -4,6 +4,7 @@ import 'package:flame/game.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; @@ -36,34 +37,43 @@ class PinballGamePage extends StatelessWidget { Widget build(BuildContext context) { final characterTheme = context.read().state.characterTheme; - final audio = context.read(); - final pinballAudio = context.read(); + final player = context.read(); + final leaderboardRepository = context.read(); - final game = isDebugMode - ? DebugPinballGame( - characterTheme: characterTheme, - audio: audio, - l10n: context.l10n, - ) - : PinballGame( - characterTheme: characterTheme, - audio: audio, - l10n: context.l10n, - ); + return BlocProvider( + create: (_) => GameBloc(), + child: Builder( + builder: (context) { + final gameBloc = context.read(); + final game = isDebugMode + ? DebugPinballGame( + characterTheme: characterTheme, + player: player, + leaderboardRepository: leaderboardRepository, + l10n: context.l10n, + gameBloc: gameBloc, + ) + : PinballGame( + characterTheme: characterTheme, + player: player, + leaderboardRepository: leaderboardRepository, + l10n: context.l10n, + gameBloc: gameBloc, + ); - final loadables = [ - ...game.preLoadAssets(), - pinballAudio.load(), - ...BonusAnimation.loadAssets(), - ...SelectedCharacter.loadAssets(), - ]; + final loadables = [ + ...game.preLoadAssets(), + ...player.load(), + ...BonusAnimation.loadAssets(), + ...SelectedCharacter.loadAssets(), + ]; - return MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => GameBloc()), - BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()), - ], - child: PinballGameView(game: game), + return BlocProvider( + create: (_) => AssetsManagerCubit(loadables)..load(), + child: PinballGameView(game: game), + ); + }, + ), ); } } @@ -110,9 +120,9 @@ class PinballGameLoadedView extends StatelessWidget { final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; final screenWidth = MediaQuery.of(context).size.width; final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); + final clampedMargin = leftMargin > 0 ? leftMargin : 0.0; return StartGameListener( - game: game, child: Stack( children: [ Positioned.fill( @@ -132,8 +142,8 @@ class PinballGameLoadedView extends StatelessWidget { ), ), Positioned( - top: 16, - left: leftMargin, + top: 0, + left: clampedMargin, 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 b40536aa..5f651a60 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -23,16 +23,18 @@ class _GameHudState extends State { /// Ratio from sprite frame (width 500, height 144) w / h = ratio static const _ratio = 3.47; - static const _width = 265.0; @override Widget build(BuildContext context) { - final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + final isGameOver = + context.select((GameBloc bloc) => bloc.state.status.isGameOver); + + final height = _calculateHeight(context); return _ScoreViewDecoration( child: SizedBox( - height: _width / _ratio, - width: _width, + height: height, + width: height * _ratio, child: BlocListener( listenWhen: (previous, current) => previous.bonusHistory.length != current.bonusHistory.length, @@ -53,6 +55,17 @@ class _GameHudState extends State { ), ); } + + double _calculateHeight(BuildContext context) { + final height = MediaQuery.of(context).size.height * 0.09; + if (height > 90) { + return 90; + } else if (height < 60) { + return 60; + } else { + return height; + } + } } class _ScoreViewDecoration extends StatelessWidget { diff --git a/lib/game/view/widgets/score_view.dart b/lib/game/view/widgets/score_view.dart index 76ab9fa4..4c0c3f46 100644 --- a/lib/game/view/widgets/score_view.dart +++ b/lib/game/view/widgets/score_view.dart @@ -13,12 +13,13 @@ class ScoreView extends StatelessWidget { @override Widget build(BuildContext context) { - final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + final isGameOver = + context.select((GameBloc bloc) => bloc.state.status.isGameOver); return Padding( padding: const EdgeInsets.symmetric( horizontal: 16, - vertical: 8, + vertical: 2, ), child: AnimatedSwitcher( duration: kThemeAnimationDuration, @@ -49,17 +50,19 @@ class _ScoreDisplay extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Text( - l10n.score.toLowerCase(), - style: Theme.of(context).textTheme.subtitle1, - ), - const _ScoreText(), - const RoundCountDisplay(), - ], + return FittedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + l10n.score.toLowerCase(), + style: Theme.of(context).textTheme.subtitle1, + ), + const _ScoreText(), + const RoundCountDisplay(), + ], + ), ); } } 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 426fcbe5..1fd26837 100644 --- a/lib/how_to_play/widgets/how_to_play_dialog.dart +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -3,8 +3,10 @@ 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'; @@ -91,12 +93,15 @@ class _HowToPlayDialogState extends State { return WillPopScope( onWillPop: () { widget.onDismissCallback.call(); + context.read().play(PinballAudio.ioPinballVoiceOver); return Future.value(true); }, child: PinballDialog( title: l10n.howToPlay, subtitle: l10n.tipsForFlips, - child: isMobile ? const _MobileBody() : const _DesktopBody(), + child: FittedBox( + child: isMobile ? const _MobileBody() : const _DesktopBody(), + ), ), ); } @@ -109,18 +114,16 @@ class _MobileBody extends StatelessWidget { Widget build(BuildContext context) { final paddingWidth = MediaQuery.of(context).size.width * 0.15; final paddingHeight = MediaQuery.of(context).size.height * 0.075; - return FittedBox( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: paddingWidth, - ), - child: Column( - children: [ - const _MobileLaunchControls(), - SizedBox(height: paddingHeight), - const _MobileFlipperControls(), - ], - ), + return Padding( + padding: EdgeInsets.symmetric( + horizontal: paddingWidth, + ), + child: Column( + children: [ + const _MobileLaunchControls(), + SizedBox(height: paddingHeight), + const _MobileFlipperControls(), + ], ), ); } @@ -189,13 +192,15 @@ class _DesktopBody extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView( - children: const [ - SizedBox(height: 16), - _DesktopLaunchControls(), - SizedBox(height: 16), - _DesktopFlipperControls(), - ], + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: const [ + _DesktopLaunchControls(), + SizedBox(height: 16), + _DesktopFlipperControls(), + ], + ), ); } } diff --git a/lib/leaderboard/models/leader_board_entry.dart b/lib/leaderboard/models/leader_board_entry.dart index a86975dd..db4980a1 100644 --- a/lib/leaderboard/models/leader_board_entry.dart +++ b/lib/leaderboard/models/leader_board_entry.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -6,9 +7,9 @@ import 'package:pinball_theme/pinball_theme.dart'; /// player's initials, score, and chosen character. /// /// {@endtemplate} -class LeaderboardEntry { +class LeaderboardEntry extends Equatable { /// {@macro leaderboard_entry} - LeaderboardEntry({ + const LeaderboardEntry({ required this.rank, required this.playerInitials, required this.score, @@ -26,6 +27,9 @@ class LeaderboardEntry { /// [CharacterTheme] for [LeaderboardEntry]. final AssetGenImage character; + + @override + List get props => [rank, playerInitials, score, character]; } /// Converts [LeaderboardEntryData] from repository to [LeaderboardEntry]. diff --git a/lib/main_development.dart b/lib/main_development.dart index 21166057..67d83b81 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -11,7 +11,7 @@ void main() { bootstrap((firestore, firebaseAuth) async { final leaderboardRepository = LeaderboardRepository(firestore); final authenticationRepository = AuthenticationRepository(firebaseAuth); - final pinballAudio = PinballAudio(); + final pinballPlayer = PinballPlayer(); unawaited( Firebase.initializeApp().then( (_) => authenticationRepository.authenticateAnonymously(), @@ -20,7 +20,7 @@ void main() { return App( authenticationRepository: authenticationRepository, leaderboardRepository: leaderboardRepository, - pinballAudio: pinballAudio, + pinballPlayer: pinballPlayer, ); }); } diff --git a/lib/main_production.dart b/lib/main_production.dart index 21166057..67d83b81 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -11,7 +11,7 @@ void main() { bootstrap((firestore, firebaseAuth) async { final leaderboardRepository = LeaderboardRepository(firestore); final authenticationRepository = AuthenticationRepository(firebaseAuth); - final pinballAudio = PinballAudio(); + final pinballPlayer = PinballPlayer(); unawaited( Firebase.initializeApp().then( (_) => authenticationRepository.authenticateAnonymously(), @@ -20,7 +20,7 @@ void main() { return App( authenticationRepository: authenticationRepository, leaderboardRepository: leaderboardRepository, - pinballAudio: pinballAudio, + pinballPlayer: pinballPlayer, ); }); } diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 21166057..67d83b81 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -11,7 +11,7 @@ void main() { bootstrap((firestore, firebaseAuth) async { final leaderboardRepository = LeaderboardRepository(firestore); final authenticationRepository = AuthenticationRepository(firebaseAuth); - final pinballAudio = PinballAudio(); + final pinballPlayer = PinballPlayer(); unawaited( Firebase.initializeApp().then( (_) => authenticationRepository.authenticateAnonymously(), @@ -20,7 +20,7 @@ void main() { return App( authenticationRepository: authenticationRepository, leaderboardRepository: leaderboardRepository, - pinballAudio: pinballAudio, + pinballPlayer: pinballPlayer, ); }); } diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart index df34b324..692116f3 100644 --- a/lib/start_game/widgets/start_game_listener.dart +++ b/lib/start_game/widgets/start_game_listener.dart @@ -4,7 +4,6 @@ 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} @@ -18,13 +17,10 @@ class StartGameListener extends StatelessWidget { 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) { @@ -35,7 +31,7 @@ class StartGameListener extends StatelessWidget { break; case StartGameStatus.selectCharacter: _onSelectCharacter(context); - _game.gameFlowController.start(); + context.read().add(const GameStarted()); break; case StartGameStatus.howToPlay: _onHowToPlay(context); @@ -57,14 +53,11 @@ class StartGameListener extends StatelessWidget { } void _onHowToPlay(BuildContext context) { - final audio = context.read(); - _showPinballDialog( context: context, child: HowToPlayDialog( onDismissCallback: () { context.read().add(const HowToPlayFinished()); - audio.ioPinballVoiceOver(); }, ), ); diff --git a/packages/pinball_audio/assets/sfx/game_over_voice_over.mp3 b/packages/pinball_audio/assets/sfx/game_over_voice_over.mp3 new file mode 100644 index 00000000..2f2ae590 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/game_over_voice_over.mp3 differ diff --git a/packages/pinball_audio/assets/sfx/google.mp3 b/packages/pinball_audio/assets/sfx/google.mp3 index 34167d44..97659b02 100644 Binary files a/packages/pinball_audio/assets/sfx/google.mp3 and b/packages/pinball_audio/assets/sfx/google.mp3 differ diff --git a/packages/pinball_audio/assets/sfx/launcher.mp3 b/packages/pinball_audio/assets/sfx/launcher.mp3 new file mode 100644 index 00000000..fde95720 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/launcher.mp3 differ diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart index 5bb8fea8..916906c4 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -14,10 +14,13 @@ class $AssetsMusicGen { class $AssetsSfxGen { const $AssetsSfxGen(); + String get afterLaunch => 'assets/sfx/after_launch.mp3'; String get bumperA => 'assets/sfx/bumper_a.mp3'; String get bumperB => 'assets/sfx/bumper_b.mp3'; + String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3'; String get google => 'assets/sfx/google.mp3'; String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; + String get launcher => 'assets/sfx/launcher.mp3'; } class Assets { diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 07257fea..56289417 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -3,10 +3,31 @@ import 'dart:math'; import 'package:audioplayers/audioplayers.dart'; import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/flame_audio.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_audio/gen/assets.gen.dart'; -/// Function that defines the contract of the creation -/// of an [AudioPool] +/// Sounds available for play +enum PinballAudio { + /// Google + google, + + /// Bumper + bumper, + + /// Background music + backgroundMusic, + + /// IO Pinball voice over + ioPinballVoiceOver, + + /// Game over + gameOverVoiceOver, + + /// Launcher + launcher, +} + +/// Defines the contract of the creation of an [AudioPool]. typedef CreateAudioPool = Future Function( String sound, { bool? repeating, @@ -15,28 +36,109 @@ typedef CreateAudioPool = Future Function( String? prefix, }); -/// Function that defines the contract for playing a single -/// audio +/// Defines the contract for playing a single audio. typedef PlaySingleAudio = Future Function(String); -/// Function that defines the contract for looping a single -/// audio +/// Defines the contract for looping a single audio. typedef LoopSingleAudio = Future Function(String); -/// Function that defines the contract for pre fetching an -/// audio +/// Defines the contract for pre fetching an audio. typedef PreCacheSingleAudio = Future Function(String); -/// Function that defines the contract for configuring -/// an [AudioCache] instance +/// Defines the contract for configuring an [AudioCache] instance. typedef ConfigureAudioCache = void Function(AudioCache); -/// {@template pinball_audio} +abstract class _Audio { + void play(); + Future load(); + + String prefixFile(String file) { + return 'packages/pinball_audio/$file'; + } +} + +class _SimplePlayAudio extends _Audio { + _SimplePlayAudio({ + required this.preCacheSingleAudio, + required this.playSingleAudio, + required this.path, + }); + + final PreCacheSingleAudio preCacheSingleAudio; + final PlaySingleAudio playSingleAudio; + final String path; + + @override + Future load() => preCacheSingleAudio(prefixFile(path)); + + @override + void play() { + playSingleAudio(prefixFile(path)); + } +} + +class _LoopAudio extends _Audio { + _LoopAudio({ + required this.preCacheSingleAudio, + required this.loopSingleAudio, + required this.path, + }); + + final PreCacheSingleAudio preCacheSingleAudio; + final LoopSingleAudio loopSingleAudio; + final String path; + + @override + Future load() => preCacheSingleAudio(prefixFile(path)); + + @override + void play() { + loopSingleAudio(prefixFile(path)); + } +} + +class _BumperAudio extends _Audio { + _BumperAudio({ + required this.createAudioPool, + required this.seed, + }); + + final CreateAudioPool createAudioPool; + final Random seed; + + late AudioPool bumperA; + late AudioPool bumperB; + + @override + Future load() async { + await Future.wait( + [ + createAudioPool( + prefixFile(Assets.sfx.bumperA), + maxPlayers: 4, + prefix: '', + ).then((pool) => bumperA = pool), + createAudioPool( + prefixFile(Assets.sfx.bumperB), + maxPlayers: 4, + prefix: '', + ).then((pool) => bumperB = pool), + ], + ); + } + + @override + void play() { + (seed.nextBool() ? bumperA : bumperB).start(volume: 0.6); + } +} + +/// {@template pinball_player} /// Sound manager for the pinball game /// {@endtemplate} -class PinballAudio { - /// {@macro pinball_audio} - PinballAudio({ +class PinballPlayer { + /// {@macro pinball_player} + PinballPlayer({ CreateAudioPool? createAudioPool, PlaySingleAudio? playSingleAudio, LoopSingleAudio? loopSingleAudio, @@ -52,7 +154,39 @@ class PinballAudio { ((AudioCache a) { a.prefix = ''; }), - _seed = seed ?? Random(); + _seed = seed ?? Random() { + audios = { + PinballAudio.google: _SimplePlayAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.google, + ), + PinballAudio.launcher: _SimplePlayAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.launcher, + ), + PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.ioPinballVoiceOver, + ), + PinballAudio.gameOverVoiceOver: _SimplePlayAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.gameOverVoiceOver, + ), + PinballAudio.bumper: _BumperAudio( + createAudioPool: _createAudioPool, + seed: _seed, + ), + PinballAudio.backgroundMusic: _LoopAudio( + preCacheSingleAudio: _preCacheSingleAudio, + loopSingleAudio: _loopSingleAudio, + path: Assets.music.background, + ), + }; + } final CreateAudioPool _createAudioPool; @@ -66,54 +200,24 @@ class PinballAudio { final Random _seed; - late AudioPool _bumperAPool; - - late AudioPool _bumperBPool; + /// Registered audios on the Player + @visibleForTesting + // ignore: library_private_types_in_public_api + late final Map audios; /// Loads the sounds effects into the memory - Future load() async { + List> load() { _configureAudioCache(FlameAudio.audioCache); - _bumperAPool = await _createAudioPool( - _prefixFile(Assets.sfx.bumperA), - maxPlayers: 4, - prefix: '', - ); - - _bumperBPool = await _createAudioPool( - _prefixFile(Assets.sfx.bumperB), - maxPlayers: 4, - prefix: '', - ); - - await Future.wait([ - _preCacheSingleAudio(_prefixFile(Assets.sfx.google)), - _preCacheSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)), - _preCacheSingleAudio(_prefixFile(Assets.music.background)), - ]); - } - - /// Plays a random bumper sfx. - void bumper() { - (_seed.nextBool() ? _bumperAPool : _bumperBPool).start(volume: 0.6); - } - - /// Plays the google word bonus - void googleBonus() { - _playSingleAudio(_prefixFile(Assets.sfx.google)); + return audios.values.map((a) => a.load()).toList(); } - /// Plays the I/O Pinball voice over audio. - void ioPinballVoiceOver() { - _playSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)); - } - - /// Plays the background music - void backgroundMusic() { - _loopSingleAudio(_prefixFile(Assets.music.background)); - } - - String _prefixFile(String file) { - return 'packages/pinball_audio/$file'; + /// Plays the received auido + void play(PinballAudio audio) { + assert( + audios.containsKey(audio), + 'Tried to play unregistered audio $audio', + ); + audios[audio]?.play(); } } diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 916d0f34..fdcd661b 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -51,7 +51,7 @@ void main() { late _MockLoopSingleAudio loopSingleAudio; late _PreCacheSingleAudio preCacheSingleAudio; late Random seed; - late PinballAudio audio; + late PinballPlayer player; setUpAll(() { registerFallbackValue(_MockAudioCache()); @@ -81,7 +81,7 @@ void main() { seed = _MockRandom(); - audio = PinballAudio( + player = PinballPlayer( configureAudioCache: configureAudioCache.onCall, createAudioPool: createAudioPool.onCall, playSingleAudio: playSingleAudio.onCall, @@ -92,12 +92,12 @@ void main() { }); test('can be instantiated', () { - expect(PinballAudio(), isNotNull); + expect(PinballPlayer(), isNotNull); }); group('load', () { test('creates the bumpers pools', () async { - await audio.load(); + await Future.wait(player.load()); verify( () => createAudioPool.onCall( @@ -117,25 +117,25 @@ void main() { }); test('configures the audio cache instance', () async { - await audio.load(); + await Future.wait(player.load()); verify(() => configureAudioCache.onCall(FlameAudio.audioCache)) .called(1); }); test('sets the correct prefix', () async { - audio = PinballAudio( + player = PinballPlayer( createAudioPool: createAudioPool.onCall, playSingleAudio: playSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall, ); - await audio.load(); + await Future.wait(player.load()); expect(FlameAudio.audioCache.prefix, equals('')); }); test('pre cache the assets', () async { - await audio.load(); + await Future.wait(player.load()); verify( () => preCacheSingleAudio @@ -146,6 +146,15 @@ void main() { 'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3', ), ).called(1); + verify( + () => preCacheSingleAudio.onCall( + 'packages/pinball_audio/assets/sfx/game_over_voice_over.mp3', + ), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/launcher.mp3'), + ).called(1); verify( () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/music/background.mp3'), @@ -184,8 +193,8 @@ void main() { group('when seed is true', () { test('plays the bumper A sound pool', () async { when(seed.nextBool).thenReturn(true); - await audio.load(); - audio.bumper(); + await Future.wait(player.load()); + player.play(PinballAudio.bumper); verify(() => bumperAPool.start(volume: 0.6)).called(1); }); @@ -194,8 +203,8 @@ void main() { group('when seed is false', () { test('plays the bumper B sound pool', () async { when(seed.nextBool).thenReturn(false); - await audio.load(); - audio.bumper(); + await Future.wait(player.load()); + player.play(PinballAudio.bumper); verify(() => bumperBPool.start(volume: 0.6)).called(1); }); @@ -204,8 +213,8 @@ void main() { group('googleBonus', () { test('plays the correct file', () async { - await audio.load(); - audio.googleBonus(); + await Future.wait(player.load()); + player.play(PinballAudio.google); verify( () => playSingleAudio @@ -214,10 +223,22 @@ void main() { }); }); + group('launcher', () { + test('plays the correct file', () async { + await Future.wait(player.load()); + player.play(PinballAudio.launcher); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.launcher}'), + ).called(1); + }); + }); + group('ioPinballVoiceOver', () { test('plays the correct file', () async { - await audio.load(); - audio.ioPinballVoiceOver(); + await Future.wait(player.load()); + player.play(PinballAudio.ioPinballVoiceOver); verify( () => playSingleAudio.onCall( @@ -227,10 +248,23 @@ void main() { }); }); + group('gameOverVoiceOver', () { + test('plays the correct file', () async { + await Future.wait(player.load()); + player.play(PinballAudio.gameOverVoiceOver); + + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.gameOverVoiceOver}', + ), + ).called(1); + }); + }); + group('backgroundMusic', () { test('plays the correct file', () async { - await audio.load(); - audio.backgroundMusic(); + await Future.wait(player.load()); + player.play(PinballAudio.backgroundMusic); verify( () => loopSingleAudio @@ -238,5 +272,15 @@ void main() { ).called(1); }); }); + + test( + 'throws assertions error when playing an unregistered audio', + () async { + player.audios.remove(PinballAudio.google); + await Future.wait(player.load()); + + expect(() => player.play(PinballAudio.google), throwsAssertionError); + }, + ); }); } diff --git a/packages/pinball_components/assets/images/android/spaceship/animatronic.png b/packages/pinball_components/assets/images/android/spaceship/animatronic.png index d4b165f3..733e794f 100644 Binary files a/packages/pinball_components/assets/images/android/spaceship/animatronic.png and b/packages/pinball_components/assets/images/android/spaceship/animatronic.png differ diff --git a/packages/pinball_components/assets/images/android/spaceship/saucer.png b/packages/pinball_components/assets/images/android/spaceship/saucer.png index 6c77525a..ff80f99c 100644 Binary files a/packages/pinball_components/assets/images/android/spaceship/saucer.png and b/packages/pinball_components/assets/images/android/spaceship/saucer.png differ diff --git a/packages/pinball_components/assets/images/board-background.png b/packages/pinball_components/assets/images/board-background.png index 979a0873..dabf9026 100644 Binary files a/packages/pinball_components/assets/images/board-background.png and b/packages/pinball_components/assets/images/board-background.png differ diff --git a/packages/pinball_components/assets/images/boundary/outer.png b/packages/pinball_components/assets/images/boundary/outer.png index 1f3bab69..75ccdb6b 100644 Binary files a/packages/pinball_components/assets/images/boundary/outer.png and b/packages/pinball_components/assets/images/boundary/outer.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/a/active.png b/packages/pinball_components/assets/images/dash/bumper/a/active.png index 57330eb4..bd37498d 100644 Binary files a/packages/pinball_components/assets/images/dash/bumper/a/active.png and b/packages/pinball_components/assets/images/dash/bumper/a/active.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/a/inactive.png b/packages/pinball_components/assets/images/dash/bumper/a/inactive.png index bd37498d..57330eb4 100644 Binary files a/packages/pinball_components/assets/images/dash/bumper/a/inactive.png and b/packages/pinball_components/assets/images/dash/bumper/a/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/b/active.png b/packages/pinball_components/assets/images/dash/bumper/b/active.png index fe871847..81cd775a 100644 Binary files a/packages/pinball_components/assets/images/dash/bumper/b/active.png and b/packages/pinball_components/assets/images/dash/bumper/b/active.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/b/inactive.png b/packages/pinball_components/assets/images/dash/bumper/b/inactive.png index 81cd775a..fe871847 100644 Binary files a/packages/pinball_components/assets/images/dash/bumper/b/inactive.png and b/packages/pinball_components/assets/images/dash/bumper/b/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/main/active.png b/packages/pinball_components/assets/images/dash/bumper/main/active.png index 9508b56c..51df02ee 100644 Binary files a/packages/pinball_components/assets/images/dash/bumper/main/active.png and b/packages/pinball_components/assets/images/dash/bumper/main/active.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/main/inactive.png b/packages/pinball_components/assets/images/dash/bumper/main/inactive.png index 51df02ee..9508b56c 100644 Binary files a/packages/pinball_components/assets/images/dash/bumper/main/inactive.png and b/packages/pinball_components/assets/images/dash/bumper/main/inactive.png differ diff --git a/packages/pinball_components/assets/images/launch_ramp/ramp.png b/packages/pinball_components/assets/images/launch_ramp/ramp.png index b024860a..61481f5a 100644 Binary files a/packages/pinball_components/assets/images/launch_ramp/ramp.png and b/packages/pinball_components/assets/images/launch_ramp/ramp.png differ diff --git a/packages/pinball_components/assets/images/slingshot/upper.png b/packages/pinball_components/assets/images/slingshot/upper.png index d86bd925..2338a23e 100644 Binary files a/packages/pinball_components/assets/images/slingshot/upper.png and b/packages/pinball_components/assets/images/slingshot/upper.png differ diff --git a/packages/pinball_components/assets/images/sparky/computer/glow.png b/packages/pinball_components/assets/images/sparky/computer/glow.png index 07ffdb0c..7fd9a0c8 100644 Binary files a/packages/pinball_components/assets/images/sparky/computer/glow.png and b/packages/pinball_components/assets/images/sparky/computer/glow.png differ diff --git a/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart index 4d98b419..d15a5516 100644 --- a/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart +++ b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart @@ -122,7 +122,7 @@ class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent SpriteAnimationData.sequenced( amount: amountPerRow * amountPerColumn, amountPerRow: amountPerRow, - stepTime: 1 / 24, + stepTime: 1 / 12, textureSize: textureSize, ), ); diff --git a/packages/pinball_components/lib/src/components/board_background_sprite_component.dart b/packages/pinball_components/lib/src/components/board_background_sprite_component.dart index ba5b430e..e42c2aca 100644 --- a/packages/pinball_components/lib/src/components/board_background_sprite_component.dart +++ b/packages/pinball_components/lib/src/components/board_background_sprite_component.dart @@ -9,7 +9,7 @@ class BoardBackgroundSpriteComponent extends SpriteComponent BoardBackgroundSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(0, -1), + position: Vector2(-0.2, 0.1), ) { zIndex = ZIndexes.boardBackground; } diff --git a/packages/pinball_components/lib/src/components/boundaries.dart b/packages/pinball_components/lib/src/components/boundaries.dart index 84d3aaeb..4be27cef 100644 --- a/packages/pinball_components/lib/src/components/boundaries.dart +++ b/packages/pinball_components/lib/src/components/boundaries.dart @@ -68,7 +68,7 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { _BottomBoundarySpriteComponent() : super( anchor: Anchor.center, - position: Vector2(-5, 55.6), + position: Vector2(-5.2, 55.6), ); @override diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart index 208936c8..4495053d 100644 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart @@ -60,10 +60,10 @@ class DashNestBumper extends BodyComponent with InitialPosition { Iterable? children, }) : this._( majorRadius: 3, - minorRadius: 2.5, + minorRadius: 2.2, activeAssetPath: Assets.images.dash.bumper.a.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, - spritePosition: Vector2(0.35, -1.2), + spritePosition: Vector2(0.3, -1.3), bloc: DashNestBumperCubit(), children: [ ...?children, @@ -75,11 +75,11 @@ class DashNestBumper extends BodyComponent with InitialPosition { DashNestBumper.b({ Iterable? children, }) : this._( - majorRadius: 3, - minorRadius: 2.5, + majorRadius: 3.1, + minorRadius: 2.2, activeAssetPath: Assets.images.dash.bumper.b.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, - spritePosition: Vector2(0.35, -1.2), + spritePosition: Vector2(0.4, -1.2), bloc: DashNestBumperCubit(), children: [ ...?children, diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index 5125d4bc..38bb2588 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -37,46 +37,46 @@ class _DinoTopWall extends BodyComponent with InitialPosition { List _createFixtureDefs() { final topEdgeShape = EdgeShape() ..set( - Vector2(29.25, -35.27), - Vector2(28.4, -34.77), + Vector2(29.05, -35.27), + Vector2(28.2, -34.77), ); final topCurveShape = BezierCurveShape( controlPoints: [ topEdgeShape.vertex2, - Vector2(21.35, -28.72), - Vector2(23.45, -24.62), + Vector2(21.15, -28.72), + Vector2(23.25, -24.62), ], ); final tunnelTopEdgeShape = EdgeShape() ..set( topCurveShape.vertices.last, - Vector2(30.35, -27.32), + Vector2(30.15, -27.32), ); final tunnelBottomEdgeShape = EdgeShape() ..set( - Vector2(30.75, -23.17), - Vector2(25.45, -21.22), + Vector2(30.55, -23.17), + Vector2(25.25, -21.22), ); final middleEdgeShape = EdgeShape() ..set( tunnelBottomEdgeShape.vertex2, - Vector2(27.45, -19.32), + Vector2(27.25, -19.32), ); final bottomEdgeShape = EdgeShape() ..set( middleEdgeShape.vertex2, - Vector2(24.65, -15.02), + Vector2(24.45, -15.02), ); final undersideEdgeShape = EdgeShape() ..set( bottomEdgeShape.vertex2, - Vector2(31.75, -13.77), + Vector2(31.55, -13.77), ); return [ @@ -108,7 +108,7 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef, ZIndex { _DinoTopWallSpriteComponent() : super( - position: Vector2(22.75, -38.07), + position: Vector2(22.55, -38.07), ) { zIndex = ZIndexes.dinoTopWall; } @@ -129,7 +129,7 @@ class _DinoTopWallSpriteComponent extends SpriteComponent class _DinoTopWallTunnelSpriteComponent extends SpriteComponent with HasGameRef, ZIndex { _DinoTopWallTunnelSpriteComponent() - : super(position: Vector2(23.31, -26.01)) { + : super(position: Vector2(23.11, -26.01)) { zIndex = ZIndexes.dinoTopWallTunnel; } @@ -162,28 +162,28 @@ class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex { List _createFixtureDefs() { final topEdgeShape = EdgeShape() ..set( - Vector2(32.4, -8.8), - Vector2(25, -7.7), + Vector2(32.2, -8.8), + Vector2(24.8, -7.7), ); final topLeftCurveShape = BezierCurveShape( controlPoints: [ topEdgeShape.vertex2, - Vector2(21.8, -7), - Vector2(29.8, 13.8), + Vector2(21.6, -7), + Vector2(29.6, 13.8), ], ); final bottomLeftEdgeShape = EdgeShape() ..set( topLeftCurveShape.vertices.last, - Vector2(31.9, 44.1), + Vector2(31.7, 44.1), ); final bottomEdgeShape = EdgeShape() ..set( bottomLeftEdgeShape.vertex2, - Vector2(37.8, 44.1), + Vector2(37.6, 44.1), ); return [ @@ -219,6 +219,6 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(23.8, -9.5); + position = Vector2(23.6, -9.5); } } diff --git a/packages/pinball_components/lib/src/components/flapper/flapper.dart b/packages/pinball_components/lib/src/components/flapper/flapper.dart index f336273e..11852934 100644 --- a/packages/pinball_components/lib/src/components/flapper/flapper.dart +++ b/packages/pinball_components/lib/src/components/flapper/flapper.dart @@ -18,9 +18,9 @@ class Flapper extends Component { children: [ FlapperSpinningBehavior(), ], - )..initialPosition = Vector2(4, -69.3), + )..initialPosition = Vector2(3.8, -69.3), _FlapperStructure(), - _FlapperExit()..initialPosition = Vector2(-0.6, -33.8), + _FlapperExit()..initialPosition = Vector2(-0.8, -33.8), _BackSupportSpriteComponent(), _FrontSupportSpriteComponent(), FlapSpriteAnimationComponent(), @@ -73,14 +73,14 @@ class _FlapperStructure extends BodyComponent with Layered { List _createFixtureDefs() { final leftEdgeShape = EdgeShape() ..set( - Vector2(1.9, -69.3), - Vector2(1.9, -66), + Vector2(1.7, -69.3), + Vector2(1.7, -66), ); final bottomEdgeShape = EdgeShape() ..set( leftEdgeShape.vertex2, - Vector2(3.9, -66), + Vector2(3.7, -66), ); return [ @@ -130,7 +130,7 @@ class FlapSpriteAnimationComponent extends SpriteAnimationComponent FlapSpriteAnimationComponent() : super( anchor: Anchor.center, - position: Vector2(2.8, -70.7), + position: Vector2(2.6, -70.7), playing: false, ) { zIndex = ZIndexes.flapper; @@ -173,7 +173,7 @@ class _BackSupportSpriteComponent extends SpriteComponent _BackSupportSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(2.95, -70.6), + position: Vector2(2.75, -70.6), ) { zIndex = ZIndexes.flapperBack; } @@ -196,7 +196,7 @@ class _FrontSupportSpriteComponent extends SpriteComponent _FrontSupportSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(2.9, -67.6), + position: Vector2(2.7, -67.7), ) { zIndex = ZIndexes.flapperFront; } diff --git a/packages/pinball_components/lib/src/components/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index 7dcc274e..e8290cff 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -40,22 +40,22 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex { final rightStraightShape = EdgeShape() ..set( - Vector2(31.4, -61.4), - Vector2(46.5, 68.4), + Vector2(31, -61.4), + Vector2(46.1, 68.4), ); final rightStraightFixtureDef = FixtureDef(rightStraightShape); fixturesDef.add(rightStraightFixtureDef); final leftStraightShape = EdgeShape() ..set( - Vector2(27.8, -61.4), - Vector2(41.5, 68.4), + Vector2(27.4, -61.4), + Vector2(41.1, 68.4), ); final leftStraightFixtureDef = FixtureDef(leftStraightShape); fixturesDef.add(leftStraightFixtureDef); final topCurveShape = ArcShape( - center: Vector2(20.5, -61.1), + center: Vector2(20.1, -61.1), arcRadius: 11, angle: 1.6, rotation: 0.1, @@ -64,7 +64,7 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex { fixturesDef.add(topCurveFixtureDef); final bottomCurveShape = ArcShape( - center: Vector2(19.3, -60.3), + center: Vector2(18.9, -60.3), arcRadius: 8.5, angle: 1.48, rotation: 0.1, @@ -74,16 +74,16 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex { final topStraightShape = EdgeShape() ..set( - Vector2(3.7, -70.1), - Vector2(19.1, -72.1), + Vector2(3.3, -70.1), + Vector2(18.7, -72.1), ); final topStraightFixtureDef = FixtureDef(topStraightShape); fixturesDef.add(topStraightFixtureDef); final bottomStraightShape = EdgeShape() ..set( - Vector2(3.7, -66.9), - Vector2(19.1, -68.8), + Vector2(3.3, -66.9), + Vector2(18.7, -68.8), ); final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); fixturesDef.add(bottomStraightFixtureDef); @@ -113,7 +113,7 @@ class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { this.sprite = sprite; size = sprite.originalSize / 10; anchor = Anchor.center; - position = Vector2(25.65, 0.7); + position = Vector2(25.25, 0.7); } } @@ -131,7 +131,7 @@ class _LaunchRampBackgroundRailingSpriteComponent extends SpriteComponent this.sprite = sprite; size = sprite.originalSize / 10; anchor = Anchor.center; - position = Vector2(25.6, -1.3); + position = Vector2(25.2, -1.3); } } @@ -149,14 +149,14 @@ class _LaunchRampForegroundRailing extends BodyComponent with ZIndex { final rightStraightShape = EdgeShape() ..set( - Vector2(27.6, -57.9), - Vector2(38.1, 42.6), + Vector2(27.2, -57.9), + Vector2(37.7, 42.6), ); final rightStraightFixtureDef = FixtureDef(rightStraightShape); fixturesDef.add(rightStraightFixtureDef); final curveShape = ArcShape( - center: Vector2(20.1, -59.3), + center: Vector2(19.7, -59.3), arcRadius: 7.5, angle: 1.8, rotation: -0.13, @@ -166,8 +166,8 @@ class _LaunchRampForegroundRailing extends BodyComponent with ZIndex { final topStraightShape = EdgeShape() ..set( - Vector2(3.7, -66.8), - Vector2(19.7, -66.8), + Vector2(3.3, -66.8), + Vector2(19.3, -66.8), ); final topStraightFixtureDef = FixtureDef(topStraightShape); fixturesDef.add(topStraightFixtureDef); @@ -198,6 +198,6 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent this.sprite = sprite; size = sprite.originalSize / 10; anchor = Anchor.center; - position = Vector2(22.8, 0.5); + position = Vector2(22.4, 0.5); } } diff --git a/packages/pinball_components/lib/src/components/multiball/multiball.dart b/packages/pinball_components/lib/src/components/multiball/multiball.dart index ca348604..663490ca 100644 --- a/packages/pinball_components/lib/src/components/multiball/multiball.dart +++ b/packages/pinball_components/lib/src/components/multiball/multiball.dart @@ -36,8 +36,8 @@ class Multiball extends Component { Multiball.a({ Iterable? children, }) : this._( - position: Vector2(-23, 7.5), - rotation: -24 * math.pi / 180, + position: Vector2(-23.3, 7.5), + rotation: -27 * math.pi / 180, bloc: MultiballCubit(), children: children, ); @@ -46,8 +46,8 @@ class Multiball extends Component { Multiball.b({ Iterable? children, }) : this._( - position: Vector2(-7.2, -6.2), - rotation: -5 * math.pi / 180, + position: Vector2(-7.65, -6.2), + rotation: -2 * math.pi / 180, bloc: MultiballCubit(), children: children, ); @@ -56,8 +56,8 @@ class Multiball extends Component { Multiball.c({ Iterable? children, }) : this._( - position: Vector2(-0.7, -9.3), - rotation: 2.7 * math.pi / 180, + position: Vector2(-1.1, -9.3), + rotation: 6 * math.pi / 180, bloc: MultiballCubit(), children: children, ); @@ -66,8 +66,8 @@ class Multiball extends Component { Multiball.d({ Iterable? children, }) : this._( - position: Vector2(15, 7), - rotation: 24 * math.pi / 180, + position: Vector2(14.8, 7), + rotation: 27 * math.pi / 180, bloc: MultiballCubit(), children: children, ); diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart index 79b370a0..5b9b77b2 100644 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -1,5 +1,6 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -13,16 +14,23 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { /// {@macro plunger} Plunger({ required this.compressionDistance, - }) : super(renderBody: false) { + }) : super( + renderBody: false, + children: [_PlungerSpriteAnimationGroupComponent()], + ) { zIndex = ZIndexes.plunger; layer = Layer.launcher; } + /// Creates a [Plunger] without any children. + /// + /// This can be used for testing [Plunger]'s behaviors in isolation. + @visibleForTesting + Plunger.test({required this.compressionDistance}); + /// Distance the plunger can lower. final double compressionDistance; - late final _PlungerSpriteAnimationGroupComponent _spriteComponent; - List _createFixtureDefs() { final fixturesDef = []; @@ -78,8 +86,10 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { /// Set a constant downward velocity on the [Plunger]. void pull() { + final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!; + body.linearVelocity = Vector2(0, 7); - _spriteComponent.pull(); + sprite.pull(); } /// Set an upward velocity on the [Plunger]. @@ -87,17 +97,19 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { /// The velocity's magnitude depends on how far the [Plunger] has been pulled /// from its original [initialPosition]. void release() { + final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!; + _pullingDownTime = 0; final velocity = (initialPosition.y - body.position.y) * 11; body.linearVelocity = Vector2(0, velocity); - _spriteComponent.release(); + sprite.release(); } @override void update(double dt) { // Ensure that we only pull or release when the time is greater than zero. if (_pullingDownTime > 0) { - _pullingDownTime -= dt; + _pullingDownTime -= PinballForge2DGame.clampDt(dt); if (_pullingDownTime <= 0) { release(); } else { @@ -127,9 +139,6 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { Future onLoad() async { await super.onLoad(); await _anchorToJoint(); - - _spriteComponent = _PlungerSpriteAnimationGroupComponent(); - await add(_spriteComponent); } } diff --git a/packages/pinball_components/lib/src/components/slingshot.dart b/packages/pinball_components/lib/src/components/slingshot.dart index e203c082..5d9e7849 100644 --- a/packages/pinball_components/lib/src/components/slingshot.dart +++ b/packages/pinball_components/lib/src/components/slingshot.dart @@ -13,15 +13,13 @@ class Slingshots extends Component with ZIndex { : super( children: [ Slingshot( - length: 5.64, angle: -0.017, spritePath: Assets.images.slingshot.upper.keyName, - )..initialPosition = Vector2(22.3, -1.58), + )..initialPosition = Vector2(22.7, -0.3), Slingshot( - length: 3.46, angle: -0.468, spritePath: Assets.images.slingshot.lower.keyName, - )..initialPosition = Vector2(24.7, 6.2), + )..initialPosition = Vector2(24.6, 6.1), ], ) { zIndex = ZIndexes.slingshots; @@ -34,11 +32,9 @@ class Slingshots extends Component with ZIndex { class Slingshot extends BodyComponent with InitialPosition { /// {@macro slingshot} Slingshot({ - required double length, required double angle, required String spritePath, - }) : _length = length, - _angle = angle, + }) : _angle = angle, super( children: [ _SlinghsotSpriteComponent(spritePath, angle: angle), @@ -47,29 +43,28 @@ class Slingshot extends BodyComponent with InitialPosition { renderBody: false, ); - final double _length; - final double _angle; List _createFixtureDefs() { + const length = 3.46; const circleRadius = 1.55; final topCircleShape = CircleShape()..radius = circleRadius; - topCircleShape.position.setValues(0, -_length / 2); + topCircleShape.position.setValues(0, -length / 2); final bottomCircleShape = CircleShape()..radius = circleRadius; - bottomCircleShape.position.setValues(0, _length / 2); + bottomCircleShape.position.setValues(0, length / 2); final leftEdgeShape = EdgeShape() ..set( - Vector2(circleRadius, _length / 2), - Vector2(circleRadius, -_length / 2), + Vector2(circleRadius, length / 2), + Vector2(circleRadius, -length / 2), ); final rightEdgeShape = EdgeShape() ..set( - Vector2(-circleRadius, _length / 2), - Vector2(-circleRadius, -_length / 2), + Vector2(-circleRadius, length / 2), + Vector2(-circleRadius, -length / 2), ); return [ diff --git a/packages/pinball_components/lib/src/components/sparky_computer.dart b/packages/pinball_components/lib/src/components/sparky_computer.dart index 512c9d48..8e2fc905 100644 --- a/packages/pinball_components/lib/src/components/sparky_computer.dart +++ b/packages/pinball_components/lib/src/components/sparky_computer.dart @@ -32,18 +32,18 @@ class _ComputerBase extends BodyComponent with InitialPosition, ZIndex { List _createFixtureDefs() { final leftEdge = EdgeShape() ..set( - Vector2(-14.9, -46), - Vector2(-15.3, -49.6), + Vector2(-15.3, -45.9), + Vector2(-15.7, -49.5), ); final topEdge = EdgeShape() ..set( - Vector2(-15.3, -49.6), - Vector2(-10.7, -50.6), + leftEdge.vertex2, + Vector2(-11.1, -50.5), ); final rightEdge = EdgeShape() ..set( - Vector2(-10.7, -50.6), - Vector2(-9, -47.2), + topEdge.vertex2, + Vector2(-9.4, -47.1), ); return [ @@ -67,7 +67,7 @@ class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef { _ComputerBaseSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(-12.1, -48.15), + position: Vector2(-12.44, -48.15), ); @override @@ -89,7 +89,7 @@ class _ComputerTopSpriteComponent extends SpriteComponent _ComputerTopSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(-12.52, -49.37), + position: Vector2(-12.86, -49.37), ) { zIndex = ZIndexes.computerTop; } @@ -113,7 +113,7 @@ class _ComputerGlowSpriteComponent extends SpriteComponent _ComputerGlowSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(7.4, 10), + position: Vector2(4, 11), ) { zIndex = ZIndexes.computerGlow; } diff --git a/packages/pinball_components/lib/src/components/z_indexes.dart b/packages/pinball_components/lib/src/components/z_indexes.dart index b59a9a4b..88447312 100644 --- a/packages/pinball_components/lib/src/components/z_indexes.dart +++ b/packages/pinball_components/lib/src/components/z_indexes.dart @@ -33,7 +33,7 @@ abstract class ZIndexes { static const outerBoundary = _above + boardBackground; - static const outerBottomBoundary = _above + rocket; + static const outerBottomBoundary = _above + bottomBoundary; // Bottom Group @@ -77,7 +77,7 @@ abstract class ZIndexes { static const computerTop = _above + ballOnBoard; - static const computerGlow = _above + ballOnBoard; + static const computerGlow = _above + computerTop; static const sparkyAnimatronic = _above + spaceshipRampForegroundRailing; diff --git a/packages/pinball_components/test/src/components/golden/android_animatronic/end.png b/packages/pinball_components/test/src/components/golden/android_animatronic/end.png index 3d54999f..d6ddbb68 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_animatronic/end.png and b/packages/pinball_components/test/src/components/golden/android_animatronic/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png b/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png index 44916338..00f4da13 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png and b/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_animatronic/start.png b/packages/pinball_components/test/src/components/golden/android_animatronic/start.png index 95580e91..31d8bfdf 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_animatronic/start.png and b/packages/pinball_components/test/src/components/golden/android_animatronic/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/end.png b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png index a64b4724..ce8cdd24 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_spaceship/end.png and b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png index 90361e22..49f6c9ce 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png and b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/start.png b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png index 649a8654..eb109628 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_spaceship/start.png and b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/board-background.png b/packages/pinball_components/test/src/components/golden/board-background.png index 789c5465..31abceb1 100644 Binary files a/packages/pinball_components/test/src/components/golden/board-background.png and b/packages/pinball_components/test/src/components/golden/board-background.png differ diff --git a/packages/pinball_components/test/src/components/golden/boundaries.png b/packages/pinball_components/test/src/components/golden/boundaries.png index 9e9b5633..68f57a86 100644 Binary files a/packages/pinball_components/test/src/components/golden/boundaries.png and b/packages/pinball_components/test/src/components/golden/boundaries.png differ diff --git a/packages/pinball_components/test/src/components/golden/dino-walls.png b/packages/pinball_components/test/src/components/golden/dino-walls.png index c7d55c04..31b317c1 100644 Binary files a/packages/pinball_components/test/src/components/golden/dino-walls.png and b/packages/pinball_components/test/src/components/golden/dino-walls.png differ diff --git a/packages/pinball_components/test/src/components/golden/flapper/end.png b/packages/pinball_components/test/src/components/golden/flapper/end.png index 31319b37..cd04b3d2 100644 Binary files a/packages/pinball_components/test/src/components/golden/flapper/end.png and b/packages/pinball_components/test/src/components/golden/flapper/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/flapper/middle.png b/packages/pinball_components/test/src/components/golden/flapper/middle.png index 4f0484f3..d4788641 100644 Binary files a/packages/pinball_components/test/src/components/golden/flapper/middle.png and b/packages/pinball_components/test/src/components/golden/flapper/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/flapper/start.png b/packages/pinball_components/test/src/components/golden/flapper/start.png index e6da466a..d4e9b3fc 100644 Binary files a/packages/pinball_components/test/src/components/golden/flapper/start.png and b/packages/pinball_components/test/src/components/golden/flapper/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/launch-ramp.png b/packages/pinball_components/test/src/components/golden/launch-ramp.png index 52ab2510..50a4ed4c 100644 Binary files a/packages/pinball_components/test/src/components/golden/launch-ramp.png and b/packages/pinball_components/test/src/components/golden/launch-ramp.png differ diff --git a/packages/pinball_components/test/src/components/golden/slingshots.png b/packages/pinball_components/test/src/components/golden/slingshots.png index d0b01f76..9408324c 100644 Binary files a/packages/pinball_components/test/src/components/golden/slingshots.png and b/packages/pinball_components/test/src/components/golden/slingshots.png differ diff --git a/packages/pinball_components/test/src/components/golden/sparky-computer.png b/packages/pinball_components/test/src/components/golden/sparky-computer.png index 1ade03c2..ebe2e98e 100644 Binary files a/packages/pinball_components/test/src/components/golden/sparky-computer.png and b/packages/pinball_components/test/src/components/golden/sparky-computer.png differ diff --git a/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart b/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart index deb69a44..c612ecb9 100644 --- a/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart +++ b/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart @@ -1,20 +1,17 @@ // ignore_for_file: cascade_invocations, prefer_const_constructors 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_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../helpers/helpers.dart'; - -class _MockMultiplierCubit extends Mock implements MultiplierCubit {} - -void main() { - group('Multiplier', () { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ Assets.images.multiplier.x2.lit.keyName, Assets.images.multiplier.x2.dimmed.keyName, Assets.images.multiplier.x3.lit.keyName, @@ -25,8 +22,16 @@ void main() { Assets.images.multiplier.x5.dimmed.keyName, Assets.images.multiplier.x6.lit.keyName, Assets.images.multiplier.x6.dimmed.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); + ]); + } +} + +class _MockMultiplierCubit extends Mock implements MultiplierCubit {} + +void main() { + group('Multiplier', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); late MultiplierCubit bloc; setUp(() { @@ -85,7 +90,7 @@ void main() { flameTester.testGameWidget( 'lit when bloc state is lit', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -116,7 +121,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x2-lit.png'), ); }, @@ -125,7 +130,7 @@ void main() { flameTester.testGameWidget( 'dimmed when bloc state is dimmed', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -156,7 +161,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x2-dimmed.png'), ); }, @@ -169,7 +174,7 @@ void main() { flameTester.testGameWidget( 'lit when bloc state is lit', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -200,7 +205,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x3-lit.png'), ); }, @@ -209,7 +214,7 @@ void main() { flameTester.testGameWidget( 'dimmed when bloc state is dimmed', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -240,7 +245,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x3-dimmed.png'), ); }, @@ -253,7 +258,7 @@ void main() { flameTester.testGameWidget( 'lit when bloc state is lit', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -284,7 +289,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x4-lit.png'), ); }, @@ -293,7 +298,7 @@ void main() { flameTester.testGameWidget( 'dimmed when bloc state is dimmed', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -324,7 +329,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x4-dimmed.png'), ); }, @@ -337,7 +342,7 @@ void main() { flameTester.testGameWidget( 'lit when bloc state is lit', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -368,7 +373,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x5-lit.png'), ); }, @@ -377,7 +382,7 @@ void main() { flameTester.testGameWidget( 'dimmed when bloc state is dimmed', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -408,7 +413,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x5-dimmed.png'), ); }, @@ -421,7 +426,7 @@ void main() { flameTester.testGameWidget( 'lit when bloc state is lit', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -452,7 +457,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x6-lit.png'), ); }, @@ -461,7 +466,7 @@ void main() { flameTester.testGameWidget( 'dimmed when bloc state is dimmed', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); whenListen( bloc, @@ -492,7 +497,7 @@ void main() { ); await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/multipliers/x6-dimmed.png'), ); }, diff --git a/packages/pinball_components/test/src/components/plunger_test.dart b/packages/pinball_components/test/src/components/plunger_test.dart index abb42d68..ea1ba826 100644 --- a/packages/pinball_components/test/src/components/plunger_test.dart +++ b/packages/pinball_components/test/src/components/plunger_test.dart @@ -14,6 +14,17 @@ void main() { group('Plunger', () { const compressionDistance = 0.0; + test('can be instantiated', () { + expect( + Plunger(compressionDistance: compressionDistance), + isA(), + ); + expect( + Plunger.test(compressionDistance: compressionDistance), + isA(), + ); + }); + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { @@ -141,7 +152,11 @@ void main() { expect(plunger.body.linearVelocity.y, isPositive); - await tester.pump(const Duration(seconds: 2)); + // Call game update at 120 FPS, so that the plunger will act as if it + // was pulled for 2 seconds. + for (var i = 0.0; i < 2; i += 1 / 120) { + game.update(1 / 20); + } expect(plunger.body.linearVelocity.y, isZero); }, diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart index 6f8a40f7..38f09b59 100644 --- a/packages/pinball_flame/lib/pinball_flame.dart +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -3,6 +3,7 @@ library pinball_flame; export 'src/canvas/canvas.dart'; export 'src/component_controller.dart'; export 'src/contact_behavior.dart'; +export 'src/flame_provider.dart'; export 'src/keyboard_input_controller.dart'; export 'src/parent_is_a.dart'; export 'src/pinball_forge2d_game.dart'; diff --git a/packages/pinball_flame/lib/src/contact_behavior.dart b/packages/pinball_flame/lib/src/contact_behavior.dart index ff715b12..92f108d8 100644 --- a/packages/pinball_flame/lib/src/contact_behavior.dart +++ b/packages/pinball_flame/lib/src/contact_behavior.dart @@ -26,6 +26,7 @@ class ContactBehavior extends Component @override Future onLoad() async { + await super.onLoad(); if (_fixturesUserData.isNotEmpty) { for (final fixture in _targetedFixtures) { fixture.userData = _UserData.fromFixture(fixture)..add(this); diff --git a/packages/pinball_flame/lib/src/flame_provider.dart b/packages/pinball_flame/lib/src/flame_provider.dart new file mode 100644 index 00000000..35afb0a5 --- /dev/null +++ b/packages/pinball_flame/lib/src/flame_provider.dart @@ -0,0 +1,65 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame/components.dart'; + +class FlameProvider extends Component { + FlameProvider.value( + this.provider, { + Iterable? children, + }) : super( + children: children, + ); + + final T provider; +} + +class MultiFlameProvider extends Component { + MultiFlameProvider({ + required List> providers, + Iterable? children, + }) : _providers = providers, + _initialChildren = children, + assert(providers.isNotEmpty, 'At least one provider must be given') { + _addProviders(); + } + + final List> _providers; + final Iterable? _initialChildren; + FlameProvider? _lastProvider; + + Future _addProviders() async { + final _list = [..._providers]; + + var current = _list.removeAt(0); + while (_list.isNotEmpty) { + final provider = _list.removeAt(0); + await current.add(provider); + current = provider; + } + + await add(_providers.first); + _lastProvider = current; + + _initialChildren?.forEach(add); + } + + @override + Future add(Component component) async { + if (_lastProvider == null) { + await super.add(component); + } + await _lastProvider?.add(component); + } +} + +extension ReadFlameProvider on Component { + T readProvider() { + final providers = ancestors().whereType>(); + assert( + providers.isNotEmpty, + 'No FlameProvider<$T> available on the component tree', + ); + + return providers.first.provider; + } +} diff --git a/packages/pinball_flame/lib/src/pinball_forge2d_game.dart b/packages/pinball_flame/lib/src/pinball_forge2d_game.dart index 118baad9..0013dd26 100644 --- a/packages/pinball_flame/lib/src/pinball_forge2d_game.dart +++ b/packages/pinball_flame/lib/src/pinball_forge2d_game.dart @@ -24,7 +24,7 @@ class PinballForge2DGame extends FlameGame implements Forge2DGame { @override void update(double dt) { super.update(dt); - world.stepDt(min(dt, 1 / 60)); + world.stepDt(clampDt(dt)); } @override @@ -41,4 +41,14 @@ class PinballForge2DGame extends FlameGame implements Forge2DGame { Vector2 worldToScreen(Vector2 position) { throw UnimplementedError(); } + + /// Clamp the [dt] in such a way that it would never exceed the minimal of + /// 1/60th of a second. + /// + /// Note: this is a static method because composing this class as a generic + /// on `BodyComponent` and mixins for that class will crash the Dart analyzer + /// server. + static double clampDt(double dt) { + return min(dt, 1 / 60); + } } diff --git a/packages/pinball_flame/test/src/flame_provider_test.dart b/packages/pinball_flame/test/src/flame_provider_test.dart new file mode 100644 index 00000000..cfc10613 --- /dev/null +++ b/packages/pinball_flame/test/src/flame_provider_test.dart @@ -0,0 +1,103 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(FlameGame.new); + + group( + 'FlameProvider', + () { + test('can be instantiated', () { + expect( + FlameProvider.value(true), + isA>(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final component = FlameProvider.value(true); + await game.ensureAdd(component); + expect(game.children, contains(component)); + }); + + flameTester.test('adds children', (game) async { + final component = Component(); + final provider = FlameProvider.value( + true, + children: [component], + ); + await game.ensureAdd(provider); + expect(provider.children, contains(component)); + }); + }, + ); + + group('MultiFlameProvider', () { + test('can be instantiated', () { + expect( + MultiFlameProvider( + providers: [ + FlameProvider.value(true), + ], + ), + isA(), + ); + }); + + flameTester.test('adds multiple providers', (game) async { + final provider1 = FlameProvider.value(true); + final provider2 = FlameProvider.value(true); + final providers = MultiFlameProvider( + providers: [provider1, provider2], + ); + await game.ensureAdd(providers); + expect(providers.children, contains(provider1)); + expect(provider1.children, contains(provider2)); + }); + + flameTester.test('adds children under provider', (game) async { + final component = Component(); + final provider = FlameProvider.value(true); + final providers = MultiFlameProvider( + providers: [provider], + children: [component], + ); + await game.ensureAdd(providers); + expect(provider.children, contains(component)); + }); + }); + + group( + 'ReadFlameProvider', + () { + flameTester.test('loads provider', (game) async { + final component = Component(); + final provider = FlameProvider.value( + true, + children: [component], + ); + await game.ensureAdd(provider); + expect(component.readProvider(), isTrue); + }); + + flameTester.test( + 'throws assertionError when no provider is found', + (game) async { + final component = Component(); + await game.ensureAdd(component); + + expect( + () => component.readProvider(), + throwsAssertionError, + ); + }, + ); + }, + ); +} diff --git a/packages/pinball_flame/test/src/pinball_forge2d_game_test.dart b/packages/pinball_flame/test/src/pinball_forge2d_game_test.dart index 872f8b97..b8dc9fcc 100644 --- a/packages/pinball_flame/test/src/pinball_forge2d_game_test.dart +++ b/packages/pinball_flame/test/src/pinball_forge2d_game_test.dart @@ -47,5 +47,17 @@ void main() { ); }, ); + + group('clampDt', () { + test('returns dt', () { + const dt = 0.0001; + expect(PinballForge2DGame.clampDt(dt), equals(dt)); + }); + + test('returns result of 1/60 as dt is to high', () { + const dt = 1.0; + expect(PinballForge2DGame.clampDt(dt), equals(1 / 60)); + }); + }); }); } diff --git a/packages/pinball_ui/lib/src/theme/pinball_text_style.dart b/packages/pinball_ui/lib/src/theme/pinball_text_style.dart index 5e0a7fa2..10383abf 100644 --- a/packages/pinball_ui/lib/src/theme/pinball_text_style.dart +++ b/packages/pinball_ui/lib/src/theme/pinball_text_style.dart @@ -45,7 +45,7 @@ abstract class PinballTextStyle { ); static const subtitle1 = TextStyle( - fontSize: 10, + fontSize: 12, fontFamily: _primaryFontFamily, package: _fontPackage, color: PinballColors.yellow, diff --git a/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart b/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart index 2af092b2..72cd66a6 100644 --- a/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart +++ b/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart @@ -26,9 +26,9 @@ void main() { expect(style.color, PinballColors.white); }); - test('subtitle1 has fontSize 10 and yellow color', () { + test('subtitle1 has fontSize 12 and yellow color', () { const style = PinballTextStyle.subtitle1; - expect(style.fontSize, 10); + expect(style.fontSize, 12); expect(style.color, PinballColors.yellow); }); diff --git a/pubspec.lock b/pubspec.lock index ffbd3899..96f9f2a6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -238,7 +238,7 @@ packages: name: flame_bloc url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.4.0" flame_forge2d: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b98c84a6..fcee1e6e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: firebase_auth: ^3.3.16 firebase_core: ^1.15.0 flame: ^1.1.1 - flame_bloc: ^1.2.0 + flame_bloc: ^1.4.0 flame_forge2d: git: url: https://github.com/flame-engine/flame/ diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index ca1cedff..4f04a89d 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -9,7 +9,7 @@ import 'package:pinball_audio/pinball_audio.dart'; class _MockAuthenticationRepository extends Mock implements AuthenticationRepository {} -class _MockPinballAudio extends Mock implements PinballAudio {} +class _MockPinballPlayer extends Mock implements PinballPlayer {} class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { } @@ -18,13 +18,13 @@ void main() { group('App', () { late AuthenticationRepository authenticationRepository; late LeaderboardRepository leaderboardRepository; - late PinballAudio pinballAudio; + late PinballPlayer pinballPlayer; setUp(() { authenticationRepository = _MockAuthenticationRepository(); leaderboardRepository = _MockLeaderboardRepository(); - pinballAudio = _MockPinballAudio(); - when(pinballAudio.load).thenAnswer((_) => Future.value()); + pinballPlayer = _MockPinballPlayer(); + when(pinballPlayer.load).thenAnswer((_) => [Future.value()]); }); testWidgets('renders PinballGamePage', (tester) async { @@ -32,7 +32,7 @@ void main() { App( authenticationRepository: authenticationRepository, leaderboardRepository: leaderboardRepository, - pinballAudio: pinballAudio, + pinballPlayer: pinballPlayer, ), ); expect(find.byType(PinballGamePage), findsOneWidget); diff --git a/test/footer/footer_test.dart b/test/footer/footer_test.dart index f8f69259..8f683cbf 100644 --- a/test/footer/footer_test.dart +++ b/test/footer/footer_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -7,6 +8,21 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import '../helpers/helpers.dart'; +bool _tapTextSpan(RichText richText, String text) { + final isTapped = !richText.text.visitChildren( + (visitor) => _findTextAndTap(visitor, text), + ); + return isTapped; +} + +bool _findTextAndTap(InlineSpan visitor, String text) { + if (visitor is TextSpan && visitor.text == text) { + (visitor.recognizer as TapGestureRecognizer?)?.onTap?.call(); + return false; + } + return true; +} + class _MockUrlLauncher extends Mock with MockPlatformInterfaceMixin implements UrlLauncherPlatform {} @@ -49,7 +65,7 @@ void main() { ).thenAnswer((_) async => true); await tester.pumpApp(const Footer()); final flutterTextFinder = find.byWidgetPredicate( - (widget) => widget is RichText && tapTextSpan(widget, 'Flutter'), + (widget) => widget is RichText && _tapTextSpan(widget, 'Flutter'), ); await tester.tap(flutterTextFinder); await tester.pumpAndSettle(); @@ -84,7 +100,7 @@ void main() { ).thenAnswer((_) async => true); await tester.pumpApp(const Footer()); final firebaseTextFinder = find.byWidgetPredicate( - (widget) => widget is RichText && tapTextSpan(widget, 'Firebase'), + (widget) => widget is RichText && _tapTextSpan(widget, 'Firebase'), ); await tester.tap(firebaseTextFinder); await tester.pumpAndSettle(); diff --git a/test/game/behaviors/bumper_noisy_behavior_test.dart b/test/game/behaviors/bumper_noise_behavior_test.dart similarity index 52% rename from test/game/behaviors/bumper_noisy_behavior_test.dart rename to test/game/behaviors/bumper_noise_behavior_test.dart index b288e4c6..d8075726 100644 --- a/test/game/behaviors/bumper_noisy_behavior_test.dart +++ b/test/game/behaviors/bumper_noise_behavior_test.dart @@ -6,45 +6,53 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball_audio/pinball_audio.dart'; - -import '../../helpers/helpers.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + Future pump(_TestBodyComponent child, {required PinballPlayer player}) { + return ensureAdd( + FlameProvider.value( + player, + children: [ + child, + ], + ), + ); + } +} class _TestBodyComponent extends BodyComponent { @override - Body createBody() { - return world.createBody(BodyDef()); - } + Body createBody() => world.createBody(BodyDef()); } -class _MockPinballAudio extends Mock implements PinballAudio {} +class _MockPinballPlayer extends Mock implements PinballPlayer {} class _MockContact extends Mock implements Contact {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('BumperNoisyBehavior', () {}); + group('BumperNoiseBehavior', () {}); - late PinballAudio audio; - final flameTester = FlameTester( - () => EmptyPinballTestGame(audio: audio), - ); + late PinballPlayer player; + final flameTester = FlameTester(_TestGame.new); setUp(() { - audio = _MockPinballAudio(); + player = _MockPinballPlayer(); }); flameTester.testGameWidget( 'plays bumper sound', setUp: (game, _) async { - final behavior = BumperNoisyBehavior(); + final behavior = BumperNoiseBehavior(); final parent = _TestBodyComponent(); - await game.ensureAdd(parent); + await game.pump(parent, player: player); await parent.ensureAdd(behavior); behavior.beginContact(Object(), _MockContact()); }, verify: (_, __) async { - verify(audio.bumper).called(1); + verify(() => player.play(PinballAudio.bumper)).called(1); }, ); } diff --git a/test/game/behaviors/camera_focusing_behavior_test.dart b/test/game/behaviors/camera_focusing_behavior_test.dart new file mode 100644 index 00000000..a856b392 --- /dev/null +++ b/test/game/behaviors/camera_focusing_behavior_test.dart @@ -0,0 +1,143 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/game.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/behaviors/camera_focusing_behavior.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'CameraFocusingBehavior', + () { + final flameTester = FlameTester(FlameGame.new); + + test('can be instantiated', () { + expect( + CameraFocusingBehavior(), + isA(), + ); + }); + + flameTester.test('loads', (game) async { + late final behavior = CameraFocusingBehavior(); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [behavior], + ), + ); + expect(game.descendants(), contains(behavior)); + }); + + flameTester.test( + 'changes focus when loaded', + (game) async { + final behavior = CameraFocusingBehavior(); + final previousZoom = game.camera.zoom; + expect(game.camera.follow, isNull); + + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [behavior], + ), + ); + + expect(game.camera.follow, isNotNull); + expect(game.camera.zoom, isNot(equals(previousZoom))); + }, + ); + + flameTester.test( + 'listenWhen only listens when status changes', + (game) async { + final behavior = CameraFocusingBehavior(); + const waiting = GameState.initial(); + final playing = + const GameState.initial().copyWith(status: GameStatus.playing); + final gameOver = + const GameState.initial().copyWith(status: GameStatus.gameOver); + + expect(behavior.listenWhen(waiting, waiting), isFalse); + expect(behavior.listenWhen(waiting, playing), isTrue); + expect(behavior.listenWhen(waiting, gameOver), isTrue); + + expect(behavior.listenWhen(playing, playing), isFalse); + expect(behavior.listenWhen(playing, waiting), isTrue); + expect(behavior.listenWhen(playing, gameOver), isTrue); + + expect(behavior.listenWhen(gameOver, gameOver), isFalse); + expect(behavior.listenWhen(gameOver, waiting), isTrue); + expect(behavior.listenWhen(gameOver, playing), isTrue); + }, + ); + + group('onNewState', () { + flameTester.test( + 'zooms when started playing', + (game) async { + final playing = + const GameState.initial().copyWith(status: GameStatus.playing); + + final behavior = CameraFocusingBehavior(); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [behavior], + ), + ); + behavior.onNewState(playing); + final previousPosition = game.camera.position.clone(); + await game.ready(); + + final zoom = behavior.children.whereType().single; + game.update(zoom.controller.duration!); + game.update(0); + + expect(zoom.controller.completed, isTrue); + expect( + game.camera.position, + isNot(equals(previousPosition)), + ); + }, + ); + + flameTester.test( + 'zooms when game is over', + (game) async { + final playing = const GameState.initial().copyWith( + status: GameStatus.gameOver, + ); + + final behavior = CameraFocusingBehavior(); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [behavior], + ), + ); + + behavior.onNewState(playing); + final previousPosition = game.camera.position.clone(); + await game.ready(); + + final zoom = behavior.children.whereType().single; + game.update(zoom.controller.duration!); + game.update(0); + + expect(zoom.controller.completed, isTrue); + expect( + game.camera.position, + isNot(equals(previousPosition)), + ); + }, + ); + }); + }, + ); +} diff --git a/test/game/behaviors/scoring_behavior_test.dart b/test/game/behaviors/scoring_behavior_test.dart index 47903d8a..ef3f10ca 100644 --- a/test/game/behaviors/scoring_behavior_test.dart +++ b/test/game/behaviors/scoring_behavior_test.dart @@ -1,6 +1,6 @@ // ignore_for_file: cascade_invocations -import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -10,7 +10,29 @@ 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 _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.score.fiveThousand.keyName, + Assets.images.score.twentyThousand.keyName, + Assets.images.score.twoHundredThousand.keyName, + Assets.images.score.oneMillion.keyName, + ]); + } + + Future pump(BodyComponent child, {GameBloc? gameBloc}) { + return ensureAdd( + FlameBlocProvider.value( + value: gameBloc ?? GameBloc(), + children: [ + ZCanvasComponent(children: [child]) + ], + ), + ); + } +} class _TestBodyComponent extends BodyComponent { @override @@ -27,18 +49,13 @@ class _MockContact extends Mock implements Contact {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.score.fiveThousand.keyName, - Assets.images.score.twentyThousand.keyName, - Assets.images.score.twoHundredThousand.keyName, - Assets.images.score.oneMillion.keyName, - ]; late GameBloc bloc; late Ball ball; late BodyComponent parent; setUp(() { + bloc = _MockGameBloc(); ball = _MockBall(); final ballBody = _MockBody(); when(() => ball.body).thenReturn(ballBody); @@ -47,22 +64,7 @@ void main() { parent = _TestBodyComponent(); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () { - bloc = _MockGameBloc(); - const state = GameState( - totalScore: 0, - roundScore: 0, - multiplier: 1, - rounds: 3, - bonusHistory: [], - ); - whenListen(bloc, Stream.value(state), initialState: state); - return bloc; - }, - assets: assets, - ); + final flameBlocTester = FlameTester(_TestGame.new); group('ScoringBehavior', () { test('can be instantiated', () { @@ -75,16 +77,16 @@ void main() { ); }); - flameBlocTester.testGameWidget( + flameBlocTester.test( 'can be loaded', - setUp: (game, tester) async { - final canvas = ZCanvasComponent(children: [parent]); + (game) async { + await game.pump(parent); + final behavior = ScoringBehavior( points: Points.fiveThousand, position: Vector2.zero(), ); - await parent.add(behavior); - await game.ensureAdd(canvas); + await parent.ensureAdd(behavior); expect( parent.firstChild(), @@ -93,13 +95,12 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameBlocTester.test( 'emits Scored event with points when added', - setUp: (game, tester) async { - const points = Points.oneMillion; - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); + (game) async { + await game.pump(parent, gameBloc: bloc); + const points = Points.oneMillion; final behavior = ScoringBehavior( points: points, position: Vector2(0, 0), @@ -114,11 +115,10 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameBlocTester.test( 'correctly renders text', - setUp: (game, tester) async { - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); + (game) async { + await game.pump(parent); const points = Points.oneMillion; final position = Vector2.all(1); @@ -144,8 +144,8 @@ void main() { flameBlocTester.testGameWidget( 'is removed after duration', setUp: (game, tester) async { - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); + await game.onLoad(); + await game.pump(parent); const duration = 2.0; final behavior = ScoringBehavior( @@ -172,8 +172,8 @@ void main() { flameBlocTester.testGameWidget( 'beginContact adds a ScoringBehavior', setUp: (game, tester) async { - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); + await game.onLoad(); + await game.pump(parent); final behavior = ScoringContactBehavior(points: Points.oneMillion); await parent.ensureAdd(behavior); @@ -191,8 +191,8 @@ void main() { flameBlocTester.testGameWidget( "beginContact positions text at contact's position", setUp: (game, tester) async { - final canvas = ZCanvasComponent(children: [parent]); - await game.ensureAdd(canvas); + await game.onLoad(); + await game.pump(parent); final behavior = ScoringContactBehavior(points: Points.oneMillion); await parent.ensureAdd(behavior); diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 5927291b..3e5abb74 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -10,6 +10,34 @@ void main() { expect(gameBloc.state.rounds, equals(3)); }); + blocTest( + 'GameStarted starts the game', + build: GameBloc.new, + act: (bloc) => bloc.add(const GameStarted()), + expect: () => [ + isA() + ..having( + (state) => state.status, + 'status', + GameStatus.playing, + ), + ], + ); + + blocTest( + 'GameOver finishes the game', + build: GameBloc.new, + act: (bloc) => bloc.add(const GameOver()), + expect: () => [ + isA() + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), + ], + ); + group('RoundLost', () { blocTest( 'decreases number of rounds ' @@ -23,6 +51,24 @@ void main() { ], ); + blocTest( + 'sets game over when there are no more rounds', + build: GameBloc.new, + act: (bloc) { + bloc + ..add(const RoundLost()) + ..add(const RoundLost()) + ..add(const RoundLost()); + }, + expect: () => [ + isA()..having((state) => state.rounds, 'rounds', 2), + isA()..having((state) => state.rounds, 'rounds', 1), + isA() + ..having((state) => state.rounds, 'rounds', 0) + ..having((state) => state.status, 'status', GameStatus.gameOver), + ], + ); + blocTest( 'apply multiplier to roundScore and add it to totalScore ' 'when round is lost', @@ -33,6 +79,7 @@ void main() { multiplier: 3, rounds: 2, bonusHistory: [], + status: GameStatus.playing, ), act: (bloc) { bloc.add(const RoundLost()); @@ -45,8 +92,7 @@ void main() { ); blocTest( - 'resets multiplier ' - 'when round is lost', + 'resets multiplier when round is lost', build: GameBloc.new, seed: () => const GameState( totalScore: 10, @@ -54,6 +100,7 @@ void main() { multiplier: 3, rounds: 2, bonusHistory: [], + status: GameStatus.playing, ), act: (bloc) { bloc.add(const RoundLost()); @@ -66,25 +113,26 @@ void main() { group('Scored', () { blocTest( - 'increases score ' - 'when game is not over', + 'increases score when playing', build: GameBloc.new, act: (bloc) => bloc + ..add(const GameStarted()) ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ + isA() + ..having((state) => state.status, 'status', GameStatus.playing), isA() ..having((state) => state.roundScore, 'roundScore', 2) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having((state) => state.status, 'status', GameStatus.playing), isA() ..having((state) => state.roundScore, 'roundScore', 5) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having((state) => state.status, 'status', GameStatus.playing), ], ); blocTest( - "doesn't increase score " - 'when game is over', + "doesn't increase score when game is over", build: GameBloc.new, act: (bloc) { for (var i = 0; i < bloc.state.rounds; i++) { @@ -96,15 +144,27 @@ void main() { isA() ..having((state) => state.roundScore, 'roundScore', 0) ..having((state) => state.rounds, 'rounds', 2) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.roundScore, 'roundScore', 0) ..having((state) => state.rounds, 'rounds', 1) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.roundScore, 'roundScore', 0) ..having((state) => state.rounds, 'rounds', 0) - ..having((state) => state.isGameOver, 'isGameOver', true), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), ], ); }); @@ -115,15 +175,26 @@ void main() { 'when multiplier is below 6 and game is not over', build: GameBloc.new, act: (bloc) => bloc + ..add(const GameStarted()) ..add(const MultiplierIncreased()) ..add(const MultiplierIncreased()), expect: () => [ + isA() + ..having((state) => state.status, 'status', GameStatus.playing), isA() ..having((state) => state.multiplier, 'multiplier', 2) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.multiplier, 'multiplier', 3) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), ], ); @@ -137,6 +208,7 @@ void main() { multiplier: 6, rounds: 3, bonusHistory: [], + status: GameStatus.playing, ), act: (bloc) => bloc..add(const MultiplierIncreased()), expect: () => const [], @@ -147,21 +219,36 @@ void main() { 'when game is over', build: GameBloc.new, act: (bloc) { + bloc.add(const GameStarted()); for (var i = 0; i < bloc.state.rounds; i++) { bloc.add(const RoundLost()); } bloc.add(const MultiplierIncreased()); }, expect: () => [ + isA() + ..having((state) => state.status, 'status', GameStatus.playing), isA() ..having((state) => state.multiplier, 'multiplier', 1) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.multiplier, 'multiplier', 1) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.multiplier, 'multiplier', 1) - ..having((state) => state.isGameOver, 'isGameOver', true), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), ], ); }); diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index 6a39bd67..c4de5792 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -72,6 +72,32 @@ void main() { }); }); + group('GameStarted', () { + test('can be instantiated', () { + expect(const GameStarted(), isNotNull); + }); + + test('supports value equality', () { + expect( + GameStarted(), + equals(const GameStarted()), + ); + }); + }); + + group('GameOver', () { + test('can be instantiated', () { + expect(const GameOver(), isNotNull); + }); + + test('supports value equality', () { + expect( + GameOver(), + equals(const GameOver()), + ); + }); + }); + group('SparkyTurboChargeActivated', () { test('can be instantiated', () { expect(const SparkyTurboChargeActivated(), isNotNull); diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index b59115a3..670707a0 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -13,6 +13,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: const [], + status: GameStatus.waiting, ), equals( const GameState( @@ -21,6 +22,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ), ), ); @@ -35,6 +37,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ), isNotNull, ); @@ -52,6 +55,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: const [], + status: GameStatus.waiting, ), throwsAssertionError, ); @@ -69,6 +73,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: const [], + status: GameStatus.waiting, ), throwsAssertionError, ); @@ -86,6 +91,7 @@ void main() { multiplier: 0, rounds: 3, bonusHistory: const [], + status: GameStatus.waiting, ), throwsAssertionError, ); @@ -103,40 +109,13 @@ void main() { multiplier: 1, rounds: -1, bonusHistory: const [], + status: GameStatus.waiting, ), throwsAssertionError, ); }, ); - group('isGameOver', () { - test( - 'is true ' - 'when no rounds are left', () { - const gameState = GameState( - totalScore: 0, - roundScore: 0, - multiplier: 1, - rounds: 0, - bonusHistory: [], - ); - expect(gameState.isGameOver, isTrue); - }); - - test( - 'is false ' - 'when one 1 round left', () { - const gameState = GameState( - totalScore: 0, - roundScore: 0, - multiplier: 1, - rounds: 1, - bonusHistory: [], - ); - expect(gameState.isGameOver, isFalse); - }); - }); - group('copyWith', () { test( 'throws AssertionError ' @@ -148,6 +127,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ); expect( () => gameState.copyWith(totalScore: gameState.totalScore - 1), @@ -166,6 +146,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ); expect( gameState.copyWith(), @@ -184,6 +165,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ); final otherGameState = GameState( totalScore: gameState.totalScore + 1, @@ -191,6 +173,7 @@ void main() { multiplier: gameState.multiplier + 1, rounds: gameState.rounds + 1, bonusHistory: const [GameBonus.googleWord], + status: GameStatus.playing, ); expect(gameState, isNot(equals(otherGameState))); @@ -201,6 +184,7 @@ void main() { multiplier: otherGameState.multiplier, rounds: otherGameState.rounds, bonusHistory: otherGameState.bonusHistory, + status: otherGameState.status, ), equals(otherGameState), ); diff --git a/test/game/components/android_acres/android_acres_test.dart b/test/game/components/android_acres/android_acres_test.dart index 8434d5f8..e88d1608 100644 --- a/test/game/components/android_acres/android_acres_test.dart +++ b/test/game/components/android_acres/android_acres_test.dart @@ -1,56 +1,70 @@ // ignore_for_file: cascade_invocations +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/behaviors/bumper_noisy_behavior.dart'; +import 'package:pinball/game/behaviors/bumper_noise_behavior.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 '../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + 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.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, + ]); + } + + Future pump(AndroidAcres child) async { + await ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [child], + ), + ); + } +} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.android.spaceship.saucer.keyName, - Assets.images.android.spaceship.animatronic.keyName, - Assets.images.android.spaceship.lightBeam.keyName, - 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.android.bumper.a.lit.keyName, - Assets.images.android.bumper.a.dimmed.keyName, - Assets.images.android.bumper.b.lit.keyName, - Assets.images.android.bumper.b.dimmed.keyName, - Assets.images.android.bumper.cow.lit.keyName, - Assets.images.android.bumper.cow.dimmed.keyName, - ]; group('AndroidAcres', () { - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); + final flameTester = FlameTester(_TestGame.new); flameTester.test('loads correctly', (game) async { final component = AndroidAcres(); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); + await game.pump(component); + expect(game.descendants(), contains(component)); }); group('loads', () { flameTester.test( 'an AndroidSpaceship', (game) async { - await game.ensureAdd(AndroidAcres()); + await game.pump(AndroidAcres()); expect( game.descendants().whereType().length, equals(1), @@ -61,7 +75,7 @@ void main() { flameTester.test( 'an AndroidAnimatronic', (game) async { - await game.ensureAdd(AndroidAcres()); + await game.pump(AndroidAcres()); expect( game.descendants().whereType().length, equals(1), @@ -72,7 +86,7 @@ void main() { flameTester.test( 'a SpaceshipRamp', (game) async { - await game.ensureAdd(AndroidAcres()); + await game.pump(AndroidAcres()); expect( game.descendants().whereType().length, equals(1), @@ -83,7 +97,7 @@ void main() { flameTester.test( 'a SpaceshipRail', (game) async { - await game.ensureAdd(AndroidAcres()); + await game.pump(AndroidAcres()); expect( game.descendants().whereType().length, equals(1), @@ -94,7 +108,7 @@ void main() { flameTester.test( 'three AndroidBumper', (game) async { - await game.ensureAdd(AndroidAcres()); + await game.pump(AndroidAcres()); expect( game.descendants().whereType().length, equals(3), @@ -103,13 +117,13 @@ void main() { ); flameTester.test( - 'three AndroidBumpers with BumperNoisyBehavior', + 'three AndroidBumpers with BumperNoiseBehavior', (game) async { - await game.ensureAdd(AndroidAcres()); + await game.pump(AndroidAcres()); final bumpers = game.descendants().whereType(); for (final bumper in bumpers) { expect( - bumper.firstChild(), + bumper.firstChild(), isNotNull, ); } @@ -119,7 +133,7 @@ void main() { flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async { final androidAcres = AndroidAcres(); - await game.ensureAdd(androidAcres); + await game.pump(androidAcres); expect( androidAcres.children.whereType().single, isNotNull, diff --git a/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart b/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart index 6be120d5..4ecdb05b 100644 --- a/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart +++ b/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: cascade_invocations -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/extensions.dart'; +import 'package:flame_bloc/flame_bloc.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'; @@ -9,55 +9,63 @@ import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + 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.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, + ]); + } + + Future pump( + AndroidAcres child, { + required GameBloc gameBloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [child], + ), + ); + } +} class _MockGameBloc extends Mock implements GameBloc {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.android.spaceship.saucer.keyName, - Assets.images.android.spaceship.animatronic.keyName, - Assets.images.android.spaceship.lightBeam.keyName, - 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.android.bumper.a.lit.keyName, - Assets.images.android.bumper.a.dimmed.keyName, - Assets.images.android.bumper.b.lit.keyName, - Assets.images.android.bumper.b.dimmed.keyName, - Assets.images.android.bumper.cow.lit.keyName, - Assets.images.android.bumper.cow.dimmed.keyName, - ]; group('AndroidSpaceshipBonusBehavior', () { 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, - ); + final flameTester = FlameTester(_TestGame.new); - flameBlocTester.testGameWidget( + flameTester.testGameWidget( 'adds GameBonus.androidSpaceship to the game ' 'when android spacehship has a bonus', setUp: (game, tester) async { @@ -66,7 +74,10 @@ void main() { final androidSpaceship = AndroidSpaceship(position: Vector2.zero()); await parent.add(androidSpaceship); - await game.ensureAdd(parent); + await game.pump( + parent, + gameBloc: gameBloc, + ); await parent.ensureAdd(behavior); androidSpaceship.bloc.onBallEntered(); diff --git a/test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart b/test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart new file mode 100644 index 00000000..f41487cd --- /dev/null +++ b/test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart @@ -0,0 +1,142 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.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/game/behaviors/ball_spawning_behavior.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; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.load(theme.Assets.images.dash.ball.keyName); + } + + Future pump( + Iterable children, { + GameBloc? gameBloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc ?? GameBloc(), + children: [ + FlameProvider.value( + const theme.DashTheme(), + children: children, + ), + ], + ), + ); + } +} + +class _MockGameState extends Mock implements GameState {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'BallSpawningBehavior', + () { + final flameTester = FlameTester(_TestGame.new); + + test('can be instantiated', () { + expect( + BallSpawningBehavior(), + isA(), + ); + }); + + flameTester.test( + 'loads', + (game) async { + final behavior = BallSpawningBehavior(); + await game.pump([behavior]); + expect(game.descendants(), contains(behavior)); + }, + ); + + group('listenWhen', () { + test( + 'never listens when new state not playing', + () { + final waiting = const GameState.initial() + ..copyWith(status: GameStatus.waiting); + final gameOver = const GameState.initial() + ..copyWith(status: GameStatus.gameOver); + + final behavior = BallSpawningBehavior(); + expect(behavior.listenWhen(_MockGameState(), waiting), isFalse); + expect(behavior.listenWhen(_MockGameState(), gameOver), isFalse); + }, + ); + + test( + 'listens when started playing', + () { + final waiting = + const GameState.initial().copyWith(status: GameStatus.waiting); + final playing = + const GameState.initial().copyWith(status: GameStatus.playing); + + final behavior = BallSpawningBehavior(); + expect(behavior.listenWhen(waiting, playing), isTrue); + }, + ); + + test( + 'listens when lost rounds', + () { + final playing1 = const GameState.initial().copyWith( + status: GameStatus.playing, + rounds: 2, + ); + final playing2 = const GameState.initial().copyWith( + status: GameStatus.playing, + rounds: 1, + ); + + final behavior = BallSpawningBehavior(); + expect(behavior.listenWhen(playing1, playing2), isTrue); + }, + ); + + test( + "doesn't listen when didn't lose any rounds", + () { + final playing = const GameState.initial().copyWith( + status: GameStatus.playing, + rounds: 2, + ); + + final behavior = BallSpawningBehavior(); + expect(behavior.listenWhen(playing, playing), isFalse); + }, + ); + }); + + flameTester.test( + 'onNewState adds a ball', + (game) async { + final behavior = BallSpawningBehavior(); + await game.pump([ + behavior, + ZCanvasComponent(), + Plunger.test(compressionDistance: 10), + ]); + expect(game.descendants().whereType(), isEmpty); + + behavior.onNewState(_MockGameState()); + await game.ready(); + + expect(game.descendants().whereType(), isNotEmpty); + }, + ); + }, + ); +} 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 index acd17717..cb6c2784 100644 --- a/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart +++ b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.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'; @@ -12,7 +14,41 @@ 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 _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + 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, + ]); + } + + Future pump( + SpaceshipRamp child, { + required GameBloc gameBloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [ + ZCanvasComponent(children: [child]), + ], + ), + ); + } +} class _MockGameBloc extends Mock implements GameBloc {} @@ -23,21 +59,6 @@ class _MockStreamSubscription extends Mock 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; @@ -46,22 +67,13 @@ void main() { setUp(() { gameBloc = _MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + final flameTester = FlameTester(_TestGame.new); - flameBlocTester.testGameWidget( + flameTester.test( 'when hits are multiples of 10 times adds a ScoringBehavior', - setUp: (game, tester) async { + (game) async { final bloc = _MockSpaceshipRampCubit(); final streamController = StreamController(); whenListen( @@ -69,14 +81,13 @@ void main() { streamController.stream, initialState: SpaceshipRampState(hits: 9), ); - final behavior = RampBonusBehavior( - points: shotPoints, - ); - final parent = SpaceshipRamp.test( - bloc: bloc, - ); + final behavior = RampBonusBehavior(points: shotPoints); + final parent = SpaceshipRamp.test(bloc: bloc); - await game.ensureAdd(ZCanvasComponent(children: [parent])); + await game.pump( + parent, + gameBloc: gameBloc, + ); await parent.ensureAdd(behavior); streamController.add(SpaceshipRampState(hits: 10)); @@ -88,9 +99,9 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameTester.test( "when hits are not multiple of 10 times doesn't add any ScoringBehavior", - setUp: (game, tester) async { + (game) async { final bloc = _MockSpaceshipRampCubit(); final streamController = StreamController(); whenListen( @@ -98,14 +109,13 @@ void main() { streamController.stream, initialState: SpaceshipRampState.initial(), ); - final behavior = RampBonusBehavior( - points: shotPoints, - ); - final parent = SpaceshipRamp.test( - bloc: bloc, - ); + final behavior = RampBonusBehavior(points: shotPoints); + final parent = SpaceshipRamp.test(bloc: bloc); - await game.ensureAdd(ZCanvasComponent(children: [parent])); + await game.pump( + parent, + gameBloc: gameBloc, + ); await parent.ensureAdd(behavior); streamController.add(SpaceshipRampState(hits: 1)); @@ -117,9 +127,9 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameTester.test( 'closes subscription when removed', - setUp: (game, tester) async { + (game) async { final bloc = _MockSpaceshipRampCubit(); whenListen( bloc, @@ -135,11 +145,12 @@ void main() { points: shotPoints, subscription: subscription, ); - final parent = SpaceshipRamp.test( - bloc: bloc, - ); + final parent = SpaceshipRamp.test(bloc: bloc); - await game.ensureAdd(ZCanvasComponent(children: [parent])); + await game.pump( + parent, + gameBloc: gameBloc, + ); await parent.ensureAdd(behavior); parent.remove(behavior); 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 index 23f02220..ae072ea4 100644 --- a/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart +++ b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.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'; @@ -12,7 +14,41 @@ 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 _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + 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, + ]); + } + + Future pump( + SpaceshipRamp child, { + required GameBloc gameBloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [ + ZCanvasComponent(children: [child]), + ], + ), + ); + } +} class _MockGameBloc extends Mock implements GameBloc {} @@ -23,21 +59,6 @@ class _MockStreamSubscription extends Mock 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; @@ -46,23 +67,14 @@ void main() { setUp(() { gameBloc = _MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + final flameBlocTester = FlameTester(_TestGame.new); - flameBlocTester.testGameWidget( + flameBlocTester.test( 'when hits are not multiple of 10 times ' 'increases multiplier and adds a ScoringBehavior', - setUp: (game, tester) async { + (game) async { final bloc = _MockSpaceshipRampCubit(); final streamController = StreamController(); whenListen( @@ -70,14 +82,13 @@ void main() { streamController.stream, initialState: SpaceshipRampState.initial(), ); - final behavior = RampShotBehavior( - points: shotPoints, - ); - final parent = SpaceshipRamp.test( - bloc: bloc, - ); + final behavior = RampShotBehavior(points: shotPoints); + final parent = SpaceshipRamp.test(bloc: bloc); - await game.ensureAdd(ZCanvasComponent(children: [parent])); + await game.pump( + parent, + gameBloc: gameBloc, + ); await parent.ensureAdd(behavior); streamController.add(SpaceshipRampState(hits: 1)); @@ -90,10 +101,10 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameBlocTester.test( 'when hits multiple of 10 times ' "doesn't increase multiplier, neither ScoringBehavior", - setUp: (game, tester) async { + (game) async { final bloc = _MockSpaceshipRampCubit(); final streamController = StreamController(); whenListen( @@ -108,7 +119,10 @@ void main() { bloc: bloc, ); - await game.ensureAdd(ZCanvasComponent(children: [parent])); + await game.pump( + parent, + gameBloc: gameBloc, + ); await parent.ensureAdd(behavior); streamController.add(SpaceshipRampState(hits: 10)); @@ -121,9 +135,9 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameBlocTester.test( 'closes subscription when removed', - setUp: (game, tester) async { + (game) async { final bloc = _MockSpaceshipRampCubit(); whenListen( bloc, @@ -143,7 +157,10 @@ void main() { bloc: bloc, ); - await game.ensureAdd(ZCanvasComponent(children: [parent])); + await game.pump( + parent, + gameBloc: gameBloc, + ); await parent.ensureAdd(behavior); parent.remove(behavior); diff --git a/test/game/components/backbox/backbox_test.dart b/test/game/components/backbox/backbox_test.dart index 341198f8..52e2746e 100644 --- a/test/game/components/backbox/backbox_test.dart +++ b/test/game/components/backbox/backbox_test.dart @@ -1,16 +1,73 @@ -// ignore_for_file: cascade_invocations +// ignore_for_file: cascade_invocations, prefer_const_constructors -import 'package:flame/components.dart'; +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/input.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.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:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/components/backbox/displays/initials_input_display.dart'; +import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart'; +import 'package:pinball/game/components/backbox/displays/displays.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.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 _TestGame extends Forge2DGame with HasKeyboardHandlerComponents { + final character = theme.DashTheme(); + + @override + Color backgroundColor() => Colors.transparent; + + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + character.leaderboardIcon.keyName, + Assets.images.backbox.marquee.keyName, + Assets.images.backbox.displayDivider.keyName, + ]); + } + + Future pump(Backbox component) { + return ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [ + FlameProvider.value( + _MockAppLocalizations(), + children: [component], + ), + ], + ), + ); + } +} + +class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) { + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn(key); + return event; +} + +class _MockBackboxBloc extends Mock implements BackboxBloc {} + +class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { +} class _MockAppLocalizations extends Mock implements AppLocalizations { @override @@ -33,65 +90,176 @@ class _MockAppLocalizations extends Mock implements AppLocalizations { @override String get toSubmit => ''; + + @override + String get loading => ''; } 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(), - ), - ); + + final flameTester = FlameTester(_TestGame.new); + + late BackboxBloc bloc; + + setUp(() { + bloc = _MockBackboxBloc(); + whenListen( + bloc, + Stream.value(LoadingState()), + initialState: LoadingState(), + ); + }); group('Backbox', () { flameTester.test( 'loads correctly', (game) async { - final backbox = Backbox(); - await game.ensureAdd(backbox); - - expect(game.children, contains(backbox)); + final backbox = Backbox.test(bloc: bloc); + await game.pump(backbox); + expect(game.descendants(), contains(backbox)); }, ); flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); game.camera ..followVector2(Vector2(0, -130)) ..zoom = 6; - await game.ensureAdd(Backbox()); + await game.pump( + Backbox.test(bloc: bloc), + ); await tester.pump(); }, verify: (game, tester) async { await expectLater( - find.byGame(), + find.byGame<_TestGame>(), matchesGoldenFile('../golden/backbox.png'), ); }, ); flameTester.test( - 'initialsInput adds InitialsInputDisplay', + 'requestInitials adds InitialsInputDisplay', (game) async { - final backbox = Backbox(); - await game.ensureAdd(backbox); - await backbox.initialsInput( + final backbox = Backbox.test( + bloc: BackboxBloc( + leaderboardRepository: _MockLeaderboardRepository(), + ), + ); + await game.pump(backbox); + backbox.requestInitials( score: 0, - characterIconPath: characterIconPath, - onSubmit: (_) {}, + character: game.character, + ); + await game.ready(); + + expect( + backbox.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'adds PlayerInitialsSubmitted when initials are submitted', + (game) async { + final bloc = _MockBackboxBloc(); + final state = InitialsFormState( + score: 10, + character: game.character, + ); + whenListen( + bloc, + Stream.value(state), + initialState: state, + ); + final backbox = Backbox.test(bloc: bloc); + await game.pump(backbox); + + game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {}); + verify( + () => bloc.add( + PlayerInitialsSubmitted( + score: 10, + initials: 'AAA', + character: game.character, + ), + ), + ).called(1); + }, + ); + + flameTester.test( + 'adds InitialsSubmissionSuccessDisplay on InitialsSuccessState', + (game) async { + whenListen( + bloc, + Stream.value(InitialsSuccessState()), + initialState: InitialsSuccessState(), + ); + final backbox = Backbox.test(bloc: bloc); + await game.pump(backbox); + + expect( + game + .descendants() + .whereType() + .length, + equals(1), + ); + }, + ); + + flameTester.test( + 'adds InitialsSubmissionFailureDisplay on InitialsFailureState', + (game) async { + whenListen( + bloc, + Stream.value(InitialsFailureState()), + initialState: InitialsFailureState(), + ); + final backbox = Backbox.test(bloc: bloc); + await game.pump(backbox); + + expect( + game + .descendants() + .whereType() + .length, + equals(1), + ); + }, + ); + + flameTester.test( + 'closes the subscription when it is removed', + (game) async { + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: LoadingState(), ); + + final backbox = Backbox.test(bloc: bloc); + await game.pump(backbox); + + backbox.removeFromParent(); + await game.ready(); + + streamController.add(InitialsFailureState()); await game.ready(); - expect(backbox.firstChild(), isNotNull); + expect( + backbox + .descendants() + .whereType() + .isEmpty, + isTrue, + ); }, ); }); diff --git a/test/game/components/backbox/bloc/backbox_bloc_test.dart b/test/game/components/backbox/bloc/backbox_bloc_test.dart new file mode 100644 index 00000000..c2fbc088 --- /dev/null +++ b/test/game/components/backbox/bloc/backbox_bloc_test.dart @@ -0,0 +1,92 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { +} + +void main() { + late LeaderboardRepository leaderboardRepository; + + group('BackboxBloc', () { + blocTest( + 'adds InitialsFormState on PlayerInitialsRequested', + setUp: () { + leaderboardRepository = _MockLeaderboardRepository(); + }, + build: () => BackboxBloc(leaderboardRepository: leaderboardRepository), + act: (bloc) => bloc.add( + PlayerInitialsRequested( + score: 100, + character: AndroidTheme(), + ), + ), + expect: () => [ + InitialsFormState(score: 100, character: AndroidTheme()), + ], + ); + + group('PlayerInitialsSubmitted', () { + blocTest( + 'adds [LoadingState, InitialsSuccessState] when submission succeeds', + setUp: () { + leaderboardRepository = _MockLeaderboardRepository(); + when( + () => leaderboardRepository.addLeaderboardEntry( + LeaderboardEntryData( + playerInitials: 'AAA', + score: 10, + character: CharacterType.dash, + ), + ), + ).thenAnswer((_) async {}); + }, + build: () => BackboxBloc(leaderboardRepository: leaderboardRepository), + act: (bloc) => bloc.add( + PlayerInitialsSubmitted( + score: 10, + initials: 'AAA', + character: DashTheme(), + ), + ), + expect: () => [ + LoadingState(), + InitialsSuccessState(), + ], + ); + + blocTest( + 'adds [LoadingState, InitialsFailureState] when submission fails', + setUp: () { + leaderboardRepository = _MockLeaderboardRepository(); + when( + () => leaderboardRepository.addLeaderboardEntry( + LeaderboardEntryData( + playerInitials: 'AAA', + score: 10, + character: CharacterType.dash, + ), + ), + ).thenThrow(Exception('Error')); + }, + build: () => BackboxBloc(leaderboardRepository: leaderboardRepository), + act: (bloc) => bloc.add( + PlayerInitialsSubmitted( + score: 10, + initials: 'AAA', + character: DashTheme(), + ), + ), + expect: () => [ + LoadingState(), + InitialsFailureState(), + ], + ); + }); + }); +} diff --git a/test/game/components/backbox/bloc/backbox_event_test.dart b/test/game/components/backbox/bloc/backbox_event_test.dart new file mode 100644 index 00000000..5fc766a9 --- /dev/null +++ b/test/game/components/backbox/bloc/backbox_event_test.dart @@ -0,0 +1,126 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +void main() { + group('BackboxEvent', () { + group('PlayerInitialsRequested', () { + test('can be instantiated', () { + expect( + PlayerInitialsRequested(score: 0, character: AndroidTheme()), + isNotNull, + ); + }); + + test('supports value comparison', () { + expect( + PlayerInitialsRequested(score: 0, character: AndroidTheme()), + equals( + PlayerInitialsRequested(score: 0, character: AndroidTheme()), + ), + ); + + expect( + PlayerInitialsRequested(score: 0, character: AndroidTheme()), + isNot( + equals( + PlayerInitialsRequested(score: 1, character: AndroidTheme()), + ), + ), + ); + + expect( + PlayerInitialsRequested(score: 0, character: AndroidTheme()), + isNot( + equals( + PlayerInitialsRequested(score: 0, character: SparkyTheme()), + ), + ), + ); + }); + }); + + group('PlayerInitialsSubmitted', () { + test('can be instantiated', () { + expect( + PlayerInitialsSubmitted( + score: 0, + initials: 'AAA', + character: AndroidTheme(), + ), + isNotNull, + ); + }); + + test('supports value comparison', () { + expect( + PlayerInitialsSubmitted( + score: 0, + initials: 'AAA', + character: AndroidTheme(), + ), + equals( + PlayerInitialsSubmitted( + score: 0, + initials: 'AAA', + character: AndroidTheme(), + ), + ), + ); + + expect( + PlayerInitialsSubmitted( + score: 0, + initials: 'AAA', + character: AndroidTheme(), + ), + isNot( + equals( + PlayerInitialsSubmitted( + score: 1, + initials: 'AAA', + character: AndroidTheme(), + ), + ), + ), + ); + + expect( + PlayerInitialsSubmitted( + score: 0, + initials: 'AAA', + character: AndroidTheme(), + ), + isNot( + equals( + PlayerInitialsSubmitted( + score: 0, + initials: 'AAA', + character: SparkyTheme(), + ), + ), + ), + ); + + expect( + PlayerInitialsSubmitted( + score: 0, + initials: 'AAA', + character: AndroidTheme(), + ), + isNot( + equals( + PlayerInitialsSubmitted( + score: 0, + initials: 'BBB', + character: AndroidTheme(), + ), + ), + ), + ); + }); + }); + }); +} diff --git a/test/game/components/backbox/bloc/backbox_state_test.dart b/test/game/components/backbox/bloc/backbox_state_test.dart new file mode 100644 index 00000000..4708c9bb --- /dev/null +++ b/test/game/components/backbox/bloc/backbox_state_test.dart @@ -0,0 +1,116 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +void main() { + group('BackboxState', () { + group('LoadingState', () { + test('can be instantiated', () { + expect(LoadingState(), isNotNull); + }); + + test('supports value comparison', () { + expect(LoadingState(), equals(LoadingState())); + }); + }); + + group('LeaderboardSuccessState', () { + test('can be instantiated', () { + expect(LeaderboardSuccessState(), isNotNull); + }); + + test('supports value comparison', () { + expect(LeaderboardSuccessState(), equals(LeaderboardSuccessState())); + }); + }); + + group('LeaderboardFailureState', () { + test('can be instantiated', () { + expect(LeaderboardFailureState(), isNotNull); + }); + + test('supports value comparison', () { + expect(LeaderboardFailureState(), equals(LeaderboardFailureState())); + }); + }); + + group('InitialsFormState', () { + test('can be instantiated', () { + expect( + InitialsFormState( + score: 0, + character: AndroidTheme(), + ), + isNotNull, + ); + }); + + test('supports value comparison', () { + expect( + InitialsFormState( + score: 0, + character: AndroidTheme(), + ), + equals( + InitialsFormState( + score: 0, + character: AndroidTheme(), + ), + ), + ); + + expect( + InitialsFormState( + score: 0, + character: AndroidTheme(), + ), + isNot( + equals( + InitialsFormState( + score: 1, + character: AndroidTheme(), + ), + ), + ), + ); + + expect( + InitialsFormState( + score: 0, + character: AndroidTheme(), + ), + isNot( + equals( + InitialsFormState( + score: 0, + character: SparkyTheme(), + ), + ), + ), + ); + }); + }); + + group('InitialsSuccessState', () { + test('can be instantiated', () { + expect(InitialsSuccessState(), isNotNull); + }); + + test('supports value comparison', () { + expect(InitialsSuccessState(), equals(InitialsSuccessState())); + }); + + group('InitialsFailureState', () { + test('can be instantiated', () { + expect(InitialsFailureState(), isNotNull); + }); + + test('supports value comparison', () { + expect(InitialsFailureState(), equals(InitialsFailureState())); + }); + }); + }); + }); +} diff --git a/test/game/components/backbox/displays/initials_input_display_test.dart b/test/game/components/backbox/displays/initials_input_display_test.dart index 993e0678..1b92aedd 100644 --- a/test/game/components/backbox/displays/initials_input_display_test.dart +++ b/test/game/components/backbox/displays/initials_input_display_test.dart @@ -1,17 +1,50 @@ // ignore_for_file: cascade_invocations import 'package:flame/components.dart'; +import 'package:flame/input.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.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/bloc/game_bloc.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_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; -import '../../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents { + final characterIconPath = theme.Assets.images.dash.leaderboardIcon.keyName; + + @override + Future onLoad() async { + await super.onLoad(); + images.prefix = ''; + await images.loadAll( + [ + characterIconPath, + Assets.images.backbox.displayDivider.keyName, + ], + ); + } + + Future pump(InitialsInputDisplay component) { + return ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [ + FlameProvider.value( + _MockAppLocalizations(), + children: [component], + ), + ], + ), + ); + } +} class _MockAppLocalizations extends Mock implements AppLocalizations { @override @@ -38,71 +71,61 @@ class _MockAppLocalizations extends Mock implements AppLocalizations { 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(), - ), - ); + + final flameTester = FlameTester(_TestGame.new); group('InitialsInputDisplay', () { flameTester.test( 'loads correctly', (game) async { - final initialsInputDisplay = InitialsInputDisplay( + final component = InitialsInputDisplay( score: 0, - characterIconPath: characterIconPath, + characterIconPath: game.characterIconPath, onSubmit: (_) {}, ); - await game.ensureAdd(initialsInputDisplay); - - expect(game.children, contains(initialsInputDisplay)); + await game.pump(component); + expect(game.descendants(), contains(component)); }, ); flameTester.testGameWidget( 'can change the initials', setUp: (game, tester) async { - await game.images.loadAll(assets); - final initialsInputDisplay = InitialsInputDisplay( + await game.onLoad(); + final component = InitialsInputDisplay( score: 1000, - characterIconPath: characterIconPath, + characterIconPath: game.characterIconPath, onSubmit: (_) {}, ); - await game.ensureAdd(initialsInputDisplay); + await game.pump(component); - // Focus is already on the first letter - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + // Focus is on the first letter + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); - // Move to the next an press up again + // Move to the next an press down again await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); // One more time await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); - // Back to the previous and increase one more + // Back to the previous and press down again await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); }, verify: (game, tester) async { - final initialsInputDisplay = + final component = game.descendants().whereType().single; - expect(initialsInputDisplay.initials, equals('BCB')); + expect(component.initials, equals('BCB')); }, ); @@ -110,15 +133,15 @@ void main() { flameTester.testGameWidget( 'submits the initials', setUp: (game, tester) async { - await game.images.loadAll(assets); - final initialsInputDisplay = InitialsInputDisplay( + await game.onLoad(); + final component = InitialsInputDisplay( score: 1000, - characterIconPath: characterIconPath, + characterIconPath: game.characterIconPath, onSubmit: (value) { submitedInitials = value; }, ); - await game.ensureAdd(initialsInputDisplay); + await game.pump(component); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); @@ -132,18 +155,18 @@ void main() { flameTester.testGameWidget( 'cycles the char up and down when it has focus', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); await game.ensureAdd( InitialsLetterPrompt(hasFocus: true, position: Vector2.zero()), ); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); - await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pump(); }, verify: (game, tester) async { final prompt = game.firstChild(); @@ -154,7 +177,7 @@ void main() { flameTester.testGameWidget( "does nothing when it doesn't have focus", setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); await game.ensureAdd( InitialsLetterPrompt(position: Vector2.zero()), ); @@ -170,7 +193,7 @@ void main() { flameTester.testGameWidget( 'blinks the prompt when it has the focus', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); await game.ensureAdd( InitialsLetterPrompt(position: Vector2.zero(), hasFocus: true), ); diff --git a/test/game/components/backbox/displays/initials_submission_failure_display_test.dart b/test/game/components/backbox/displays/initials_submission_failure_display_test.dart new file mode 100644 index 00000000..b37b41e7 --- /dev/null +++ b/test/game/components/backbox/displays/initials_submission_failure_display_test.dart @@ -0,0 +1,21 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/components/backbox/displays/initials_submission_failure_display.dart'; + +void main() { + group('InitialsSubmissionFailureDisplay', () { + final flameTester = FlameTester(Forge2DGame.new); + + flameTester.test('renders correctly', (game) async { + await game.ensureAdd(InitialsSubmissionFailureDisplay()); + + final component = game.firstChild(); + expect(component, isNotNull); + expect(component?.text, equals('Failure!')); + }); + }); +} diff --git a/test/game/components/backbox/displays/initials_submission_success_display_test.dart b/test/game/components/backbox/displays/initials_submission_success_display_test.dart new file mode 100644 index 00000000..7ad3d182 --- /dev/null +++ b/test/game/components/backbox/displays/initials_submission_success_display_test.dart @@ -0,0 +1,21 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/components/backbox/displays/initials_submission_success_display.dart'; + +void main() { + group('InitialsSubmissionSuccessDisplay', () { + final flameTester = FlameTester(Forge2DGame.new); + + flameTester.test('renders correctly', (game) async { + await game.ensureAdd(InitialsSubmissionSuccessDisplay()); + + final component = game.firstChild(); + expect(component, isNotNull); + expect(component?.text, equals('Success!')); + }); + }); +} diff --git a/test/game/components/backbox/displays/loading_display_test.dart b/test/game/components/backbox/displays/loading_display_test.dart new file mode 100644 index 00000000..efd84097 --- /dev/null +++ b/test/game/components/backbox/displays/loading_display_test.dart @@ -0,0 +1,68 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.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/game/bloc/game_bloc.dart'; +import 'package:pinball/game/components/backbox/displays/loading_display.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + Future pump(LoadingDisplay component) { + return ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [ + FlameProvider.value( + _MockAppLocalizations(), + children: [component], + ), + ], + ), + ); + } +} + +class _MockAppLocalizations extends Mock implements AppLocalizations { + @override + String get loading => 'Loading'; +} + +void main() { + group('LoadingDisplay', () { + final flameTester = FlameTester(_TestGame.new); + + flameTester.test('renders correctly', (game) async { + await game.pump(LoadingDisplay()); + + final component = game.descendants().whereType().first; + expect(component, isNotNull); + expect(component.text, equals('Loading')); + }); + + flameTester.test('use ellipses as animation', (game) async { + await game.pump(LoadingDisplay()); + + final component = game.descendants().whereType().first; + expect(component.text, equals('Loading')); + + final timer = component.firstChild(); + + timer?.update(1.1); + expect(component.text, equals('Loading.')); + + timer?.update(1.1); + expect(component.text, equals('Loading..')); + + timer?.update(1.1); + expect(component.text, equals('Loading...')); + + timer?.update(1.1); + expect(component.text, equals('Loading')); + }); + }); +} diff --git a/test/game/components/bottom_group_test.dart b/test/game/components/bottom_group_test.dart index 1d9e58ab..fab8dfaf 100644 --- a/test/game/components/bottom_group_test.dart +++ b/test/game/components/bottom_group_test.dart @@ -1,36 +1,47 @@ // ignore_for_file: cascade_invocations +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.kicker.left.lit.keyName, + Assets.images.kicker.left.dimmed.keyName, + Assets.images.kicker.right.lit.keyName, + Assets.images.kicker.right.dimmed.keyName, + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ]); + } +} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.kicker.left.lit.keyName, - Assets.images.kicker.left.dimmed.keyName, - Assets.images.kicker.right.lit.keyName, - Assets.images.kicker.right.dimmed.keyName, - Assets.images.baseboard.left.keyName, - Assets.images.baseboard.right.keyName, - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - ]; - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); group('BottomGroup', () { + final flameTester = FlameTester(_TestGame.new); + flameTester.test( 'loads correctly', (game) async { final bottomGroup = BottomGroup(); - await game.ensureAdd(bottomGroup); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [bottomGroup], + ), + ); - expect(game.contains(bottomGroup), isTrue); + expect(game.descendants(), contains(bottomGroup)); }, ); @@ -39,7 +50,12 @@ void main() { 'one left flipper', (game) async { final bottomGroup = BottomGroup(); - await game.ensureAdd(bottomGroup); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [bottomGroup], + ), + ); final leftFlippers = bottomGroup.descendants().whereType().where( @@ -53,7 +69,12 @@ void main() { 'one right flipper', (game) async { final bottomGroup = BottomGroup(); - await game.ensureAdd(bottomGroup); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [bottomGroup], + ), + ); final rightFlippers = bottomGroup.descendants().whereType().where( @@ -67,7 +88,12 @@ void main() { 'two Baseboards', (game) async { final bottomGroup = BottomGroup(); - await game.ensureAdd(bottomGroup); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [bottomGroup], + ), + ); final basebottomGroups = bottomGroup.descendants().whereType(); @@ -79,7 +105,12 @@ void main() { 'two Kickers', (game) async { final bottomGroup = BottomGroup(); - await game.ensureAdd(bottomGroup); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [bottomGroup], + ), + ); final kickers = bottomGroup.descendants().whereType(); expect(kickers.length, equals(2)); diff --git a/test/game/components/camera_controller_test.dart b/test/game/components/camera_controller_test.dart deleted file mode 100644 index 934f6340..00000000 --- a/test/game/components/camera_controller_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame/game.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/components/camera_controller.dart'; -import 'package:pinball_components/pinball_components.dart'; - -void main() { - group('CameraController', () { - late FlameGame game; - late CameraController controller; - - setUp(() async { - game = FlameGame()..onGameResize(Vector2(100, 200)); - - controller = CameraController(game); - await game.ensureAdd(controller); - }); - - test('loads correctly', () async { - expect(game.firstChild(), isNotNull); - }); - - test('correctly calculates the zooms', () async { - expect(controller.gameFocus.zoom.toInt(), equals(12)); - expect(controller.waitingBackboxFocus.zoom.toInt(), equals(11)); - }); - - test('correctly sets the initial zoom and position', () async { - expect(game.camera.zoom, equals(controller.waitingBackboxFocus.zoom)); - expect( - game.camera.follow, - equals(controller.waitingBackboxFocus.position), - ); - }); - - group('focusOnGame', () { - test('changes the zoom', () async { - controller.focusOnGame(); - - await game.ready(); - final zoom = game.firstChild(); - expect(zoom, isNotNull); - expect(zoom?.value, equals(controller.gameFocus.zoom)); - }); - - test('moves the camera after the zoom is completed', () async { - controller.focusOnGame(); - 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(-4, -120)); - }); - }); - - group('focusOnWaitingBackbox', () { - test('changes the zoom', () async { - controller.focusOnWaitingBackbox(); - - await game.ready(); - final zoom = game.firstChild(); - expect(zoom, isNotNull); - expect(zoom?.value, equals(controller.waitingBackboxFocus.zoom)); - }); - - test('moves the camera after the zoom is completed', () async { - controller.focusOnWaitingBackbox(); - 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(-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 dc142ffd..95451515 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: cascade_invocations -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.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'; @@ -9,32 +9,29 @@ 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'; - -// TODO(allisonryan0002): remove once -// https://github.com/flame-engine/flame/pull/1520 is merged -class _WrappedBallController extends BallController { - _WrappedBallController(Ball ball, this._gameRef) : super(ball); - - final PinballGame _gameRef; - +class _TestGame extends Forge2DGame { @override - PinballGame get gameRef => _gameRef; + Future onLoad() async { + images.prefix = ''; + await images.load(theme.Assets.images.dash.ball.keyName); + } + + Future pump(Ball child, {required GameBloc gameBloc}) async { + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [child], + ), + ); + } } class _MockGameBloc extends Mock implements GameBloc {} -class _MockPinballGame extends Mock implements PinballGame {} - -class _MockControlledBall extends Mock implements ControlledBall {} - class _MockBall extends Mock implements Ball {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - theme.Assets.images.dash.ball.keyName, - ]; group('BallController', () { late Ball ball; @@ -43,18 +40,9 @@ void main() { setUp(() { ball = Ball(); gameBloc = _MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + final flameBlocTester = FlameTester(_TestGame.new); test('can be instantiated', () { expect( @@ -64,95 +52,20 @@ void main() { }); flameBlocTester.testGameWidget( - "lost doesn't adds RoundLost to GameBloc " - 'when there are balls left', + 'turboCharge adds TurboChargeActivated', setUp: (game, tester) async { - final controller = BallController(ball); - await ball.add(controller); - await game.ensureAdd(ball); - - final otherBall = Ball(); - final otherController = BallController(otherBall); - await otherBall.add(otherController); - await game.ensureAdd(otherBall); - - controller.lost(); - await game.ready(); - }, - verify: (game, tester) async { - verifyNever(() => gameBloc.add(const RoundLost())); - }, - ); + await game.onLoad(); - flameBlocTester.testGameWidget( - 'lost adds RoundLost to GameBloc ' - 'when there are no balls left', - setUp: (game, tester) async { final controller = BallController(ball); await ball.add(controller); - await game.ensureAdd(ball); + await game.pump(ball, gameBloc: gameBloc); - controller.lost(); - await game.ready(); + await controller.turboCharge(); }, verify: (game, tester) async { - verify(() => gameBloc.add(const RoundLost())).called(1); + verify(() => gameBloc.add(const SparkyTurboChargeActivated())) + .called(1); }, ); - - group('turboCharge', () { - setUpAll(() { - registerFallbackValue(Vector2.zero()); - registerFallbackValue(Component()); - }); - - 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); - - await controller.turboCharge(); - }, - verify: (game, tester) async { - verify(() => gameBloc.add(const SparkyTurboChargeActivated())) - .called(1); - }, - ); - - flameBlocTester.test( - 'initially stops the ball', - (game) async { - final gameRef = _MockPinballGame(); - final ball = _MockControlledBall(); - final controller = _WrappedBallController(ball, gameRef); - when(() => gameRef.read()).thenReturn(gameBloc); - when(() => ball.controller).thenReturn(controller); - when(() => ball.add(any())).thenAnswer((_) async {}); - - await controller.turboCharge(); - - verify(ball.stop).called(1); - }, - ); - - flameBlocTester.test( - 'resumes the ball', - (game) async { - final gameRef = _MockPinballGame(); - final ball = _MockControlledBall(); - final controller = _WrappedBallController(ball, gameRef); - when(() => gameRef.read()).thenReturn(gameBloc); - when(() => ball.controller).thenReturn(controller); - when(() => ball.add(any())).thenAnswer((_) async {}); - - await controller.turboCharge(); - - verify(ball.resume).called(1); - }, - ); - }); }); } diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index f9561b60..00a69f9e 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -1,6 +1,9 @@ import 'dart:collection'; import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/input.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -10,36 +13,39 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ]); + } + + Future pump(Flipper flipper, {required GameBloc gameBloc}) { + return ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [flipper], + ), + ); + } +} + class _MockGameBloc extends Mock implements GameBloc {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - ]; - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () { - final bloc = _MockGameBloc(); - const state = GameState( - totalScore: 0, - roundScore: 0, - multiplier: 1, - rounds: 0, - bonusHistory: [], - ); - whenListen(bloc, Stream.value(state), initialState: state); - return bloc; - }, - assets: assets, - ); + final flameTester = FlameTester(_TestGame.new); group('FlipperController', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + }); + group('onKeyEvent', () { final leftKeys = UnmodifiableListView([ LogicalKeyboardKey.arrowLeft, @@ -65,8 +71,16 @@ void main() { 'moves upwards ' 'when ${event.logicalKey.keyLabel} is pressed', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.playing, + ), + ); + await game.ready(); - await game.add(flipper); + await game.pump(flipper, gameBloc: gameBloc); controller.onKeyEvent(event, {}); expect(flipper.body.linearVelocity.y, isNegative); @@ -76,13 +90,20 @@ void main() { }); testRawKeyDownEvents(leftKeys, (event) { - flameBlocTester.testGameWidget( + flameTester.test( 'does nothing when is game over', - setUp: (game, tester) async { - await game.ensureAdd(flipper); + (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.gameOver, + ), + ); + + await game.pump(flipper, gameBloc: gameBloc); controller.onKeyEvent(event, {}); - }, - verify: (game, tester) async { + expect(flipper.body.linearVelocity.y, isZero); expect(flipper.body.linearVelocity.x, isZero); }, @@ -94,8 +115,16 @@ void main() { 'moves downwards ' 'when ${event.logicalKey.keyLabel} is released', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.playing, + ), + ); + await game.ready(); - await game.add(flipper); + await game.pump(flipper, gameBloc: gameBloc); controller.onKeyEvent(event, {}); expect(flipper.body.linearVelocity.y, isPositive); @@ -109,8 +138,14 @@ void main() { 'does nothing ' 'when ${event.logicalKey.keyLabel} is released', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + await game.ready(); - await game.add(flipper); + await game.pump(flipper, gameBloc: gameBloc); controller.onKeyEvent(event, {}); expect(flipper.body.linearVelocity.y, isZero); @@ -135,8 +170,16 @@ void main() { 'moves upwards ' 'when ${event.logicalKey.keyLabel} is pressed', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.playing, + ), + ); + await game.ready(); - await game.add(flipper); + await game.pump(flipper, gameBloc: gameBloc); controller.onKeyEvent(event, {}); expect(flipper.body.linearVelocity.y, isNegative); @@ -150,8 +193,16 @@ void main() { 'moves downwards ' 'when ${event.logicalKey.keyLabel} is released', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.playing, + ), + ); + await game.ready(); - await game.add(flipper); + await game.pump(flipper, gameBloc: gameBloc); controller.onKeyEvent(event, {}); expect(flipper.body.linearVelocity.y, isPositive); @@ -161,13 +212,20 @@ void main() { }); testRawKeyDownEvents(rightKeys, (event) { - flameBlocTester.testGameWidget( + flameTester.test( 'does nothing when is game over', - setUp: (game, tester) async { - await game.ensureAdd(flipper); + (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.gameOver, + ), + ); + + await game.pump(flipper, gameBloc: gameBloc); controller.onKeyEvent(event, {}); - }, - verify: (game, tester) async { + expect(flipper.body.linearVelocity.y, isZero); expect(flipper.body.linearVelocity.x, isZero); }, @@ -179,8 +237,16 @@ void main() { 'does nothing ' 'when ${event.logicalKey.keyLabel} is released', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.playing, + ), + ); + await game.ready(); - await game.add(flipper); + await game.pump(flipper, gameBloc: gameBloc); controller.onKeyEvent(event, {}); expect(flipper.body.linearVelocity.y, isZero); diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index 4d9c9c74..25b1f739 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -1,39 +1,73 @@ +// ignore_for_file: cascade_invocations + import 'dart:collection'; import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame/input.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/services.dart'; 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_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents { + @override + Future onLoad() async { + images.prefix = ''; + await images.load(Assets.images.plunger.plunger.keyName); + } + + Future pump( + Plunger child, { + GameBloc? gameBloc, + PinballPlayer? pinballPlayer, + }) { + return ensureAdd( + FlameBlocProvider.value( + value: gameBloc ?? GameBloc() + ..add(const GameStarted()), + children: [ + FlameProvider.value( + pinballPlayer ?? _MockPinballPlayer(), + children: [child], + ) + ], + ), + ); + } +} + class _MockGameBloc extends Mock implements GameBloc {} +class _MockPinballPlayer extends Mock implements PinballPlayer {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballTestGame.new); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () { - final bloc = _MockGameBloc(); - const state = GameState( - totalScore: 0, - roundScore: 0, - multiplier: 1, - rounds: 0, - bonusHistory: [], - ); - whenListen(bloc, Stream.value(state), initialState: state); - return bloc; - }, - ); + final flameTester = FlameTester(_TestGame.new); group('PlungerController', () { + late GameBloc gameBloc; + + final flameBlocTester = FlameTester(_TestGame.new); + + late Plunger plunger; + late PlungerController controller; + + setUp(() { + gameBloc = _MockGameBloc(); + plunger = ControlledPlunger(compressionDistance: 10); + controller = PlungerController(plunger); + plunger.add(controller); + }); + group('onKeyEvent', () { final downKeys = UnmodifiableListView([ LogicalKeyboardKey.arrowDown, @@ -41,21 +75,12 @@ void main() { LogicalKeyboardKey.keyS, ]); - late Plunger plunger; - late PlungerController controller; - - setUp(() { - plunger = Plunger(compressionDistance: 10); - controller = PlungerController(plunger); - plunger.add(controller); - }); - testRawKeyDownEvents(downKeys, (event) { flameTester.test( 'moves down ' 'when ${event.logicalKey.keyLabel} is pressed', (game) async { - await game.ensureAdd(plunger); + await game.pump(plunger); controller.onKeyEvent(event, {}); expect(plunger.body.linearVelocity.y, isPositive); @@ -70,7 +95,7 @@ void main() { 'when ${event.logicalKey.keyLabel} is released ' 'and plunger is below its starting position', (game) async { - await game.ensureAdd(plunger); + await game.pump(plunger); plunger.body.setTransform(Vector2(0, 1), 0); controller.onKeyEvent(event, {}); @@ -85,7 +110,7 @@ void main() { 'does not move when ${event.logicalKey.keyLabel} is released ' 'and plunger is in its starting position', (game) async { - await game.ensureAdd(plunger); + await game.pump(plunger); controller.onKeyEvent(event, {}); expect(plunger.body.linearVelocity.y, isZero); @@ -98,7 +123,15 @@ void main() { flameBlocTester.testGameWidget( 'does nothing when is game over', setUp: (game, tester) async { - await game.ensureAdd(plunger); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.gameOver, + ), + ); + + await game.pump(plunger, gameBloc: gameBloc); controller.onKeyEvent(event, {}); }, verify: (game, tester) async { @@ -108,5 +141,45 @@ void main() { ); }); }); + + flameTester.test( + 'adds the PlungerNoiseBehavior plunger is released', + (game) async { + await game.pump(plunger); + plunger.body.setTransform(Vector2(0, 1), 0); + plunger.release(); + + await game.ready(); + final count = + game.descendants().whereType().length; + expect(count, equals(1)); + }, + ); + }); + + group('PlungerNoiseBehavior', () { + late PinballPlayer player; + + setUp(() { + player = _MockPinballPlayer(); + }); + + flameTester.test('plays the correct sound on load', (game) async { + final parent = ControlledPlunger(compressionDistance: 10); + await game.pump(parent, pinballPlayer: player); + await parent.ensureAdd(PlungerNoiseBehavior()); + verify(() => player.play(PinballAudio.launcher)).called(1); + }); + + test('is removed on the first update', () { + final parent = Component(); + final behavior = PlungerNoiseBehavior(); + parent.add(behavior); + parent.update(0); // Run a tick to ensure it is added + + behavior.update(0); // Run its own update where the removal happens + + expect(behavior.shouldRemove, isTrue); + }); }); } diff --git a/test/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior_test.dart b/test/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior_test.dart index 22b6313b..54b3b42b 100644 --- a/test/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior_test.dart +++ b/test/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: cascade_invocations -import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -8,7 +9,34 @@ import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll( + [ + Assets.images.dino.animatronic.head.keyName, + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + ], + ); + } + + Future pump( + DinoDesert child, { + required GameBloc gameBloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [child], + ), + ); + } +} class _MockGameBloc extends Mock implements GameBloc {} @@ -16,43 +44,30 @@ class _MockBall extends Mock implements Ball {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.dino.animatronic.head.keyName, - Assets.images.dino.animatronic.mouth.keyName, - Assets.images.dino.topWall.keyName, - Assets.images.dino.bottomWall.keyName, - Assets.images.slingshot.upper.keyName, - Assets.images.slingshot.lower.keyName, - ]; group('ChromeDinoBonusBehavior', () { 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, - ); + final flameTester = FlameTester(_TestGame.new); - flameBlocTester.testGameWidget( + flameTester.testGameWidget( 'adds GameBonus.dinoChomp to the game ' 'when ChromeDinoStatus.chomping is emitted', setUp: (game, tester) async { + await game.onLoad(); final behavior = ChromeDinoBonusBehavior(); final parent = DinoDesert.test(); final chromeDino = ChromeDino(); await parent.add(chromeDino); - await game.ensureAdd(parent); + await game.pump( + parent, + gameBloc: gameBloc, + ); await parent.ensureAdd(behavior); chromeDino.bloc.onChomp(_MockBall()); diff --git a/test/game/components/dino_desert/dino_desert_test.dart b/test/game/components/dino_desert/dino_desert_test.dart index 63e45e5b..7dea25a3 100644 --- a/test/game/components/dino_desert/dino_desert_test.dart +++ b/test/game/components/dino_desert/dino_desert_test.dart @@ -1,42 +1,59 @@ // ignore_for_file: cascade_invocations +import 'package:flame_bloc/flame_bloc.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/game/behaviors/behaviors.dart'; import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.dino.animatronic.head.keyName, + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.topWall.keyName, + Assets.images.dino.topWallTunnel.keyName, + Assets.images.dino.bottomWall.keyName, + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + ]); + } + + Future pump(DinoDesert child) async { + await ensureAdd( + FlameBlocProvider.value( + value: _MockGameBloc(), + children: [child], + ), + ); + } +} + +class _MockGameBloc extends Mock implements GameBloc {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.dino.animatronic.head.keyName, - Assets.images.dino.animatronic.mouth.keyName, - Assets.images.dino.topWall.keyName, - Assets.images.dino.topWallTunnel.keyName, - Assets.images.dino.bottomWall.keyName, - Assets.images.slingshot.upper.keyName, - Assets.images.slingshot.lower.keyName, - ]; - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); + final flameTester = FlameTester(_TestGame.new); group('DinoDesert', () { flameTester.test('loads correctly', (game) async { final component = DinoDesert(); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); + await game.pump(component); + expect(game.descendants(), contains(component)); }); group('loads', () { flameTester.test( 'a ChromeDino', (game) async { - await game.ensureAdd(DinoDesert()); + await game.pump(DinoDesert()); expect( game.descendants().whereType().length, equals(1), @@ -47,17 +64,18 @@ void main() { flameTester.test( 'DinoWalls', (game) async { - await game.ensureAdd(DinoDesert()); + await game.pump(DinoDesert()); expect( game.descendants().whereType().length, equals(1), ); }, ); + flameTester.test( 'Slingshots', (game) async { - await game.ensureAdd(DinoDesert()); + await game.pump(DinoDesert()); expect( game.descendants().whereType().length, equals(1), @@ -70,7 +88,7 @@ void main() { flameTester.test( 'ScoringContactBehavior to ChromeDino', (game) async { - await game.ensureAdd(DinoDesert()); + await game.pump(DinoDesert()); final chromeDino = game.descendants().whereType().single; expect( @@ -81,10 +99,10 @@ void main() { ); flameTester.test('a ChromeDinoBonusBehavior', (game) async { - final dinoDesert = DinoDesert(); - await game.ensureAdd(dinoDesert); + final component = DinoDesert(); + await game.pump(component); expect( - dinoDesert.children.whereType().single, + component.children.whereType().single, isNotNull, ); }); diff --git a/test/game/components/drain/behaviors/draining_behavior_test.dart b/test/game/components/drain/behaviors/draining_behavior_test.dart new file mode 100644 index 00000000..d25a7da6 --- /dev/null +++ b/test/game/components/drain/behaviors/draining_behavior_test.dart @@ -0,0 +1,134 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_bloc/flame_bloc.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/game/components/drain/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.load(theme.Assets.images.dash.ball.keyName); + } + + Future pump( + Drain child, { + required GameBloc gameBloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [child], + ), + ); + } +} + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'DrainingBehavior', + () { + final flameTester = FlameTester(Forge2DGame.new); + + test('can be instantiated', () { + expect(DrainingBehavior(), isA()); + }); + + flameTester.test( + 'loads', + (game) async { + final parent = Drain.test(); + final behavior = DrainingBehavior(); + await parent.add(behavior); + await game.ensureAdd(parent); + expect(parent.contains(behavior), isTrue); + }, + ); + + group('beginContact', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + }); + + final flameBlocTester = FlameTester(_TestGame.new); + + flameBlocTester.test( + 'adds RoundLost when no balls left', + (game) async { + final drain = Drain.test(); + final behavior = DrainingBehavior(); + final ball = Ball.test(); + await drain.add(behavior); + await game.pump( + drain, + gameBloc: gameBloc, + ); + await game.ensureAdd(ball); + + behavior.beginContact(ball, _MockContact()); + await game.ready(); + + expect(game.descendants().whereType(), isEmpty); + verify(() => gameBloc.add(const RoundLost())).called(1); + }, + ); + + flameBlocTester.test( + "doesn't add RoundLost when there are balls left", + (game) async { + final drain = Drain.test(); + final behavior = DrainingBehavior(); + final ball1 = Ball.test(); + final ball2 = Ball.test(); + await drain.add(behavior); + await game.pump( + drain, + gameBloc: gameBloc, + ); + await game.ensureAddAll([ball1, ball2]); + + behavior.beginContact(ball1, _MockContact()); + await game.ready(); + + expect(game.descendants().whereType(), isNotEmpty); + verifyNever(() => gameBloc.add(const RoundLost())); + }, + ); + + flameBlocTester.test( + 'removes the Ball', + (game) async { + final drain = Drain.test(); + final behavior = DrainingBehavior(); + final ball = Ball.test(); + await drain.add(behavior); + await game.pump( + drain, + gameBloc: gameBloc, + ); + await game.ensureAdd(ball); + + behavior.beginContact(ball, _MockContact()); + await game.ready(); + + expect(game.descendants().whereType(), isEmpty); + }, + ); + }); + }, + ); +} diff --git a/test/game/components/drain_test.dart b/test/game/components/drain/drain_test.dart similarity index 57% rename from test/game/components/drain_test.dart rename to test/game/components/drain/drain_test.dart index 984abce3..b10c55e3 100644 --- a/test/game/components/drain_test.dart +++ b/test/game/components/drain/drain_test.dart @@ -3,20 +3,12 @@ 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/game/game.dart'; - -import '../../helpers/helpers.dart'; - -class _MockControlledBall extends Mock implements ControlledBall {} -class _MockBallController extends Mock implements BallController {} - -class _MockContact extends Mock implements Contact {} +import 'package:pinball/game/game.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final flameTester = FlameTester(Forge2DGame.new); group('Drain', () { flameTester.test( @@ -45,19 +37,5 @@ void main() { expect(drain.body.fixtures.first.isSensor, isTrue); }, ); - - test( - 'calls lost on contact with ball', - () async { - final drain = Drain(); - final ball = _MockControlledBall(); - final controller = _MockBallController(); - when(() => ball.controller).thenReturn(controller); - - drain.beginContact(ball, _MockContact()); - - verify(controller.lost).called(1); - }, - ); }); } 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 71b41029..3dcd870b 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 @@ -1,8 +1,7 @@ // ignore_for_file: cascade_invocations -import 'dart:async'; - -import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -12,7 +11,37 @@ 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 _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.dash.animatronic.keyName, + theme.Assets.images.dash.ball.keyName, + ]); + } + + Future pump( + FlutterForest child, { + required GameBloc gameBloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [ + FlameProvider.value( + const theme.DashTheme(), + children: [ + ZCanvasComponent( + children: [child], + ), + ], + ), + ], + ), + ); + } +} class _MockGameBloc extends Mock implements GameBloc {} @@ -21,34 +50,21 @@ void main() { group('FlutterForestBonusBehavior', () { late GameBloc gameBloc; - final assets = [ - Assets.images.dash.animatronic.keyName, - theme.Assets.images.dash.ball.keyName, - ]; setUp(() { gameBloc = _MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + final flameTester = FlameTester(_TestGame.new); void _contactedBumper(DashNestBumper bumper) => bumper.bloc.onBallContacted(); - flameBlocTester.testGameWidget( + flameTester.testGameWidget( 'adds GameBonus.dashNest to the game ' 'when bumpers are activated three times', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); final behavior = FlutterForestBonusBehavior(); final parent = FlutterForest.test(); final bumpers = [ @@ -58,7 +74,7 @@ void main() { ]; final animatronic = DashAnimatronic(); final signpost = Signpost.test(bloc: SignpostCubit()); - await game.ensureAdd(ZCanvasComponent(children: [parent])); + await game.pump(parent, gameBloc: gameBloc); await parent.ensureAddAll([...bumpers, animatronic, signpost]); await parent.ensureAdd(behavior); @@ -76,11 +92,11 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameTester.testGameWidget( 'adds a new Ball to the game ' 'when bumpers are activated three times', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); final behavior = FlutterForestBonusBehavior(); final parent = FlutterForest.test(); final bumpers = [ @@ -90,7 +106,7 @@ void main() { ]; final animatronic = DashAnimatronic(); final signpost = Signpost.test(bloc: SignpostCubit()); - await game.ensureAdd(ZCanvasComponent(children: [parent])); + await game.pump(parent, gameBloc: gameBloc); await parent.ensureAddAll([...bumpers, animatronic, signpost]); await parent.ensureAdd(behavior); @@ -110,11 +126,11 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameTester.testGameWidget( 'progress the signpost ' 'when bumpers are activated', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); final behavior = FlutterForestBonusBehavior(); final parent = FlutterForest.test(); final bumpers = [ @@ -124,7 +140,7 @@ void main() { ]; final animatronic = DashAnimatronic(); final signpost = Signpost.test(bloc: SignpostCubit()); - await game.ensureAdd(ZCanvasComponent(children: [parent])); + await game.pump(parent, gameBloc: gameBloc); await parent.ensureAddAll([...bumpers, animatronic, signpost]); await parent.ensureAdd(behavior); diff --git a/test/game/components/flutter_forest/flutter_forest_test.dart b/test/game/components/flutter_forest/flutter_forest_test.dart index 6dddcd7b..470719d8 100644 --- a/test/game/components/flutter_forest/flutter_forest_test.dart +++ b/test/game/components/flutter_forest/flutter_forest_test.dart @@ -1,40 +1,67 @@ // ignore_for_file: cascade_invocations +import 'package:flame_bloc/flame_bloc.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/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import '../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.dash.bumper.a.active.keyName, + Assets.images.dash.bumper.a.inactive.keyName, + Assets.images.dash.bumper.b.active.keyName, + Assets.images.dash.bumper.b.inactive.keyName, + Assets.images.dash.animatronic.keyName, + Assets.images.signpost.inactive.keyName, + Assets.images.signpost.active1.keyName, + Assets.images.signpost.active2.keyName, + Assets.images.signpost.active3.keyName, + ]); + } + + Future pump(FlutterForest child) async { + await ensureAdd( + FlameBlocProvider.value( + value: _MockGameBloc(), + children: [ + FlameProvider.value( + _MockPinballPlayer(), + children: [ + ZCanvasComponent(children: [child]), + ], + ), + ], + ), + ); + } +} + +class _MockPinballPlayer extends Mock implements PinballPlayer {} + +class _MockGameBloc extends Mock implements GameBloc {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, - Assets.images.dash.bumper.a.active.keyName, - Assets.images.dash.bumper.a.inactive.keyName, - Assets.images.dash.bumper.b.active.keyName, - Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.animatronic.keyName, - Assets.images.signpost.inactive.keyName, - Assets.images.signpost.active1.keyName, - Assets.images.signpost.active2.keyName, - Assets.images.signpost.active3.keyName, - ]; - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); + final flameTester = FlameTester(_TestGame.new); group('FlutterForest', () { flameTester.test( 'loads correctly', (game) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); - expect(game.descendants(), contains(flutterForest)); + final component = FlutterForest(); + await game.pump(component); + expect(game.descendants(), contains(component)); }, ); @@ -42,8 +69,8 @@ void main() { flameTester.test( 'a Signpost', (game) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); + final component = FlutterForest(); + await game.pump(component); expect( game.descendants().whereType().length, equals(1), @@ -54,8 +81,8 @@ void main() { flameTester.test( 'a DashAnimatronic', (game) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); + final component = FlutterForest(); + await game.pump(component); expect( game.descendants().whereType().length, equals(1), @@ -66,8 +93,8 @@ void main() { flameTester.test( 'three DashNestBumper', (game) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); + final component = FlutterForest(); + await game.pump(component); expect( game.descendants().whereType().length, equals(3), @@ -76,14 +103,14 @@ void main() { ); flameTester.test( - 'three DashNestBumpers with BumperNoisyBehavior', + 'three DashNestBumpers with BumperNoiseBehavior', (game) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); + final component = FlutterForest(); + await game.pump(component); final bumpers = game.descendants().whereType(); for (final bumper in bumpers) { expect( - bumper.firstChild(), + bumper.firstChild(), isNotNull, ); } diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart new file mode 100644 index 00000000..7118aa8d --- /dev/null +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -0,0 +1,145 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.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_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.load(Assets.images.backbox.marquee.keyName); + } + + Future pump( + Iterable children, { + PinballPlayer? pinballPlayer, + }) async { + return ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [ + MultiFlameProvider( + providers: [ + FlameProvider.value( + pinballPlayer ?? _MockPinballPlayer(), + ), + FlameProvider.value( + const theme.DashTheme(), + ), + ], + children: children, + ), + ], + ), + ); + } +} + +class _MockPinballPlayer extends Mock implements PinballPlayer {} + +class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('GameBlocStatusListener', () { + test('can be instantiated', () { + expect( + GameBlocStatusListener(), + isA(), + ); + }); + + final flameTester = FlameTester(_TestGame.new); + + flameTester.test( + 'can be loaded', + (game) async { + final component = GameBlocStatusListener(); + await game.pump([component]); + expect(game.descendants(), contains(component)); + }, + ); + + group('listenWhen', () { + test('is true when the game over state has changed', () { + const state = GameState( + totalScore: 0, + roundScore: 10, + multiplier: 1, + rounds: 0, + bonusHistory: [], + status: GameStatus.playing, + ); + + const previous = GameState.initial(); + expect( + GameBlocStatusListener().listenWhen(previous, state), + isTrue, + ); + }); + }); + + group('onNewState', () { + flameTester.test( + 'changes the backbox display when the game is over', + (game) async { + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox(leaderboardRepository: repository); + final state = const GameState.initial() + ..copyWith( + status: GameStatus.gameOver, + ); + + await game.pump([component, backbox]); + + expect(() => component.onNewState(state), returnsNormally); + }, + ); + + flameTester.test( + 'plays the background music on start', + (game) async { + final player = _MockPinballPlayer(); + final component = GameBlocStatusListener(); + await game.pump([component], pinballPlayer: player); + + component.onNewState( + const GameState.initial().copyWith(status: GameStatus.playing), + ); + + verify(() => player.play(PinballAudio.backgroundMusic)).called(1); + }, + ); + + flameTester.test( + 'plays the game over voice over when it is game over', + (game) async { + final player = _MockPinballPlayer(); + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox(leaderboardRepository: repository); + await game.pump([component, backbox], pinballPlayer: player); + + component.onNewState( + const GameState.initial().copyWith(status: GameStatus.gameOver), + ); + + verify(() => player.play(PinballAudio.gameOverVoiceOver)).called(1); + }, + ); + }); + }); +} diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart deleted file mode 100644 index e403396d..00000000 --- a/test/game/components/game_flow_controller_test.dart +++ /dev/null @@ -1,123 +0,0 @@ -// ignore_for_file: type_annotate_public_apis, prefer_const_constructors - -import 'package:flame/game.dart'; -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_theme/pinball_theme.dart'; - -class _MockPinballGame extends Mock implements PinballGame {} - -class _MockBackbox extends Mock implements Backbox {} - -class _MockCameraController extends Mock implements CameraController {} - -class _MockActiveOverlaysNotifier extends Mock - implements ActiveOverlaysNotifier {} - -class _MockPinballAudio extends Mock implements PinballAudio {} - -void main() { - group('GameFlowController', () { - group('listenWhen', () { - test('is true when the game over state has changed', () { - final state = GameState( - totalScore: 0, - roundScore: 10, - multiplier: 1, - rounds: 0, - bonusHistory: const [], - ); - - final previous = GameState.initial(); - expect( - GameFlowController(_MockPinballGame()).listenWhen(previous, state), - isTrue, - ); - }); - }); - - group('onNewState', () { - late PinballGame game; - late Backbox backbox; - late CameraController cameraController; - late GameFlowController gameFlowController; - late PinballAudio pinballAudio; - late ActiveOverlaysNotifier overlays; - - setUp(() { - game = _MockPinballGame(); - backbox = _MockBackbox(); - cameraController = _MockCameraController(); - gameFlowController = GameFlowController(game); - overlays = _MockActiveOverlaysNotifier(); - pinballAudio = _MockPinballAudio(); - - when( - () => backbox.initialsInput( - score: any(named: 'score'), - characterIconPath: any(named: 'characterIconPath'), - onSubmit: any(named: 'onSubmit'), - ), - ).thenAnswer((_) async {}); - when(cameraController.focusOnWaitingBackbox).thenAnswer((_) async {}); - when(cameraController.focusOnGame).thenAnswer((_) async {}); - - when(() => overlays.remove(any())).thenAnswer((_) => true); - - when(() => game.descendants().whereType()) - .thenReturn([backbox]); - when(game.firstChild).thenReturn(cameraController); - when(() => game.overlays).thenReturn(overlays); - when(() => game.characterTheme).thenReturn(DashTheme()); - when(() => game.audio).thenReturn(pinballAudio); - }); - - test( - 'changes the backbox display and camera correctly ' - 'when the game is over', - () { - gameFlowController.onNewState( - GameState( - totalScore: 0, - roundScore: 10, - multiplier: 1, - rounds: 0, - bonusHistory: const [], - ), - ); - - verify( - () => backbox.initialsInput( - score: 0, - characterIconPath: any(named: 'characterIconPath'), - onSubmit: any(named: 'onSubmit'), - ), - ).called(1); - verify(cameraController.focusOnGameOverBackbox).called(1); - }, - ); - - test( - 'changes the backbox and camera correctly when it is not a game over', - () { - gameFlowController.onNewState(GameState.initial()); - - verify(cameraController.focusOnGame).called(1); - verify(() => overlays.remove(PinballGame.playButtonOverlay)) - .called(1); - }, - ); - - test( - 'plays the background music on start', - () { - gameFlowController.onNewState(GameState.initial()); - - verify(pinballAudio.backgroundMusic).called(1); - }, - ); - }); - }); -} diff --git a/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart b/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart index c9910fd7..40afeb09 100644 --- a/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart +++ b/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart @@ -1,55 +1,71 @@ // ignore_for_file: cascade_invocations -import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.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/game/components/google_word/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; -import '../../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.googleWord.letter1.lit.keyName, + Assets.images.googleWord.letter1.dimmed.keyName, + Assets.images.googleWord.letter2.lit.keyName, + Assets.images.googleWord.letter2.dimmed.keyName, + Assets.images.googleWord.letter3.lit.keyName, + Assets.images.googleWord.letter3.dimmed.keyName, + Assets.images.googleWord.letter4.lit.keyName, + Assets.images.googleWord.letter4.dimmed.keyName, + Assets.images.googleWord.letter5.lit.keyName, + Assets.images.googleWord.letter5.dimmed.keyName, + Assets.images.googleWord.letter6.lit.keyName, + Assets.images.googleWord.letter6.dimmed.keyName, + ]); + } + + Future pump(GoogleWord child, {required GameBloc gameBloc}) async { + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [ + FlameProvider.value( + _MockPinballPlayer(), + children: [child], + ) + ], + ), + ); + } +} class _MockGameBloc extends Mock implements GameBloc {} +class _MockPinballPlayer extends Mock implements PinballPlayer {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.googleWord.letter1.lit.keyName, - Assets.images.googleWord.letter1.dimmed.keyName, - Assets.images.googleWord.letter2.lit.keyName, - Assets.images.googleWord.letter2.dimmed.keyName, - Assets.images.googleWord.letter3.lit.keyName, - Assets.images.googleWord.letter3.dimmed.keyName, - Assets.images.googleWord.letter4.lit.keyName, - Assets.images.googleWord.letter4.dimmed.keyName, - Assets.images.googleWord.letter5.lit.keyName, - Assets.images.googleWord.letter5.dimmed.keyName, - Assets.images.googleWord.letter6.lit.keyName, - Assets.images.googleWord.letter6.dimmed.keyName, - ]; group('GoogleWordBonusBehaviors', () { 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, - ); + final flameTester = FlameTester(_TestGame.new); - flameBlocTester.testGameWidget( + flameTester.testGameWidget( 'adds GameBonus.googleWord to the game when all letters are activated', setUp: (game, tester) async { + await game.onLoad(); final behavior = GoogleWordBonusBehavior(); final parent = GoogleWord.test(); final letters = [ @@ -61,7 +77,7 @@ void main() { GoogleLetter(5), ]; await parent.addAll(letters); - await game.ensureAdd(parent); + await game.pump(parent, gameBloc: gameBloc); await parent.ensureAdd(behavior); for (final letter in letters) { diff --git a/test/game/components/google_word/google_word_test.dart b/test/game/components/google_word/google_word_test.dart index 11751238..c0258281 100644 --- a/test/game/components/google_word/google_word_test.dart +++ b/test/game/components/google_word/google_word_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,25 +8,40 @@ import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.googleWord.letter1.lit.keyName, + Assets.images.googleWord.letter1.dimmed.keyName, + Assets.images.googleWord.letter2.lit.keyName, + Assets.images.googleWord.letter2.dimmed.keyName, + Assets.images.googleWord.letter3.lit.keyName, + Assets.images.googleWord.letter3.dimmed.keyName, + Assets.images.googleWord.letter4.lit.keyName, + Assets.images.googleWord.letter4.dimmed.keyName, + Assets.images.googleWord.letter5.lit.keyName, + Assets.images.googleWord.letter5.dimmed.keyName, + Assets.images.googleWord.letter6.lit.keyName, + Assets.images.googleWord.letter6.dimmed.keyName, + ]); + } + + Future pump(GoogleWord child, {GameBloc? gameBloc}) { + return ensureAdd( + FlameBlocProvider.value( + value: gameBloc ?? GameBloc(), + children: [child], + ), + ); + } +} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.googleWord.letter1.lit.keyName, - Assets.images.googleWord.letter1.dimmed.keyName, - Assets.images.googleWord.letter2.lit.keyName, - Assets.images.googleWord.letter2.dimmed.keyName, - Assets.images.googleWord.letter3.lit.keyName, - Assets.images.googleWord.letter3.dimmed.keyName, - Assets.images.googleWord.letter4.lit.keyName, - Assets.images.googleWord.letter4.dimmed.keyName, - Assets.images.googleWord.letter5.lit.keyName, - Assets.images.googleWord.letter5.dimmed.keyName, - Assets.images.googleWord.letter6.lit.keyName, - Assets.images.googleWord.letter6.dimmed.keyName, - ]; - final flameTester = FlameTester(() => EmptyPinballTestGame(assets: assets)); + + final flameTester = FlameTester(_TestGame.new); group('GoogleWord', () { flameTester.test( @@ -33,7 +49,7 @@ void main() { (game) async { const word = 'Google'; final googleWord = GoogleWord(position: Vector2.zero()); - await game.ensureAdd(googleWord); + await game.pump(googleWord); final letters = googleWord.children.whereType(); expect(letters.length, equals(word.length)); @@ -42,7 +58,7 @@ void main() { flameTester.test('adds a GoogleWordBonusBehavior', (game) async { final googleWord = GoogleWord(position: Vector2.zero()); - await game.ensureAdd(googleWord); + await game.pump(googleWord); expect( googleWord.children.whereType().single, isNotNull, diff --git a/test/game/components/launcher_test.dart b/test/game/components/launcher_test.dart index c76e6b7e..35272569 100644 --- a/test/game/components/launcher_test.dart +++ b/test/game/components/launcher_test.dart @@ -1,36 +1,49 @@ // ignore_for_file: cascade_invocations +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.launchRamp.ramp.keyName, + Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.launchRamp.foregroundRailing.keyName, + Assets.images.flapper.backSupport.keyName, + Assets.images.flapper.frontSupport.keyName, + Assets.images.flapper.flap.keyName, + Assets.images.plunger.plunger.keyName, + Assets.images.plunger.rocket.keyName, + ]); + } + + Future pump(Launcher launchRamp) { + return ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [launchRamp], + ), + ); + } +} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.launchRamp.ramp.keyName, - Assets.images.launchRamp.backgroundRailing.keyName, - Assets.images.launchRamp.foregroundRailing.keyName, - Assets.images.flapper.backSupport.keyName, - Assets.images.flapper.frontSupport.keyName, - Assets.images.flapper.flap.keyName, - Assets.images.plunger.plunger.keyName, - Assets.images.plunger.rocket.keyName, - ]; - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); + final flameTester = FlameTester(_TestGame.new); group('Launcher', () { flameTester.test( 'loads correctly', (game) async { - final launcher = Launcher(); - await game.ensureAdd(launcher); - - expect(game.contains(launcher), isTrue); + final component = Launcher(); + await game.pump(component); + expect(game.descendants(), contains(component)); }, ); @@ -38,11 +51,11 @@ void main() { flameTester.test( 'a LaunchRamp', (game) async { - final launcher = Launcher(); - await game.ensureAdd(launcher); + final component = Launcher(); + await game.pump(component); final descendantsQuery = - launcher.descendants().whereType(); + component.descendants().whereType(); expect(descendantsQuery.length, equals(1)); }, ); @@ -50,10 +63,10 @@ void main() { flameTester.test( 'a Flapper', (game) async { - final launcher = Launcher(); - await game.ensureAdd(launcher); + final component = Launcher(); + await game.pump(component); - final descendantsQuery = launcher.descendants().whereType(); + final descendantsQuery = component.descendants().whereType(); expect(descendantsQuery.length, equals(1)); }, ); @@ -61,10 +74,10 @@ void main() { flameTester.test( 'a Plunger', (game) async { - final launcher = Launcher(); - await game.ensureAdd(launcher); + final component = Launcher(); + await game.pump(component); - final descendantsQuery = launcher.descendants().whereType(); + final descendantsQuery = component.descendants().whereType(); expect(descendantsQuery.length, equals(1)); }, ); @@ -72,11 +85,11 @@ void main() { flameTester.test( 'a RocketSpriteComponent', (game) async { - final launcher = Launcher(); - await game.ensureAdd(launcher); + final component = Launcher(); + await game.pump(component); final descendantsQuery = - launcher.descendants().whereType(); + component.descendants().whereType(); expect(descendantsQuery.length, equals(1)); }, ); diff --git a/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart index b294d350..139c7e47 100644 --- a/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart +++ b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.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'; @@ -10,7 +12,25 @@ import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, + ]); + } + + Future pump(Multiballs child, {GameBloc? gameBloc}) { + return ensureAdd( + FlameBlocProvider.value( + value: gameBloc ?? GameBloc(), + children: [child], + ), + ); + } +} class _MockGameBloc extends Mock implements GameBloc {} @@ -18,43 +38,44 @@ class _MockMultiballCubit extends Mock implements MultiballCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.multiball.lit.keyName, - Assets.images.multiball.dimmed.keyName, - ]; group('MultiballsBehavior', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = _MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), + final flameTester = FlameTester(_TestGame.new); + + test('can be instantiated', () { + expect( + MultiballsBehavior(), + isA(), ); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, + flameTester.test( + 'can be loaded', + (game) async { + final parent = Multiballs.test(); + final behavior = MultiballsBehavior(); + await game.pump(parent); + await parent.ensureAdd(behavior); + expect(parent.children, contains(behavior)); + }, ); group('listenWhen', () { test( - 'is true when the bonusHistory has changed ' - 'with a new GameBonus.dashNest', () { - final previous = GameState.initial(); - final state = previous.copyWith( - bonusHistory: [GameBonus.dashNest], - ); + 'is true when the bonusHistory has changed ' + 'with a new GameBonus.dashNest', + () { + final previous = GameState.initial(); + final state = previous.copyWith( + bonusHistory: [GameBonus.dashNest], + ); - expect( - MultiballsBehavior().listenWhen(previous, state), - isTrue, - ); - }); + expect( + MultiballsBehavior().listenWhen(previous, state), + isTrue, + ); + }, + ); test( 'is false when the bonusHistory has changed ' @@ -79,6 +100,7 @@ void main() { multiplier: 1, rounds: 0, bonusHistory: const [], + status: GameStatus.playing, ); expect( @@ -89,7 +111,18 @@ void main() { }); group('onNewState', () { - flameBlocTester.testGameWidget( + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + whenListen( + gameBloc, + Stream.empty(), + initialState: GameState.initial(), + ); + }); + + flameTester.testGameWidget( "calls 'onAnimate' once for every multiball", setUp: (game, tester) async { final behavior = MultiballsBehavior(); @@ -120,7 +153,7 @@ void main() { when(otherMultiballCubit.onAnimate).thenAnswer((_) async {}); await parent.addAll(multiballs); - await game.ensureAdd(parent); + await game.pump(parent, gameBloc: gameBloc); await parent.ensureAdd(behavior); await tester.pump(); diff --git a/test/game/components/multiballs/multiballs_test.dart b/test/game/components/multiballs/multiballs_test.dart index c1a328b1..1841d0a3 100644 --- a/test/game/components/multiballs/multiballs_test.dart +++ b/test/game/components/multiballs/multiballs_test.dart @@ -1,54 +1,57 @@ // ignore_for_file: cascade_invocations +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, + ]); + } + + Future pump(Multiballs child, {GameBloc? gameBloc}) { + return ensureAdd( + FlameBlocProvider.value( + value: gameBloc ?? GameBloc(), + children: [child], + ), + ); + } +} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.multiball.lit.keyName, - Assets.images.multiball.dimmed.keyName, - ]; - late GameBloc gameBloc; - setUp(() { - gameBloc = GameBloc(); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + final flameBlocTester = FlameTester(_TestGame.new); group('Multiballs', () { flameBlocTester.testGameWidget( 'loads correctly', setUp: (game, tester) async { final multiballs = Multiballs(); - await game.ensureAdd(multiballs); - - expect(game.contains(multiballs), isTrue); + await game.pump(multiballs); + expect(game.descendants(), contains(multiballs)); }, ); - group('loads', () { - flameBlocTester.testGameWidget( - 'four Multiball', - setUp: (game, tester) async { - final multiballs = Multiballs(); - await game.ensureAdd(multiballs); - - expect( - multiballs.descendants().whereType().length, - equals(4), - ); - }, - ); - }); + flameBlocTester.test( + 'loads four Multiball', + (game) async { + final multiballs = Multiballs(); + await game.pump(multiballs); + expect( + multiballs.descendants().whereType().length, + equals(4), + ); + }, + ); }); } diff --git a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart index ca3c5921..f1e42a51 100644 --- a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart +++ b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart @@ -4,6 +4,8 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.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'; @@ -11,7 +13,33 @@ import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ]); + } + + Future pump(Multipliers child) async { + await ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [child], + ), + ); + } +} class _MockGameBloc extends Mock implements GameBloc {} @@ -21,18 +49,6 @@ class _MockMultiplierCubit extends Mock implements MultiplierCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.multiplier.x2.lit.keyName, - Assets.images.multiplier.x2.dimmed.keyName, - Assets.images.multiplier.x3.lit.keyName, - Assets.images.multiplier.x3.dimmed.keyName, - Assets.images.multiplier.x4.lit.keyName, - Assets.images.multiplier.x4.dimmed.keyName, - Assets.images.multiplier.x5.lit.keyName, - Assets.images.multiplier.x5.dimmed.keyName, - Assets.images.multiplier.x6.lit.keyName, - Assets.images.multiplier.x6.dimmed.keyName, - ]; group('MultipliersBehavior', () { late GameBloc gameBloc; @@ -47,11 +63,7 @@ void main() { ); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + final flameBlocTester = FlameTester(_TestGame.new); group('listenWhen', () { test('is true when the multiplier has changed', () { @@ -60,10 +72,11 @@ void main() { roundScore: 10, multiplier: 2, rounds: 0, + status: GameStatus.playing, bonusHistory: const [], ); - final previous = GameState.initial(); + expect( MultipliersBehavior().listenWhen(previous, state), isTrue, @@ -76,10 +89,11 @@ void main() { roundScore: 10, multiplier: 1, rounds: 0, + status: GameStatus.playing, bonusHistory: const [], ); - final previous = GameState.initial(); + expect( MultipliersBehavior().listenWhen(previous, state), isFalse, @@ -91,6 +105,7 @@ void main() { flameBlocTester.testGameWidget( "calls 'next' once per each multiplier when GameBloc emit state", setUp: (game, tester) async { + await game.onLoad(); final behavior = MultipliersBehavior(); final parent = Multipliers.test(); final multiplierX2Cubit = _MockMultiplierCubit(); @@ -121,7 +136,7 @@ void main() { when(() => multiplierX3Cubit.next(any())).thenAnswer((_) async {}); await parent.addAll(multipliers); - await game.ensureAdd(parent); + await game.pump(parent); await parent.ensureAdd(behavior); await tester.pump(); diff --git a/test/game/components/multipliers/multipliers_test.dart b/test/game/components/multipliers/multipliers_test.dart index 6b2d95a6..7f98058e 100644 --- a/test/game/components/multipliers/multipliers_test.dart +++ b/test/game/components/multipliers/multipliers_test.dart @@ -1,63 +1,65 @@ // ignore_for_file: cascade_invocations +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ]); + } + + Future pump(Multipliers child) async { + await ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [child], + ), + ); + } +} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.multiplier.x2.lit.keyName, - Assets.images.multiplier.x2.dimmed.keyName, - Assets.images.multiplier.x3.lit.keyName, - Assets.images.multiplier.x3.dimmed.keyName, - Assets.images.multiplier.x4.lit.keyName, - Assets.images.multiplier.x4.dimmed.keyName, - Assets.images.multiplier.x5.lit.keyName, - Assets.images.multiplier.x5.dimmed.keyName, - Assets.images.multiplier.x6.lit.keyName, - Assets.images.multiplier.x6.dimmed.keyName, - ]; - - late GameBloc gameBloc; - - setUp(() { - gameBloc = GameBloc(); - }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + final flameTester = FlameTester(_TestGame.new); group('Multipliers', () { - flameBlocTester.testGameWidget( + flameTester.test( 'loads correctly', - setUp: (game, tester) async { - final multipliersGroup = Multipliers(); - await game.ensureAdd(multipliersGroup); - - expect(game.contains(multipliersGroup), isTrue); + (game) async { + final component = Multipliers(); + await game.pump(component); + expect(game.descendants(), contains(component)); }, ); - group('loads', () { - flameBlocTester.testGameWidget( - 'five Multiplier', - setUp: (game, tester) async { - final multipliersGroup = Multipliers(); - await game.ensureAdd(multipliersGroup); - - expect( - multipliersGroup.descendants().whereType().length, - equals(5), - ); - }, - ); - }); + flameTester.test( + 'loads five Multiplier', + (game) async { + final multipliersGroup = Multipliers(); + await game.pump(multipliersGroup); + expect( + multipliersGroup.descendants().whereType().length, + equals(5), + ); + }, + ); }); } diff --git a/test/game/components/sparky_scorch_test.dart b/test/game/components/sparky_scorch_test.dart index 5df250dd..92a3ab01 100644 --- a/test/game/components/sparky_scorch_test.dart +++ b/test/game/components/sparky_scorch_test.dart @@ -8,7 +8,24 @@ import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.glow.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, + ]); + } +} class _MockControlledBall extends Mock implements ControlledBall {} @@ -18,22 +35,8 @@ class _MockContact extends Mock implements Contact {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.sparky.computer.top.keyName, - Assets.images.sparky.computer.base.keyName, - Assets.images.sparky.computer.glow.keyName, - Assets.images.sparky.animatronic.keyName, - Assets.images.sparky.bumper.a.lit.keyName, - Assets.images.sparky.bumper.a.dimmed.keyName, - Assets.images.sparky.bumper.b.lit.keyName, - Assets.images.sparky.bumper.b.dimmed.keyName, - Assets.images.sparky.bumper.c.lit.keyName, - Assets.images.sparky.bumper.c.dimmed.keyName, - ]; - - final flameTester = FlameTester( - () => EmptyPinballTestGame(assets: assets), - ); + + final flameTester = FlameTester(_TestGame.new); group('SparkyScorch', () { flameTester.test('loads correctly', (game) async { @@ -77,13 +80,13 @@ void main() { ); flameTester.test( - 'three SparkyBumpers with BumperNoisyBehavior', + 'three SparkyBumpers with BumperNoiseBehavior', (game) async { await game.ensureAdd(SparkyScorch()); final bumpers = game.descendants().whereType(); for (final bumper in bumpers) { expect( - bumper.firstChild(), + bumper.firstChild(), isNotNull, ); } diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index f1f3a4cb..b983b0b8 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -7,17 +7,57 @@ import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/src/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/src/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; -import '../helpers/helpers.dart'; +class _TestPinballGame extends PinballGame { + _TestPinballGame() + : super( + characterTheme: const theme.DashTheme(), + leaderboardRepository: _MockLeaderboardRepository(), + gameBloc: GameBloc(), + l10n: _MockAppLocalizations(), + player: _MockPinballPlayer(), + ); + + @override + Future onLoad() async { + images.prefix = ''; + final futures = preLoadAssets(); + await Future.wait(futures); + await super.onLoad(); + } +} + +class _TestDebugPinballGame extends DebugPinballGame { + _TestDebugPinballGame() + : super( + characterTheme: const theme.DashTheme(), + leaderboardRepository: _MockLeaderboardRepository(), + gameBloc: GameBloc(), + l10n: _MockAppLocalizations(), + player: _MockPinballPlayer(), + ); + + @override + Future onLoad() async { + images.prefix = ''; + final futures = preLoadAssets(); + await Future.wait(futures); + await super.onLoad(); + } +} class _MockGameBloc extends Mock implements GameBloc {} -class _MockGameState extends Mock implements GameState {} +class _MockAppLocalizations extends Mock implements AppLocalizations {} class _MockEventPosition extends Mock implements EventPosition {} @@ -35,115 +75,13 @@ class _MockDragUpdateInfo extends Mock implements DragUpdateInfo {} class _MockDragEndInfo extends Mock implements DragEndInfo {} +class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { +} + +class _MockPinballPlayer extends Mock implements PinballPlayer {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.android.bumper.a.lit.keyName, - Assets.images.android.bumper.a.dimmed.keyName, - Assets.images.android.bumper.b.lit.keyName, - Assets.images.android.bumper.b.dimmed.keyName, - Assets.images.android.bumper.cow.lit.keyName, - Assets.images.android.bumper.cow.dimmed.keyName, - Assets.images.backbox.marquee.keyName, - Assets.images.backbox.displayDivider.keyName, - Assets.images.boardBackground.keyName, - theme.Assets.images.android.ball.keyName, - theme.Assets.images.dash.ball.keyName, - 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, - Assets.images.boundary.bottom.keyName, - Assets.images.boundary.outer.keyName, - Assets.images.boundary.outerBottom.keyName, - Assets.images.dino.animatronic.mouth.keyName, - Assets.images.dino.animatronic.head.keyName, - Assets.images.dino.topWall.keyName, - Assets.images.dino.topWallTunnel.keyName, - Assets.images.dino.bottomWall.keyName, - Assets.images.dash.animatronic.keyName, - Assets.images.dash.bumper.a.active.keyName, - Assets.images.dash.bumper.a.inactive.keyName, - Assets.images.dash.bumper.b.active.keyName, - Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - Assets.images.googleWord.letter1.lit.keyName, - Assets.images.googleWord.letter1.dimmed.keyName, - Assets.images.googleWord.letter2.lit.keyName, - Assets.images.googleWord.letter2.dimmed.keyName, - Assets.images.googleWord.letter3.lit.keyName, - Assets.images.googleWord.letter3.dimmed.keyName, - Assets.images.googleWord.letter4.lit.keyName, - Assets.images.googleWord.letter4.dimmed.keyName, - Assets.images.googleWord.letter5.lit.keyName, - Assets.images.googleWord.letter5.dimmed.keyName, - Assets.images.googleWord.letter6.lit.keyName, - Assets.images.googleWord.letter6.dimmed.keyName, - Assets.images.kicker.left.lit.keyName, - Assets.images.kicker.left.dimmed.keyName, - Assets.images.kicker.right.lit.keyName, - Assets.images.kicker.right.dimmed.keyName, - Assets.images.launchRamp.ramp.keyName, - Assets.images.launchRamp.foregroundRailing.keyName, - Assets.images.launchRamp.backgroundRailing.keyName, - Assets.images.multiball.lit.keyName, - Assets.images.multiball.dimmed.keyName, - Assets.images.multiplier.x2.lit.keyName, - Assets.images.multiplier.x2.dimmed.keyName, - Assets.images.multiplier.x3.lit.keyName, - Assets.images.multiplier.x3.dimmed.keyName, - Assets.images.multiplier.x4.lit.keyName, - Assets.images.multiplier.x4.dimmed.keyName, - Assets.images.multiplier.x5.lit.keyName, - Assets.images.multiplier.x5.dimmed.keyName, - Assets.images.multiplier.x6.lit.keyName, - Assets.images.multiplier.x6.dimmed.keyName, - Assets.images.plunger.plunger.keyName, - Assets.images.plunger.rocket.keyName, - Assets.images.signpost.inactive.keyName, - Assets.images.signpost.active1.keyName, - Assets.images.signpost.active2.keyName, - Assets.images.signpost.active3.keyName, - Assets.images.slingshot.upper.keyName, - Assets.images.slingshot.lower.keyName, - Assets.images.android.spaceship.saucer.keyName, - Assets.images.android.spaceship.animatronic.keyName, - Assets.images.android.spaceship.lightBeam.keyName, - 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.sparky.animatronic.keyName, - Assets.images.sparky.computer.top.keyName, - Assets.images.sparky.computer.base.keyName, - Assets.images.sparky.computer.glow.keyName, - Assets.images.sparky.animatronic.keyName, - Assets.images.sparky.bumper.a.lit.keyName, - Assets.images.sparky.bumper.a.dimmed.keyName, - Assets.images.sparky.bumper.b.lit.keyName, - Assets.images.sparky.bumper.b.dimmed.keyName, - Assets.images.sparky.bumper.c.lit.keyName, - Assets.images.sparky.bumper.c.dimmed.keyName, - Assets.images.flapper.flap.keyName, - Assets.images.flapper.backSupport.keyName, - Assets.images.flapper.frontSupport.keyName, - Assets.images.skillShot.decal.keyName, - Assets.images.skillShot.pin.keyName, - Assets.images.skillShot.lit.keyName, - Assets.images.skillShot.dimmed.keyName, - ]; late GameBloc gameBloc; @@ -157,19 +95,21 @@ void main() { }); group('PinballGame', () { - final flameTester = FlameTester( - () => PinballTestGame(assets: assets), - ); - - final flameBlocTester = FlameBlocTester( - gameBuilder: () => PinballTestGame(assets: assets), - blocBuilder: () => gameBloc, - ); + final flameTester = FlameTester(_TestPinballGame.new); group('components', () { - // TODO(alestiago): tests that Blueprints get added once the Blueprint - // class is removed. - flameBlocTester.test( + flameTester.test( + 'has only one BallSpawningBehavior', + (game) async { + await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( 'has only one Drain', (game) async { await game.ready(); @@ -180,7 +120,7 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'has only one BottomGroup', (game) async { await game.ready(); @@ -191,7 +131,7 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'has only one Launcher', (game) async { await game.ready(); @@ -202,7 +142,7 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'has one FlutterForest', (game) async { await game.ready(); @@ -213,11 +153,10 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'has only one Multiballs', (game) async { await game.ready(); - expect( game.descendants().whereType().length, equals(1), @@ -225,7 +164,7 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'one GoogleWord', (game) async { await game.ready(); @@ -236,7 +175,7 @@ void main() { }, ); - flameBlocTester.test('one SkillShot', (game) async { + flameTester.test('one SkillShot', (game) async { await game.ready(); expect( game.descendants().whereType().length, @@ -244,10 +183,13 @@ void main() { ); }); - flameBlocTester.testGameWidget( + flameTester.testGameWidget( 'paints sprites with FilterQuality.medium', setUp: (game, tester) async { - await game.images.loadAll(assets); + game.images.prefix = ''; + final futures = game.preLoadAssets(); + await Future.wait(futures); + await game.ready(); final descendants = game.descendants(); @@ -272,91 +214,6 @@ void main() { } }, ); - - group('controller', () { - group('listenWhen', () { - flameTester.testGameWidget( - 'listens when all balls are lost and there are more than 0 rounds', - setUp: (game, tester) async { - // TODO(ruimiguel): check why testGameWidget doesn't add any ball - // to the game. Test needs to have no balls, so fortunately works. - final newState = _MockGameState(); - when(() => newState.isGameOver).thenReturn(false); - game.descendants().whereType().forEach( - (ball) => ball.controller.lost(), - ); - await game.ready(); - - expect( - game.controller.listenWhen(_MockGameState(), newState), - isTrue, - ); - }, - ); - - flameTester.test( - "doesn't listen when some balls are left", - (game) async { - final newState = _MockGameState(); - when(() => newState.isGameOver).thenReturn(false); - - await game.ready(); - - expect( - game.descendants().whereType().length, - greaterThan(0), - ); - expect( - game.controller.listenWhen(_MockGameState(), newState), - isFalse, - ); - }, - ); - - flameTester.testGameWidget( - "doesn't listen when game is over", - setUp: (game, tester) async { - // TODO(ruimiguel): check why testGameWidget doesn't add any ball - // to the game. Test needs to have no balls, so fortunately works. - final newState = _MockGameState(); - when(() => newState.isGameOver).thenReturn(true); - game.descendants().whereType().forEach( - (ball) => ball.controller.lost(), - ); - await game.ready(); - - expect( - game.descendants().whereType().isEmpty, - isTrue, - ); - expect( - game.controller.listenWhen(_MockGameState(), newState), - isFalse, - ); - }, - ); - }); - - group('onNewState', () { - flameTester.test( - 'spawns a ball', - (game) async { - final previousBalls = - game.descendants().whereType().toList(); - - game.controller.onNewState(_MockGameState()); - await game.ready(); - final currentBalls = - game.descendants().whereType().toList(); - - expect( - currentBalls.length, - equals(previousBalls.length + 1), - ); - }, - ); - }); - }); }); group('flipper control', () { @@ -536,12 +393,9 @@ void main() { }); group('DebugPinballGame', () { - final debugAssets = [Assets.images.ball.flameEffect.keyName, ...assets]; - final debugModeFlameTester = FlameTester( - () => DebugPinballTestGame(assets: debugAssets), - ); + final flameTester = FlameTester(_TestDebugPinballGame.new); - debugModeFlameTester.test( + flameTester.test( 'adds a ball on tap up', (game) async { final eventPosition = _MockEventPosition(); @@ -571,7 +425,7 @@ void main() { }, ); - debugModeFlameTester.test( + flameTester.test( 'set lineStart on pan start', (game) async { final startPosition = Vector2.all(10); @@ -591,7 +445,7 @@ void main() { }, ); - debugModeFlameTester.test( + flameTester.test( 'set lineEnd on pan update', (game) async { final endPosition = Vector2.all(10); @@ -611,7 +465,7 @@ void main() { }, ); - debugModeFlameTester.test( + flameTester.test( 'launch ball on pan end', (game) async { final startPosition = Vector2.zero(); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 90d1b194..f78f6278 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -4,14 +4,38 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.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'; +import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../helpers/helpers.dart'; +class _TestPinballGame extends PinballGame { + _TestPinballGame() + : super( + characterTheme: const theme.DashTheme(), + leaderboardRepository: _MockLeaderboardRepository(), + gameBloc: GameBloc(), + l10n: _MockAppLocalizations(), + player: _MockPinballPlayer(), + ); + + @override + Future onLoad() async { + images.prefix = ''; + final futures = preLoadAssets(); + await Future.wait(futures); + + return super.onLoad(); + } +} + class _MockGameBloc extends Mock implements GameBloc {} class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} @@ -20,8 +44,15 @@ class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} class _MockStartGameBloc extends Mock implements StartGameBloc {} +class _MockAppLocalizations extends Mock implements AppLocalizations {} + +class _MockPinballPlayer extends Mock implements PinballPlayer {} + +class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { +} + void main() { - final game = PinballTestGame(); + final game = _TestPinballGame(); group('PinballGamePage', () { late CharacterThemeCubit characterThemeCubit; diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart index 19c860da..f4054146 100644 --- a/test/game/view/widgets/game_hud_test.dart +++ b/test/game/view/widgets/game_hud_test.dart @@ -27,6 +27,7 @@ void main() { multiplier: 1, rounds: 1, bonusHistory: [], + status: GameStatus.playing, ); setUp(() async { diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart index 049aba95..7078df77 100644 --- a/test/game/view/widgets/round_count_display_test.dart +++ b/test/game/view/widgets/round_count_display_test.dart @@ -18,6 +18,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.playing, ); setUp(() { diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart index 0e4acafc..18e94c09 100644 --- a/test/game/view/widgets/score_view_test.dart +++ b/test/game/view/widgets/score_view_test.dart @@ -23,6 +23,7 @@ void main() { multiplier: 1, rounds: 1, bonusHistory: [], + status: GameStatus.playing, ); setUp(() { @@ -54,9 +55,7 @@ void main() { final l10n = await AppLocalizations.delegate.load(const Locale('en')); stateController.add( - initialState.copyWith( - rounds: 0, - ), + initialState.copyWith(status: GameStatus.gameOver), ); await tester.pumpApp( diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart deleted file mode 100644 index 2c23e3fe..00000000 --- a/test/helpers/builders.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flame/game.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class FlameBlocTester> - extends FlameTester { - FlameBlocTester({ - required GameCreateFunction gameBuilder, - required B Function() blocBuilder, - // TODO(allisonryan0002): find alternative for testGameWidget. Loading - // assets in onLoad fails because the game loads after - List? assets, - List Function()? repositories, - }) : super( - gameBuilder, - pumpWidget: (gameWidget, tester) async { - if (assets != null) { - await Future.wait(assets.map(gameWidget.game.images.load)); - } - await tester.pumpWidget( - BlocProvider.value( - value: blocBuilder(), - child: repositories == null - ? gameWidget - : MultiRepositoryProvider( - providers: repositories.call(), - child: gameWidget, - ), - ), - ); - }, - ); -} diff --git a/test/helpers/fakes.dart b/test/helpers/fakes.dart deleted file mode 100644 index 706733a1..00000000 --- a/test/helpers/fakes.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; - -class FakeContact extends Fake implements Contact {} - -class FakeGameEvent extends Fake implements GameEvent {} diff --git a/test/helpers/forge2d.dart b/test/helpers/forge2d.dart deleted file mode 100644 index f000d404..00000000 --- a/test/helpers/forge2d.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; - -void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) { - assert( - bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty, - 'Bodies require fixtures to contact each other.', - ); - - final fixtureA = bodyA.body.fixtures.first; - final fixtureB = bodyB.body.fixtures.first; - final contact = Contact.init(fixtureA, 0, fixtureB, 0); - game.world.contactManager.contactListener?.beginContact(contact); -} diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 6621abcc..613fd5b8 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -1,8 +1,3 @@ -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/pump_app.dart b/test/helpers/pump_app.dart index a7d7ae67..45929978 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -24,12 +24,12 @@ class _MockGameBloc extends Mock implements GameBloc {} class _MockStartGameBloc extends Mock implements StartGameBloc {} -class _MockPinballAudio extends Mock implements PinballAudio {} +class _MockPinballPlayer extends Mock implements PinballPlayer {} -PinballAudio _buildDefaultPinballAudio() { - final audio = _MockPinballAudio(); - when(audio.load).thenAnswer((_) => Future.value()); - return audio; +PinballPlayer _buildDefaultPinballPlayer() { + final player = _MockPinballPlayer(); + when(player.load).thenAnswer((_) => [Future.value()]); + return player; } AssetsManagerCubit _buildDefaultAssetsManagerCubit() { @@ -55,7 +55,7 @@ extension PumpApp on WidgetTester { AssetsManagerCubit? assetsManagerCubit, CharacterThemeCubit? characterThemeCubit, LeaderboardRepository? leaderboardRepository, - PinballAudio? pinballAudio, + PinballPlayer? pinballPlayer, }) { return runAsync(() { return pumpWidget( @@ -65,7 +65,7 @@ extension PumpApp on WidgetTester { value: leaderboardRepository ?? _MockLeaderboardRepository(), ), RepositoryProvider.value( - value: pinballAudio ?? _buildDefaultPinballAudio(), + value: pinballPlayer ?? _buildDefaultPinballPlayer(), ), ], child: MultiBlocProvider( diff --git a/test/helpers/test_games.dart b/test/helpers/test_games.dart deleted file mode 100644 index aa1ef777..00000000 --- a/test/helpers/test_games.dart +++ /dev/null @@ -1,112 +0,0 @@ -// ignore_for_file: must_call_super - -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 = ''; - } -} - -class PinballTestGame extends PinballGame { - PinballTestGame({ - List? assets, - PinballAudio? audio, - CharacterTheme? theme, - AppLocalizations? l10n, - }) : _assets = assets, - super( - audio: audio ?? _MockPinballAudio(), - characterTheme: theme ?? const DashTheme(), - l10n: l10n ?? _MockAppLocalizations(), - ); - final List? _assets; - - @override - Future onLoad() async { - if (_assets != null) { - await images.loadAll(_assets!); - } - await super.onLoad(); - } -} - -class DebugPinballTestGame extends DebugPinballGame { - DebugPinballTestGame({ - List? assets, - PinballAudio? audio, - CharacterTheme? theme, - AppLocalizations? l10n, - }) : _assets = assets, - super( - audio: audio ?? _MockPinballAudio(), - characterTheme: theme ?? const DashTheme(), - l10n: l10n ?? _MockAppLocalizations(), - ); - - final List? _assets; - - @override - Future onLoad() async { - if (_assets != null) { - await images.loadAll(_assets!); - } - await super.onLoad(); - } -} - -class EmptyPinballTestGame extends PinballTestGame { - EmptyPinballTestGame({ - 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 - Future onLoad() async { - if (_assets != null) { - await images.loadAll(_assets!); - } - } -} diff --git a/test/helpers/text_span.dart b/test/helpers/text_span.dart deleted file mode 100644 index c98d33d9..00000000 --- a/test/helpers/text_span.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; - -bool tapTextSpan(RichText richText, String text) { - final isTapped = !richText.text.visitChildren( - (visitor) => _findTextAndTap(visitor, text), - ); - return isTapped; -} - -bool _findTextAndTap(InlineSpan visitor, String text) { - if (visitor is TextSpan && visitor.text == text) { - (visitor.recognizer as TapGestureRecognizer?)?.onTap?.call(); - return false; - } - return true; -} diff --git a/test/leaderboard/models/leader_board_entry_test.dart b/test/leaderboard/models/leader_board_entry_test.dart new file mode 100644 index 00000000..aa0e10a7 --- /dev/null +++ b/test/leaderboard/models/leader_board_entry_test.dart @@ -0,0 +1,42 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:pinball/leaderboard/models/leader_board_entry.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +void main() { + group('LeaderboardEntry', () { + group('toEntry', () { + test('returns the correct from a to entry data', () { + expect( + LeaderboardEntryData.empty.toEntry(1), + LeaderboardEntry( + rank: '1', + playerInitials: '', + score: 0, + character: CharacterType.dash.toTheme.leaderboardIcon, + ), + ); + }); + }); + + group('CharacterType', () { + test('toTheme returns the correct theme', () { + expect(CharacterType.dash.toTheme, equals(DashTheme())); + expect(CharacterType.sparky.toTheme, equals(SparkyTheme())); + expect(CharacterType.android.toTheme, equals(AndroidTheme())); + expect(CharacterType.dino.toTheme, equals(DinoTheme())); + }); + }); + + group('CharacterTheme', () { + test('toType returns the correct type', () { + expect(DashTheme().toType, equals(CharacterType.dash)); + expect(SparkyTheme().toType, equals(CharacterType.sparky)); + expect(AndroidTheme().toType, equals(CharacterType.android)); + expect(DinoTheme().toType, equals(CharacterType.dino)); + }); + }); + }); +} diff --git a/test/start_game/bloc/start_game_bloc_test.dart b/test/start_game/bloc/start_game_bloc_test.dart index ac548a93..45460fe3 100644 --- a/test/start_game/bloc/start_game_bloc_test.dart +++ b/test/start_game/bloc/start_game_bloc_test.dart @@ -1,26 +1,8 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball/start_game/bloc/start_game_bloc.dart'; -class _MockPinballGame extends Mock implements PinballGame {} - -class _MockGameFlowController extends Mock implements GameFlowController {} - void main() { - late PinballGame pinballGame; - - setUp(() { - pinballGame = _MockPinballGame(); - - when( - () => pinballGame.gameFlowController, - ).thenReturn( - _MockGameFlowController(), - ); - }); - group('StartGameBloc', () { blocTest( 'on PlayTapped changes status to selectCharacter', diff --git a/test/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart index ca646bc9..4e25796b 100644 --- a/test/start_game/widgets/start_game_listener_test.dart +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -12,18 +12,15 @@ import '../../helpers/helpers.dart'; class _MockStartGameBloc extends Mock implements StartGameBloc {} -class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} - -class _MockPinballGame extends Mock implements PinballGame {} +class _MockGameBloc extends Mock implements GameBloc {} -class _MockGameFlowController extends Mock implements GameFlowController {} +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} -class _MockPinballAudio extends Mock implements PinballAudio {} +class _MockPinballPlayer extends Mock implements PinballPlayer {} void main() { late StartGameBloc startGameBloc; - late PinballGame pinballGame; - late PinballAudio pinballAudio; + late PinballPlayer pinballPlayer; late CharacterThemeCubit characterThemeCubit; group('StartGameListener', () { @@ -31,14 +28,25 @@ void main() { await mockFlameImages(); startGameBloc = _MockStartGameBloc(); - pinballGame = _MockPinballGame(); - pinballAudio = _MockPinballAudio(); + pinballPlayer = _MockPinballPlayer(); characterThemeCubit = _MockCharacterThemeCubit(); }); group('on selectCharacter status', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + testWidgets( - 'calls start on the game controller', + 'calls onGameStarted event', (tester) async { whenListen( startGameBloc, @@ -47,19 +55,16 @@ void main() { ), initialState: const StartGameState.initial(), ); - final gameController = _MockGameFlowController(); - when(() => pinballGame.gameFlowController) - .thenAnswer((_) => gameController); await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), + gameBloc: gameBloc, startGameBloc: startGameBloc, ); - verify(gameController.start).called(1); + verify(() => gameBloc.add(const GameStarted())).called(1); }, ); @@ -78,15 +83,12 @@ void main() { 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(), + const StartGameListener( + child: SizedBox.shrink(), ), + gameBloc: gameBloc, startGameBloc: startGameBloc, characterThemeCubit: characterThemeCubit, ); @@ -113,9 +115,8 @@ void main() { ); await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, ); @@ -141,9 +142,8 @@ void main() { ); await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, ); @@ -173,9 +173,8 @@ void main() { ); await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, ); @@ -208,9 +207,8 @@ void main() { 'adds HowToPlayFinished event', (tester) async { await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, ); @@ -239,12 +237,11 @@ void main() { 'plays the I/O Pinball voice over audio', (tester) async { await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, - pinballAudio: pinballAudio, + pinballPlayer: pinballPlayer, ); await tester.pumpAndSettle(); @@ -261,7 +258,8 @@ void main() { ); await tester.pumpAndSettle(); - verify(pinballAudio.ioPinballVoiceOver).called(1); + verify(() => pinballPlayer.play(PinballAudio.ioPinballVoiceOver)) + .called(1); }, ); });