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 f87b4f10..44cce1df 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -1,3 +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 58% rename from lib/game/behaviors/bumper_noisy_behavior.dart rename to lib/game/behaviors/bumper_noise_behavior.dart index 86c9f7b0..9c5da701 100644 --- a/lib/game/behaviors/bumper_noisy_behavior.dart +++ b/lib/game/behaviors/bumper_noise_behavior.dart @@ -1,15 +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.player.play(PinballAudio.bumper); + readProvider().play(PinballAudio.bumper); } } diff --git a/lib/game/behaviors/camera_focusing_behavior.dart b/lib/game/behaviors/camera_focusing_behavior.dart index 9b753469..8a13821d 100644 --- a/lib/game/behaviors/camera_focusing_behavior.dart +++ b/lib/game/behaviors/camera_focusing_behavior.dart @@ -3,7 +3,6 @@ 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'; -import 'package:pinball_flame/pinball_flame.dart'; /// {@template focus_data} /// Defines a [Camera] focus point. @@ -24,7 +23,7 @@ class FocusData { /// Changes the game focus when the [GameBloc] status changes. class CameraFocusingBehavior extends Component - with ParentIsA, BlocComponent { + with FlameBlocListenable, HasGameRef { late final Map _foci; @override @@ -51,15 +50,15 @@ class CameraFocusingBehavior extends Component await super.onLoad(); _foci = { 'game': FocusData( - zoom: parent.size.y / 16, + zoom: gameRef.size.y / 16, position: Vector2(0, -7.8), ), 'waiting': FocusData( - zoom: parent.size.y / 18, + zoom: gameRef.size.y / 18, position: Vector2(0, -112), ), 'backbox': FocusData( - zoom: parent.size.y / 10, + zoom: gameRef.size.y / 10, position: Vector2(0, -111), ), }; @@ -68,7 +67,7 @@ class CameraFocusingBehavior extends Component } void _snap(FocusData data) { - parent.camera + gameRef.camera ..speed = 100 ..followVector2(data.position) ..zoom = data.zoom; @@ -77,7 +76,7 @@ class CameraFocusingBehavior extends Component void _zoom(FocusData data) { final zoom = CameraZoom(value: data.zoom); zoom.completed.then((_) { - parent.camera.moveTo(data.position); + 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_state.dart b/lib/game/bloc/game_state.dart index a9e86720..d0311442 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -27,6 +27,7 @@ enum GameStatus { } extension GameStatusX on GameStatus { + bool get isWaiting => this == GameStatus.waiting; bool get isPlaying => this == GameStatus.playing; bool get isGameOver => this == GameStatus.gameOver; } diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index fd0a4711..7f9fff13 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -35,19 +35,19 @@ class AndroidAcres extends Component { AndroidBumper.a( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(-25.2, 1.5), AndroidBumper.b( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(-32.9, -9.3), AndroidBumper.cow( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..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 30b2a1aa..9414ae96 100644 --- a/lib/game/components/backbox/backbox.dart +++ b/lib/game/components/backbox/backbox.dart @@ -5,7 +5,6 @@ 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/game/pinball_game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' hide Assets; @@ -13,7 +12,7 @@ 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({ required LeaderboardRepository leaderboardRepository, diff --git a/lib/game/components/backbox/displays/initials_input_display.dart b/lib/game/components/backbox/displays/initials_input_display.dart index f4900891..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'; @@ -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,8 +132,7 @@ class _ScoreTextComponent extends TextComponent { ); } -class _NameLabelTextComponent extends TextComponent - with HasGameRef { +class _NameLabelTextComponent extends TextComponent { _NameLabelTextComponent() : super( anchor: Anchor.center, @@ -149,7 +147,7 @@ class _NameLabelTextComponent extends TextComponent @override Future onLoad() async { await super.onLoad(); - text = gameRef.l10n.name; + text = readProvider().name; } } @@ -300,8 +298,7 @@ class _InstructionsComponent extends PositionComponent with HasGameRef { ); } -class _EnterInitialsTextComponent extends TextComponent - with HasGameRef { +class _EnterInitialsTextComponent extends TextComponent { _EnterInitialsTextComponent() : super( anchor: Anchor.center, @@ -312,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, @@ -331,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, @@ -347,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, @@ -367,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, @@ -383,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 index 178354c2..4cc5a9f5 100644 --- a/lib/game/components/backbox/displays/initials_submission_failure_display.dart +++ b/lib/game/components/backbox/displays/initials_submission_failure_display.dart @@ -15,8 +15,7 @@ final _bodyTextPaint = TextPaint( /// {@template initials_submission_failure_display} /// [Backbox] display for when a failure occurs during initials submission. /// {@endtemplate} -class InitialsSubmissionFailureDisplay extends TextComponent - with HasGameRef { +class InitialsSubmissionFailureDisplay extends TextComponent { @override Future onLoad() async { await super.onLoad(); diff --git a/lib/game/components/backbox/displays/initials_submission_success_display.dart b/lib/game/components/backbox/displays/initials_submission_success_display.dart index 46c35b0e..c963a660 100644 --- a/lib/game/components/backbox/displays/initials_submission_success_display.dart +++ b/lib/game/components/backbox/displays/initials_submission_success_display.dart @@ -15,8 +15,7 @@ final _bodyTextPaint = TextPaint( /// {@template initials_submission_success_display} /// [Backbox] display for initials successfully submitted. /// {@endtemplate} -class InitialsSubmissionSuccessDisplay extends TextComponent - with HasGameRef { +class InitialsSubmissionSuccessDisplay extends TextComponent { @override Future onLoad() async { await super.onLoad(); diff --git a/lib/game/components/backbox/displays/loading_display.dart b/lib/game/components/backbox/displays/loading_display.dart index 7b1d4280..6178b940 100644 --- a/lib/game/components/backbox/displays/loading_display.dart +++ b/lib/game/components/backbox/displays/loading_display.dart @@ -1,7 +1,8 @@ import 'package:flame/components.dart'; import 'package:flutter/material.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'; final _bodyTextPaint = TextPaint( @@ -15,7 +16,7 @@ final _bodyTextPaint = TextPaint( /// {@template loading_display} /// Display used to show the loading animation. /// {@endtemplate} -class LoadingDisplay extends TextComponent with HasGameRef { +class LoadingDisplay extends TextComponent { /// {@template loading_display} LoadingDisplay(); @@ -27,7 +28,7 @@ class LoadingDisplay extends TextComponent with HasGameRef { position = Vector2(0, -10); anchor = Anchor.center; - text = _label = gameRef.l10n.loading; + text = _label = readProvider().loading; textRenderer = _bodyTextPaint; await add( diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 8f900475..b96b6a65 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -5,7 +5,7 @@ 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_bloc_status_listener.dart'; export 'google_word/google_word.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 9d5a8164..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?.status.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 999fae5e..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?.status.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/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 9116d25b..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.2, -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 746d9c44..1cc055ae 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -19,25 +19,25 @@ class FlutterForest extends Component with ZIndex { Signpost( children: [ ScoringContactBehavior(points: Points.fiveThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..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(21.8, -46.75), DashAnimatronic()..position = Vector2(20, -66), diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index 167447e6..6e11f3d6 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -2,10 +2,12 @@ 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 BlocComponent, HasGameRef { + with FlameBlocListenable, HasGameRef { @override bool listenWhen(GameState? previousState, GameState newState) { return previousState?.status != newState.status; @@ -17,14 +19,14 @@ class GameBlocStatusListener extends Component case GameStatus.waiting: break; case GameStatus.playing: - gameRef.player.play(PinballAudio.backgroundMusic); + readProvider().play(PinballAudio.backgroundMusic); gameRef.overlays.remove(PinballGame.playButtonOverlay); break; case GameStatus.gameOver: - gameRef.player.play(PinballAudio.gameOverVoiceOver); + readProvider().play(PinballAudio.gameOverVoiceOver); gameRef.descendants().whereType().first.requestInitials( score: state.displayScore, - character: gameRef.characterTheme, + character: readProvider(), ); break; } 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 a9522e76..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,4 +1,5 @@ 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'; @@ -6,7 +7,7 @@ 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(); @@ -21,10 +22,8 @@ class GoogleWordBonusBehavior extends Component .every((letter) => letter.bloc.state == GoogleLetterState.lit); if (achievedBonus) { - gameRef.player.play(PinballAudio.google); - 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/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/sparky_scorch.dart b/lib/game/components/sparky_scorch.dart index 0fee3e13..b820e89d 100644 --- a/lib/game/components/sparky_scorch.dart +++ b/lib/game/components/sparky_scorch.dart @@ -18,19 +18,19 @@ 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.2, -49.9), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index d9bba606..b4886e4c 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -17,19 +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 CharacterTheme characterTheme, required this.leaderboardRepository, - required this.l10n, - required this.player, - }) : super(gravity: Vector2(0, 30)) { + 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 @@ -38,62 +40,68 @@ class PinballGame extends PinballForge2DGame @override Color backgroundColor() => Colors.transparent; - final CharacterTheme characterTheme; + final CharacterTheme _characterTheme; - final PinballPlayer player; + final PinballPlayer _player; final LeaderboardRepository leaderboardRepository; - final AppLocalizations l10n; + final AppLocalizations _l10n; + + final GameBloc _gameBloc; @override Future onLoad() async { - final machine = [ - BoardBackgroundSpriteComponent(), - Boundaries(), - Backbox(leaderboardRepository: leaderboardRepository), - ]; - final decals = [ - GoogleWord(position: Vector2(-4.45, 1.8)), - Multipliers(), - Multiballs(), - SkillShot( + await add( + FlameBlocProvider.value( + value: _gameBloc, children: [ - ScoringContactBehavior(points: Points.oneMillion), + MultiFlameProvider( + providers: [ + FlameProvider.value(_player), + FlameProvider.value(_characterTheme), + FlameProvider.value(leaderboardRepository), + FlameProvider.value(_l10n), + ], + children: [ + 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(), + ], + ), + ], + ), + ], + ), ], ), - ]; - final characterAreas = [ - AndroidAcres(), - DinoDesert(), - FlutterForest(), - SparkyScorch(), - ]; - - await addAll( - [ - GameBlocStatusListener(), - CameraFocusingBehavior(), - CanvasComponent( - onSpritePainted: (paint) { - if (paint.filterQuality != FilterQuality.medium) { - paint.filterQuality = FilterQuality.medium; - } - }, - children: [ - ZCanvasComponent( - children: [ - ...machine, - ...decals, - ...characterAreas, - Drain(), - BottomGroup(), - Launcher(), - ], - ), - ], - ), - ], ); await super.onLoad(); @@ -147,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; - return noBallsLeft && newState.status.isPlaying; - } - - @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 LeaderboardRepository leaderboardRepository, required AppLocalizations l10n, required PinballPlayer player, + required GameBloc gameBloc, }) : super( characterTheme: characterTheme, 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 31ba304b..be6615f1 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -40,33 +40,40 @@ class PinballGamePage extends StatelessWidget { final player = context.read(); final leaderboardRepository = context.read(); - final game = isDebugMode - ? DebugPinballGame( - characterTheme: characterTheme, - player: player, - leaderboardRepository: leaderboardRepository, - l10n: context.l10n, - ) - : PinballGame( - characterTheme: characterTheme, - player: player, - leaderboardRepository: leaderboardRepository, - 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(), - ...player.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), + ); + }, + ), ); } } 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 2bace523..916906c4 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -14,11 +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 dd3e8242..56289417 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -22,6 +22,9 @@ enum PinballAudio { /// Game over gameOverVoiceOver, + + /// Launcher + launcher, } /// Defines the contract of the creation of an [AudioPool]. @@ -158,6 +161,11 @@ class PinballPlayer { playSingleAudio: _playSingleAudio, path: Assets.sfx.google, ), + PinballAudio.launcher: _SimplePlayAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.launcher, + ), PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( preCacheSingleAudio: _preCacheSingleAudio, playSingleAudio: _playSingleAudio, diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index b7760aa5..fdcd661b 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -151,6 +151,10 @@ void main() { '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'), @@ -219,6 +223,18 @@ 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 Future.wait(player.load()); diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart index 040c3287..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,10 +97,12 @@ 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 @@ -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/z_indexes.dart b/packages/pinball_components/lib/src/components/z_indexes.dart index d15cea64..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 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 fd759f8d..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 { 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/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/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/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 64% rename from test/game/behaviors/bumper_noisy_behavior_test.dart rename to test/game/behaviors/bumper_noise_behavior_test.dart index 18d90fbd..d8075726 100644 --- a/test/game/behaviors/bumper_noisy_behavior_test.dart +++ b/test/game/behaviors/bumper_noise_behavior_test.dart @@ -6,14 +6,24 @@ 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 _MockPinballPlayer extends Mock implements PinballPlayer {} @@ -23,12 +33,10 @@ class _MockContact extends Mock implements Contact {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('BumperNoisyBehavior', () {}); + group('BumperNoiseBehavior', () {}); late PinballPlayer player; - final flameTester = FlameTester( - () => EmptyPinballTestGame(player: player), - ); + final flameTester = FlameTester(_TestGame.new); setUp(() { player = _MockPinballPlayer(); @@ -37,9 +45,9 @@ void main() { 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()); }, diff --git a/test/game/behaviors/camera_focusing_behavior_test.dart b/test/game/behaviors/camera_focusing_behavior_test.dart index ba6ea3a1..a856b392 100644 --- a/test/game/behaviors/camera_focusing_behavior_test.dart +++ b/test/game/behaviors/camera_focusing_behavior_test.dart @@ -1,22 +1,20 @@ // 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'; -import '../../helpers/helpers.dart'; - void main() { TestWidgetsFlutterBinding.ensureInitialized(); group( 'CameraFocusingBehavior', () { - final flameTester = FlameTester( - EmptyPinballTestGame.new, - ); + final flameTester = FlameTester(FlameGame.new); test('can be instantiated', () { expect( @@ -26,9 +24,14 @@ void main() { }); flameTester.test('loads', (game) async { - final behavior = CameraFocusingBehavior(); - await game.ensureAdd(behavior); - expect(game.contains(behavior), isTrue); + late final behavior = CameraFocusingBehavior(); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [behavior], + ), + ); + expect(game.descendants(), contains(behavior)); }); flameTester.test( @@ -38,7 +41,12 @@ void main() { final previousZoom = game.camera.zoom; expect(game.camera.follow, isNull); - await game.ensureAdd(behavior); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [behavior], + ), + ); expect(game.camera.follow, isNotNull); expect(game.camera.zoom, isNot(equals(previousZoom))); @@ -77,8 +85,12 @@ void main() { const GameState.initial().copyWith(status: GameStatus.playing); final behavior = CameraFocusingBehavior(); - await game.ensureAdd(behavior); - + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [behavior], + ), + ); behavior.onNewState(playing); final previousPosition = game.camera.position.clone(); await game.ready(); @@ -103,7 +115,12 @@ void main() { ); final behavior = CameraFocusingBehavior(); - await game.ensureAdd(behavior); + await game.ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [behavior], + ), + ); behavior.onNewState(playing); final previousPosition = game.camera.position.clone(); diff --git a/test/game/behaviors/scoring_behavior_test.dart b/test/game/behaviors/scoring_behavior_test.dart index 5673e165..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,23 +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: [], - status: GameStatus.playing, - ); - whenListen(bloc, Stream.value(state), initialState: state); - return bloc; - }, - assets: assets, - ); + final flameBlocTester = FlameTester(_TestGame.new); group('ScoringBehavior', () { test('can be instantiated', () { @@ -76,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(), @@ -94,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), @@ -115,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); @@ -145,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( @@ -173,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); @@ -192,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/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 33d43aa8..52e2746e 100644 --- a/test/game/components/backbox/backbox_test.dart +++ b/test/game/components/backbox/backbox_test.dart @@ -3,7 +3,9 @@ import 'dart:async'; 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/material.dart'; import 'package:flutter/services.dart'; @@ -15,9 +17,39 @@ 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 @@ -65,18 +97,8 @@ class _MockAppLocalizations extends Mock implements AppLocalizations { void main() { TestWidgetsFlutterBinding.ensureInitialized(); - const character = theme.AndroidTheme(); - final assets = [ - character.leaderboardIcon.keyName, - 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; @@ -94,27 +116,26 @@ void main() { 'loads correctly', (game) async { final backbox = Backbox.test(bloc: bloc); - await game.ensureAdd(backbox); - - expect(game.children, contains(backbox)); + 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( + 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'), ); }, @@ -128,10 +149,10 @@ void main() { leaderboardRepository: _MockLeaderboardRepository(), ), ); - await game.ensureAdd(backbox); + await game.pump(backbox); backbox.requestInitials( score: 0, - character: character, + character: game.character, ); await game.ready(); @@ -148,7 +169,7 @@ void main() { final bloc = _MockBackboxBloc(); final state = InitialsFormState( score: 10, - character: theme.AndroidTheme(), + character: game.character, ); whenListen( bloc, @@ -156,7 +177,7 @@ void main() { initialState: state, ); final backbox = Backbox.test(bloc: bloc); - await game.ensureAdd(backbox); + await game.pump(backbox); game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {}); verify( @@ -164,7 +185,7 @@ void main() { PlayerInitialsSubmitted( score: 10, initials: 'AAA', - character: theme.AndroidTheme(), + character: game.character, ), ), ).called(1); @@ -180,7 +201,7 @@ void main() { initialState: InitialsSuccessState(), ); final backbox = Backbox.test(bloc: bloc); - await game.ensureAdd(backbox); + await game.pump(backbox); expect( game @@ -201,7 +222,7 @@ void main() { initialState: InitialsFailureState(), ); final backbox = Backbox.test(bloc: bloc); - await game.ensureAdd(backbox); + await game.pump(backbox); expect( game @@ -224,7 +245,7 @@ void main() { ); final backbox = Backbox.test(bloc: bloc); - await game.ensureAdd(backbox); + await game.pump(backbox); backbox.removeFromParent(); await game.ready(); 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 e2a3c58c..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,43 +71,33 @@ 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 on the first letter await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); @@ -99,10 +122,10 @@ void main() { 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,7 +155,7 @@ 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()), ); @@ -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 index 5989445f..b37b41e7 100644 --- a/test/game/components/backbox/displays/initials_submission_failure_display_test.dart +++ b/test/game/components/backbox/displays/initials_submission_failure_display_test.dart @@ -1,15 +1,14 @@ // 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'; -import '../../../../helpers/helpers.dart'; - void main() { group('InitialsSubmissionFailureDisplay', () { - final flameTester = FlameTester(EmptyKeyboardPinballTestGame.new); + final flameTester = FlameTester(Forge2DGame.new); flameTester.test('renders correctly', (game) async { await game.ensureAdd(InitialsSubmissionFailureDisplay()); 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 index 1bd1fcd9..7ad3d182 100644 --- a/test/game/components/backbox/displays/initials_submission_success_display_test.dart +++ b/test/game/components/backbox/displays/initials_submission_success_display_test.dart @@ -1,15 +1,14 @@ // 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'; -import '../../../../helpers/helpers.dart'; - void main() { group('InitialsSubmissionSuccessDisplay', () { - final flameTester = FlameTester(EmptyKeyboardPinballTestGame.new); + final flameTester = FlameTester(Forge2DGame.new); flameTester.test('renders correctly', (game) async { await game.ensureAdd(InitialsSubmissionSuccessDisplay()); diff --git a/test/game/components/backbox/displays/loading_display_test.dart b/test/game/components/backbox/displays/loading_display_test.dart index a09d0d68..efd84097 100644 --- a/test/game/components/backbox/displays/loading_display_test.dart +++ b/test/game/components/backbox/displays/loading_display_test.dart @@ -1,13 +1,31 @@ // 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'; -import '../../../../helpers/helpers.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 @@ -16,39 +34,35 @@ class _MockAppLocalizations extends Mock implements AppLocalizations { void main() { group('LoadingDisplay', () { - final flameTester = FlameTester( - () => EmptyPinballTestGame( - l10n: _MockAppLocalizations(), - ), - ); + final flameTester = FlameTester(_TestGame.new); flameTester.test('renders correctly', (game) async { - await game.ensureAdd(LoadingDisplay()); + await game.pump(LoadingDisplay()); - final component = game.firstChild(); + final component = game.descendants().whereType().first; expect(component, isNotNull); - expect(component?.text, equals('Loading')); + expect(component.text, equals('Loading')); }); flameTester.test('use ellipses as animation', (game) async { - await game.ensureAdd(LoadingDisplay()); + await game.pump(LoadingDisplay()); - final component = game.firstChild(); - expect(component?.text, equals('Loading')); + final component = game.descendants().whereType().first; + expect(component.text, equals('Loading')); - final timer = component?.firstChild(); + final timer = component.firstChild(); timer?.update(1.1); - expect(component?.text, equals('Loading.')); + expect(component.text, equals('Loading.')); timer?.update(1.1); - expect(component?.text, equals('Loading..')); + expect(component.text, equals('Loading..')); timer?.update(1.1); - expect(component?.text, equals('Loading...')); + expect(component.text, equals('Loading...')); timer?.update(1.1); - expect(component?.text, equals('Loading')); + 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/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 af262dbf..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,17 +13,31 @@ 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 flameTester = FlameTester(_TestGame.new); group('FlipperController', () { late GameBloc gameBloc; @@ -29,12 +46,6 @@ void main() { gameBloc = _MockGameBloc(); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); - group('onKeyEvent', () { final leftKeys = UnmodifiableListView([ LogicalKeyboardKey.arrowLeft, @@ -63,11 +74,13 @@ void main() { whenListen( gameBloc, const Stream.empty(), - initialState: const GameState.initial(), + 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); @@ -77,9 +90,9 @@ void main() { }); testRawKeyDownEvents(leftKeys, (event) { - flameBlocTester.testGameWidget( + flameTester.test( 'does nothing when is game over', - setUp: (game, tester) async { + (game) async { whenListen( gameBloc, const Stream.empty(), @@ -88,10 +101,9 @@ void main() { ), ); - await game.ensureAdd(flipper); + 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); }, @@ -106,11 +118,13 @@ void main() { whenListen( gameBloc, const Stream.empty(), - initialState: const GameState.initial(), + 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); @@ -131,7 +145,7 @@ void main() { ); await game.ready(); - await game.add(flipper); + await game.pump(flipper, gameBloc: gameBloc); controller.onKeyEvent(event, {}); expect(flipper.body.linearVelocity.y, isZero); @@ -159,11 +173,13 @@ void main() { whenListen( gameBloc, const Stream.empty(), - initialState: const GameState.initial(), + 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); @@ -180,11 +196,13 @@ void main() { whenListen( gameBloc, const Stream.empty(), - initialState: const GameState.initial(), + 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); @@ -194,9 +212,9 @@ void main() { }); testRawKeyDownEvents(rightKeys, (event) { - flameBlocTester.testGameWidget( + flameTester.test( 'does nothing when is game over', - setUp: (game, tester) async { + (game) async { whenListen( gameBloc, const Stream.empty(), @@ -205,10 +223,9 @@ void main() { ), ); - await game.ensureAdd(flipper); + 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); }, @@ -220,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 f91b0c37..25b1f739 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -1,34 +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 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); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - ); - group('onKeyEvent', () { final downKeys = UnmodifiableListView([ LogicalKeyboardKey.arrowDown, @@ -36,27 +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 { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - - await game.ensureAdd(plunger); + await game.pump(plunger); controller.onKeyEvent(event, {}); expect(plunger.body.linearVelocity.y, isPositive); @@ -71,13 +95,7 @@ void main() { 'when ${event.logicalKey.keyLabel} is released ' 'and plunger is below its starting position', (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - - await game.ensureAdd(plunger); + await game.pump(plunger); plunger.body.setTransform(Vector2(0, 1), 0); controller.onKeyEvent(event, {}); @@ -92,13 +110,7 @@ void main() { 'does not move when ${event.logicalKey.keyLabel} is released ' 'and plunger is in its starting position', (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - - await game.ensureAdd(plunger); + await game.pump(plunger); controller.onKeyEvent(event, {}); expect(plunger.body.linearVelocity.y, isZero); @@ -119,7 +131,7 @@ void main() { ), ); - await game.ensureAdd(plunger); + await game.pump(plunger, gameBloc: gameBloc); controller.onKeyEvent(event, {}); }, verify: (game, tester) async { @@ -129,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 index 73f47161..7118aa8d 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -1,39 +1,89 @@ -// ignore_for_file: type_annotate_public_apis, prefer_const_constructors +// ignore_for_file: cascade_invocations -import 'package:flame/game.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: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_theme/pinball_theme.dart'; - -class _MockPinballGame extends Mock implements PinballGame {} - -class _MockBackbox extends Mock implements Backbox {} - -class _MockActiveOverlaysNotifier extends Mock - implements ActiveOverlaysNotifier {} +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', () { - setUpAll(() { - registerFallbackValue(AndroidTheme()); + 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', () { - final state = GameState( + const state = GameState( totalScore: 0, roundScore: 10, multiplier: 1, rounds: 0, - bonusHistory: const [], + bonusHistory: [], status: GameStatus.playing, ); - final previous = GameState.initial(); + const previous = GameState.initial(); expect( GameBlocStatusListener().listenWhen(previous, state), isTrue, @@ -42,92 +92,52 @@ void main() { }); group('onNewState', () { - late PinballGame game; - late Backbox backbox; - late GameBlocStatusListener gameBlocStatusListener; - late PinballPlayer pinballPlayer; - late ActiveOverlaysNotifier overlays; - - setUp(() { - game = _MockPinballGame(); - backbox = _MockBackbox(); - gameBlocStatusListener = GameBlocStatusListener(); - overlays = _MockActiveOverlaysNotifier(); - pinballPlayer = _MockPinballPlayer(); - - gameBlocStatusListener.mockGameRef(game); - - when( - () => backbox.requestInitials( - score: any(named: 'score'), - character: any(named: 'character'), - ), - ).thenAnswer((_) async {}); - - when(() => overlays.remove(any())).thenAnswer((_) => true); - - when(() => game.descendants().whereType()) - .thenReturn([backbox]); - when(() => game.overlays).thenReturn(overlays); - when(() => game.characterTheme).thenReturn(DashTheme()); - when(() => game.player).thenReturn(pinballPlayer); - }); - - test( + flameTester.test( 'changes the backbox display when the game is over', - () { - final state = GameState( - totalScore: 0, - roundScore: 10, - multiplier: 1, - rounds: 0, - bonusHistory: const [], - status: GameStatus.gameOver, - ); - gameBlocStatusListener.onNewState(state); - - verify( - () => backbox.requestInitials( - score: any(named: 'score'), - character: any(named: 'character'), - ), - ).called(1); + (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); }, ); - test( - 'changes the backbox when it is not a game over', - () { - gameBlocStatusListener.onNewState( - GameState.initial().copyWith(status: GameStatus.playing), - ); - - verify(() => overlays.remove(PinballGame.playButtonOverlay)) - .called(1); - }, - ); - - test( + flameTester.test( 'plays the background music on start', - () { - gameBlocStatusListener.onNewState( - GameState.initial().copyWith(status: GameStatus.playing), + (game) async { + final player = _MockPinballPlayer(); + final component = GameBlocStatusListener(); + await game.pump([component], pinballPlayer: player); + + component.onNewState( + const GameState.initial().copyWith(status: GameStatus.playing), ); - verify(() => pinballPlayer.play(PinballAudio.backgroundMusic)) - .called(1); + verify(() => player.play(PinballAudio.backgroundMusic)).called(1); }, ); - test( + flameTester.test( 'plays the game over voice over when it is game over', - () { - gameBlocStatusListener.onNewState( - GameState.initial().copyWith(status: GameStatus.gameOver), + (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(() => pinballPlayer.play(PinballAudio.gameOverVoiceOver)) - .called(1); + verify(() => player.play(PinballAudio.gameOverVoiceOver)).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 03c50041..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 ' @@ -90,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(); @@ -121,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 ef39aad2..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', () { @@ -63,8 +75,8 @@ void main() { status: GameStatus.playing, bonusHistory: const [], ); - final previous = GameState.initial(); + expect( MultipliersBehavior().listenWhen(previous, state), isTrue, @@ -80,8 +92,8 @@ void main() { status: GameStatus.playing, bonusHistory: const [], ); - final previous = GameState.initial(); + expect( MultipliersBehavior().listenWhen(previous, state), isFalse, @@ -93,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(); @@ -123,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 cf70ad43..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.status).thenReturn(GameStatus.playing); - 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.status).thenReturn(GameStatus.playing); - - 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.status).thenReturn(GameStatus.gameOver); - 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/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/test_games.dart b/test/helpers/test_games.dart deleted file mode 100644 index 220693c3..00000000 --- a/test/helpers/test_games.dart +++ /dev/null @@ -1,122 +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:leaderboard_repository/leaderboard_repository.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 _MockPinballPlayer extends Mock implements PinballPlayer {} - -class _MockAppLocalizations extends Mock implements AppLocalizations {} - -class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { -} - -class TestGame extends Forge2DGame with FlameBloc { - TestGame() { - images.prefix = ''; - } -} - -class PinballTestGame extends PinballGame { - PinballTestGame({ - List? assets, - PinballPlayer? player, - LeaderboardRepository? leaderboardRepository, - CharacterTheme? theme, - AppLocalizations? l10n, - }) : _assets = assets, - super( - player: player ?? _MockPinballPlayer(), - leaderboardRepository: - leaderboardRepository ?? _MockLeaderboardRepository(), - 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, - PinballPlayer? player, - LeaderboardRepository? leaderboardRepository, - CharacterTheme? theme, - AppLocalizations? l10n, - }) : _assets = assets, - super( - player: player ?? _MockPinballPlayer(), - leaderboardRepository: - leaderboardRepository ?? _MockLeaderboardRepository(), - 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, - PinballPlayer? player, - CharacterTheme? theme, - AppLocalizations? l10n, - }) : super( - assets: assets, - player: player, - 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, - PinballPlayer? player, - CharacterTheme? theme, - AppLocalizations? l10n, - }) : super( - assets: assets, - player: player, - 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; -}