From ace61193fb913273120594cb58382a115a93daea Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 5 May 2022 21:05:22 +0100 Subject: [PATCH 1/6] refactor: removed `GameBallsController` (#349) --- .../behaviors/ball_spawning_behavior.dart | 33 +++++ lib/game/behaviors/behaviors.dart | 1 + lib/game/bloc/game_state.dart | 1 + lib/game/components/components.dart | 2 +- lib/game/components/controlled_ball.dart | 17 --- lib/game/components/drain.dart | 34 ----- .../components/drain/behaviors/behaviors.dart | 1 + .../drain/behaviors/draining_behavior.dart | 21 +++ lib/game/components/drain/drain.dart | 36 ++++++ lib/game/pinball_game.dart | 49 +------ .../lib/src/components/plunger.dart | 25 ++-- .../test/src/components/plunger_test.dart | 11 ++ .../ball_spawning_behavior_test.dart | 117 +++++++++++++++++ .../game/components/controlled_ball_test.dart | 37 ------ .../behaviors/draining_behavior_test.dart | 121 ++++++++++++++++++ .../components/{ => drain}/drain_test.dart | 24 +--- test/game/pinball_game_test.dart | 101 ++------------- 17 files changed, 377 insertions(+), 254 deletions(-) create mode 100644 lib/game/behaviors/ball_spawning_behavior.dart delete mode 100644 lib/game/components/drain.dart create mode 100644 lib/game/components/drain/behaviors/behaviors.dart create mode 100644 lib/game/components/drain/behaviors/draining_behavior.dart create mode 100644 lib/game/components/drain/drain.dart create mode 100644 test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart create mode 100644 test/game/components/drain/behaviors/draining_behavior_test.dart rename test/game/components/{ => drain}/drain_test.dart (60%) diff --git a/lib/game/behaviors/ball_spawning_behavior.dart b/lib/game/behaviors/ball_spawning_behavior.dart new file mode 100644 index 00000000..3602615b --- /dev/null +++ b/lib/game/behaviors/ball_spawning_behavior.dart @@ -0,0 +1,33 @@ +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'; + +/// Spawns a new [Ball] into the game when all balls are lost and still +/// [GameStatus.playing]. +class BallSpawningBehavior extends Component + with ParentIsA, BlocComponent { + @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 = parent.descendants().whereType().single; + final canvas = parent.descendants().whereType().single; + final ball = ControlledBall.launch(characterTheme: parent.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..243fff82 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -1,3 +1,4 @@ +export 'ball_spawning_behavior.dart'; export 'bumper_noisy_behavior.dart'; export 'camera_focusing_behavior.dart'; export 'scoring_behavior.dart'; 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/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..2356e0d8 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -22,9 +22,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) { @@ -47,12 +45,6 @@ class BallController extends ComponentController /// {@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 { @@ -70,13 +62,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/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..36512efa --- /dev/null +++ b/lib/game/components/drain/behaviors/draining_behavior.dart @@ -0,0 +1,21 @@ +import 'package:flame/components.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) { + gameRef.read().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/pinball_game.dart b/lib/game/pinball_game.dart index 438dd7da..bbab932b 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -17,11 +17,7 @@ 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 FlameBloc, HasKeyboardHandlerComponents, MultiTouchTapDetector { PinballGame({ required this.characterTheme, required this.leaderboardRepository, @@ -29,7 +25,6 @@ class PinballGame extends PinballForge2DGame required this.player, }) : super(gravity: Vector2(0, 30)) { images.prefix = ''; - controller = _GameBallsController(this); } /// Identifier of the play button overlay @@ -73,6 +68,7 @@ class PinballGame extends PinballForge2DGame await addAll( [ GameBlocStatusListener(), + BallSpawningBehavior(), CameraFocusingBehavior(), CanvasComponent( onSpritePainted: (paint) { @@ -147,43 +143,6 @@ 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, @@ -195,9 +154,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { player: player, leaderboardRepository: leaderboardRepository, l10n: l10n, - ) { - controller = _GameBallsController(this); - } + ); Vector2? lineStart; Vector2? lineEnd; 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/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/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..41c3e301 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart @@ -0,0 +1,117 @@ +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; + +import '../../../../helpers/test_games.dart'; + +class _MockGameState extends Mock implements GameState {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'BallSpawningBehavior', + () { + final flameTester = FlameTester(EmptyPinballTestGame.new); + + test('can be instantiated', () { + expect( + BallSpawningBehavior(), + isA(), + ); + }); + + flameTester.test( + 'loads', + (game) async { + final behavior = BallSpawningBehavior(); + await game.ensureAdd(behavior); + expect(game.contains(behavior), isTrue); + }, + ); + + 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 { + await game.images.load(theme.Assets.images.dash.ball.keyName); + final behavior = BallSpawningBehavior(); + await game.ensureAddAll([ + 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/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index dc142ffd..04ac0e0f 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -63,43 +63,6 @@ void main() { ); }); - flameBlocTester.testGameWidget( - "lost doesn't adds RoundLost to GameBloc " - 'when there are balls left', - 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())); - }, - ); - - 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); - - controller.lost(); - await game.ready(); - }, - verify: (game, tester) async { - verify(() => gameBloc.add(const RoundLost())).called(1); - }, - ); - group('turboCharge', () { setUpAll(() { registerFallbackValue(Vector2.zero()); 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..dbc62006 --- /dev/null +++ b/test/game/components/drain/behaviors/draining_behavior_test.dart @@ -0,0 +1,121 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/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; + +import '../../../../helpers/helpers.dart'; + +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', () { + final asset = theme.Assets.images.dash.ball.keyName; + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'adds RoundLost when no balls left', + setUp: (game, tester) async { + await game.images.load(asset); + + final drain = Drain.test(); + final behavior = DrainingBehavior(); + final ball = Ball.test(); + await drain.add(behavior); + await game.ensureAddAll([drain, ball]); + + behavior.beginContact(ball, _MockContact()); + await game.ready(); + + expect(game.descendants().whereType(), isEmpty); + verify(() => gameBloc.add(const RoundLost())).called(1); + }, + ); + + flameBlocTester.testGameWidget( + "doesn't add RoundLost when there are balls left", + setUp: (game, tester) async { + await game.images.load(asset); + + final drain = Drain.test(); + final behavior = DrainingBehavior(); + final ball1 = Ball.test(); + final ball2 = Ball.test(); + await drain.add(behavior); + await game.ensureAddAll([ + drain, + ball1, + ball2, + ]); + + behavior.beginContact(ball1, _MockContact()); + await game.ready(); + + expect(game.descendants().whereType(), isNotEmpty); + verifyNever(() => gameBloc.add(const RoundLost())); + }, + ); + + flameBlocTester.testGameWidget( + 'removes the Ball', + setUp: (game, tester) async { + await game.images.load(asset); + final drain = Drain.test(); + final behavior = DrainingBehavior(); + final ball = Ball.test(); + await drain.add(behavior); + await game.ensureAddAll([drain, 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 60% rename from test/game/components/drain_test.dart rename to test/game/components/drain/drain_test.dart index 984abce3..98c55ca1 100644 --- a/test/game/components/drain_test.dart +++ b/test/game/components/drain/drain_test.dart @@ -3,16 +3,10 @@ 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 {} +import 'package:pinball/game/game.dart'; -class _MockContact extends Mock implements Contact {} +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -45,19 +39,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/pinball_game_test.dart b/test/game/pinball_game_test.dart index cf70ad43..e2998f5d 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -9,6 +9,7 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter/gestures.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_components/pinball_components.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; @@ -17,8 +18,6 @@ import '../helpers/helpers.dart'; class _MockGameBloc extends Mock implements GameBloc {} -class _MockGameState extends Mock implements GameState {} - class _MockEventPosition extends Mock implements EventPosition {} class _MockTapDownDetails extends Mock implements TapDownDetails {} @@ -167,8 +166,17 @@ void main() { ); group('components', () { - // TODO(alestiago): tests that Blueprints get added once the Blueprint - // class is removed. + flameBlocTester.test( + 'has only one BallSpawningBehavior', + (game) async { + await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameBlocTester.test( 'has only one Drain', (game) async { @@ -272,91 +280,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', () { From df7408728e0be2fe48e411bf3086462d0c695fa7 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 5 May 2022 18:16:08 -0300 Subject: [PATCH 2/6] feat: adding `Plunger` sound effects (#324) * feat: adding launcher sfx * Apply suggestions from code review Co-authored-by: Alejandro Santiago * fix test Co-authored-by: Alejandro Santiago --- lib/game/components/controlled_plunger.dart | 24 ++++++ .../pinball_audio/assets/sfx/launcher.mp3 | Bin 0 -> 19726 bytes .../pinball_audio/lib/gen/assets.gen.dart | 2 + .../pinball_audio/lib/src/pinball_audio.dart | 8 ++ .../test/src/pinball_audio_test.dart | 16 ++++ .../components/controlled_plunger_test.dart | 77 ++++++++++++++---- 6 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 packages/pinball_audio/assets/sfx/launcher.mp3 diff --git a/lib/game/components/controlled_plunger.dart b/lib/game/components/controlled_plunger.dart index 999fae5e..ac9cc19d 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,6 +15,29 @@ class ControlledPlunger extends Plunger with Controls { : super(compressionDistance: compressionDistance) { controller = PlungerController(this); } + + @override + void release() { + super.release(); + + add(PlungerNoisyBehavior()); + } +} + +/// A behavior attached to the plunger when it launches the ball +/// which plays the related sound effects. +class PlungerNoisyBehavior extends Component with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + gameRef.player.play(PinballAudio.launcher); + } + + @override + void update(double dt) { + super.update(dt); + removeFromParent(); + } } /// {@template plunger_controller} diff --git a/packages/pinball_audio/assets/sfx/launcher.mp3 b/packages/pinball_audio/assets/sfx/launcher.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..fde95720759eaf02bf4d8f30b2dbc8cb2b476d95 GIT binary patch literal 19726 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 a_LnP`Nn1h%HA7dy=OGZ&?xU>X9s&ULC0RBA literal 0 HcmV?d00001 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/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index f91b0c37..211cf82f 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -1,18 +1,25 @@ +// ignore_for_file: cascade_invocations + import 'dart:collection'; import 'package:bloc_test/bloc_test.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame/components.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 '../../helpers/helpers.dart'; class _MockGameBloc extends Mock implements GameBloc {} +class _MockPinballPlayer extends Mock implements PinballPlayer {} + +class _MockPinballGame extends Mock implements PinballGame {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(EmptyPinballTestGame.new); @@ -20,15 +27,21 @@ void main() { group('PlungerController', () { late GameBloc gameBloc; - setUp(() { - gameBloc = _MockGameBloc(); - }); - final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, ); + late Plunger plunger; + late PlungerController controller; + + setUp(() { + gameBloc = _MockGameBloc(); + plunger = ControlledPlunger(compressionDistance: 10); + controller = PlungerController(plunger); + plunger.add(controller); + }); + group('onKeyEvent', () { final downKeys = UnmodifiableListView([ LogicalKeyboardKey.arrowDown, @@ -36,15 +49,6 @@ 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 ' @@ -129,5 +133,50 @@ void main() { ); }); }); + + flameTester.test( + 'adds the PlungerNoisyBehavior plunger is released', + (game) async { + await game.ensureAdd(plunger); + plunger.body.setTransform(Vector2(0, 1), 0); + plunger.release(); + + await game.ready(); + final count = + game.descendants().whereType().length; + expect(count, equals(1)); + }, + ); + }); + + group('PlungerNoisyBehavior', () { + late PinballGame game; + late PinballPlayer player; + late PlungerNoisyBehavior behavior; + + setUp(() { + game = _MockPinballGame(); + player = _MockPinballPlayer(); + + when(() => game.player).thenReturn(player); + behavior = PlungerNoisyBehavior(); + behavior.mockGameRef(game); + }); + + test('plays the correct sound on load', () async { + await behavior.onLoad(); + + verify(() => player.play(PinballAudio.launcher)).called(1); + }); + + test('is removed on the first update', () { + final parent = Component(); + 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); + }); }); } From 135b4ed5c2d8c69ec6792a90c2a99227e08b81d0 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 5 May 2022 18:34:20 -0300 Subject: [PATCH 3/6] refactor: renaming noisy to noise (#353) --- lib/game/behaviors/behaviors.dart | 2 +- ..._noisy_behavior.dart => bumper_noise_behavior.dart} | 2 +- lib/game/components/android_acres/android_acres.dart | 6 +++--- lib/game/components/controlled_plunger.dart | 4 ++-- lib/game/components/flutter_forest/flutter_forest.dart | 8 ++++---- lib/game/components/sparky_scorch.dart | 6 +++--- test/game/behaviors/bumper_noisy_behavior_test.dart | 4 ++-- .../components/android_acres/android_acres_test.dart | 6 +++--- test/game/components/controlled_plunger_test.dart | 10 +++++----- .../components/flutter_forest/flutter_forest_test.dart | 4 ++-- test/game/components/sparky_scorch_test.dart | 4 ++-- 11 files changed, 28 insertions(+), 28 deletions(-) rename lib/game/behaviors/{bumper_noisy_behavior.dart => bumper_noise_behavior.dart} (87%) diff --git a/lib/game/behaviors/behaviors.dart b/lib/game/behaviors/behaviors.dart index 243fff82..44cce1df 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -1,4 +1,4 @@ export 'ball_spawning_behavior.dart'; -export 'bumper_noisy_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 87% rename from lib/game/behaviors/bumper_noisy_behavior.dart rename to lib/game/behaviors/bumper_noise_behavior.dart index 86c9f7b0..e89ec23a 100644 --- a/lib/game/behaviors/bumper_noisy_behavior.dart +++ b/lib/game/behaviors/bumper_noise_behavior.dart @@ -6,7 +6,7 @@ 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 with HasGameRef { @override void beginContact(Object other, Contact contact) { super.beginContact(other, contact); diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index 649ef196..af4ec451 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, 1.3), AndroidBumper.b( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(-32.8, -9.2), AndroidBumper.cow( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(-20.5, -13.8), AndroidSpaceshipBonusBehavior(), diff --git a/lib/game/components/controlled_plunger.dart b/lib/game/components/controlled_plunger.dart index ac9cc19d..bcebbacc 100644 --- a/lib/game/components/controlled_plunger.dart +++ b/lib/game/components/controlled_plunger.dart @@ -20,13 +20,13 @@ class ControlledPlunger extends Plunger with Controls { void release() { super.release(); - add(PlungerNoisyBehavior()); + add(PlungerNoiseBehavior()); } } /// A behavior attached to the plunger when it launches the ball /// which plays the related sound effects. -class PlungerNoisyBehavior extends Component with HasGameRef { +class PlungerNoiseBehavior extends Component with HasGameRef { @override Future onLoad() async { await super.onLoad(); diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index 259b6bb2..a9219ba0 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(8.35, -58.3), DashNestBumper.main( children: [ ScoringContactBehavior(points: Points.twoHundredThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(18.55, -59.35), DashNestBumper.a( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(8.95, -51.95), DashNestBumper.b( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoisyBehavior(), + BumperNoiseBehavior(), ], )..initialPosition = Vector2(22.3, -46.75), DashAnimatronic()..position = Vector2(20, -66), diff --git a/lib/game/components/sparky_scorch.dart b/lib/game/components/sparky_scorch.dart index 5a266b4e..e7e5004a 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, -49.9), diff --git a/test/game/behaviors/bumper_noisy_behavior_test.dart b/test/game/behaviors/bumper_noisy_behavior_test.dart index 18d90fbd..e860a094 100644 --- a/test/game/behaviors/bumper_noisy_behavior_test.dart +++ b/test/game/behaviors/bumper_noisy_behavior_test.dart @@ -23,7 +23,7 @@ class _MockContact extends Mock implements Contact {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('BumperNoisyBehavior', () {}); + group('BumperNoiseBehavior', () {}); late PinballPlayer player; final flameTester = FlameTester( @@ -37,7 +37,7 @@ void main() { flameTester.testGameWidget( 'plays bumper sound', setUp: (game, _) async { - final behavior = BumperNoisyBehavior(); + final behavior = BumperNoiseBehavior(); final parent = _TestBodyComponent(); await game.ensureAdd(parent); 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..5de7576b 100644 --- a/test/game/components/android_acres/android_acres_test.dart +++ b/test/game/components/android_acres/android_acres_test.dart @@ -2,7 +2,7 @@ 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'; @@ -103,13 +103,13 @@ void main() { ); flameTester.test( - 'three AndroidBumpers with BumperNoisyBehavior', + 'three AndroidBumpers with BumperNoiseBehavior', (game) async { await game.ensureAdd(AndroidAcres()); final bumpers = game.descendants().whereType(); for (final bumper in bumpers) { expect( - bumper.firstChild(), + bumper.firstChild(), isNotNull, ); } diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index 211cf82f..f772b39a 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -135,7 +135,7 @@ void main() { }); flameTester.test( - 'adds the PlungerNoisyBehavior plunger is released', + 'adds the PlungerNoiseBehavior plunger is released', (game) async { await game.ensureAdd(plunger); plunger.body.setTransform(Vector2(0, 1), 0); @@ -143,23 +143,23 @@ void main() { await game.ready(); final count = - game.descendants().whereType().length; + game.descendants().whereType().length; expect(count, equals(1)); }, ); }); - group('PlungerNoisyBehavior', () { + group('PlungerNoiseBehavior', () { late PinballGame game; late PinballPlayer player; - late PlungerNoisyBehavior behavior; + late PlungerNoiseBehavior behavior; setUp(() { game = _MockPinballGame(); player = _MockPinballPlayer(); when(() => game.player).thenReturn(player); - behavior = PlungerNoisyBehavior(); + behavior = PlungerNoiseBehavior(); behavior.mockGameRef(game); }); diff --git a/test/game/components/flutter_forest/flutter_forest_test.dart b/test/game/components/flutter_forest/flutter_forest_test.dart index 6dddcd7b..bc0e5ff4 100644 --- a/test/game/components/flutter_forest/flutter_forest_test.dart +++ b/test/game/components/flutter_forest/flutter_forest_test.dart @@ -76,14 +76,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 bumpers = game.descendants().whereType(); for (final bumper in bumpers) { expect( - bumper.firstChild(), + bumper.firstChild(), isNotNull, ); } diff --git a/test/game/components/sparky_scorch_test.dart b/test/game/components/sparky_scorch_test.dart index 5df250dd..0eeb9b45 100644 --- a/test/game/components/sparky_scorch_test.dart +++ b/test/game/components/sparky_scorch_test.dart @@ -77,13 +77,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, ); } From a7150279c8e7deb6c27e719fa48e2485c35fe149 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Thu, 5 May 2022 17:19:07 -0500 Subject: [PATCH 4/6] fix: render issue (#350) --- packages/pinball_components/lib/src/components/z_indexes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pinball_components/lib/src/components/z_indexes.dart b/packages/pinball_components/lib/src/components/z_indexes.dart index b59a9a4b..e98ac3cb 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 From 0f12e00affd32f042a2bd50077889da29261378d Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 5 May 2022 22:23:23 -0300 Subject: [PATCH 5/6] refactor: replacing google sfx (#354) --- packages/pinball_audio/assets/sfx/google.mp3 | Bin 23867 -> 145587 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/pinball_audio/assets/sfx/google.mp3 b/packages/pinball_audio/assets/sfx/google.mp3 index 34167d44cad1923bcd68a220a04a5b216ea49173..97659b02e962bd772c050282b5eff45c01b2c79f 100644 GIT binary patch literal 145587 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

C%I7xc_nQm^;URA5w;3r`k2YaGZa=vJ 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≥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Πd@G1A^Z{yTqF71c4!#x#f!+;wpnH{6yzZ&6r`4R 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

4HALO`umi(xL1DhgW{<72sIWje;@BWS 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 literal 23867 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 Date: Fri, 6 May 2022 05:31:29 +0100 Subject: [PATCH 6/6] chore: migrating to `flame_bloc` 1.4.0 (#357) * refactor: migrating to flame_bloc * test: passed all tests * test: updeted ControlledFlipperTest * test: tested FlameProvider * test: awaited loading * test: waited loading ScoreBehavior * refactor: removed unused files * refactor: removed FlameBlocTester * refactor: moved helper * test: fixed typo name * refactor: renamed file name to bumper_noise_behavior_test.dart * refactor: renamed to TestDebugPinballGame * refactor: removed uncessary KeybordHandler mixin * refactor: removed unecessary imports * test: removed testGameWidgets --- .../behaviors/ball_spawning_behavior.dart | 10 +- lib/game/behaviors/bumper_noise_behavior.dart | 6 +- .../behaviors/camera_focusing_behavior.dart | 13 +- lib/game/behaviors/scoring_behavior.dart | 10 +- .../android_spaceship_bonus_behavior.dart | 7 +- .../behaviors/ramp_bonus_behavior.dart | 4 +- .../behaviors/ramp_shot_behavior.dart | 5 +- lib/game/components/backbox/backbox.dart | 3 +- .../displays/initials_input_display.dart | 36 ++-- .../initials_submission_failure_display.dart | 3 +- .../initials_submission_success_display.dart | 3 +- .../backbox/displays/loading_display.dart | 7 +- lib/game/components/controlled_ball.dart | 5 +- lib/game/components/controlled_flipper.dart | 4 +- lib/game/components/controlled_plunger.dart | 12 +- .../behaviors/chrome_dino_bonus_behavior.dart | 5 +- .../drain/behaviors/draining_behavior.dart | 10 +- .../flutter_forest_bonus_behavior.dart | 16 +- .../components/game_bloc_status_listener.dart | 10 +- .../behaviors/google_word_bonus_behavior.dart | 9 +- .../behaviors/multiballs_behavior.dart | 5 +- .../behaviors/multipliers_behavior.dart | 5 +- lib/game/pinball_game.dart | 118 +++++----- lib/game/view/pinball_game_page.dart | 57 ++--- .../multiplier/multiplier_test.dart | 67 +++--- packages/pinball_flame/lib/pinball_flame.dart | 1 + .../lib/src/contact_behavior.dart | 1 + .../pinball_flame/lib/src/flame_provider.dart | 65 ++++++ .../test/src/flame_provider_test.dart | 103 +++++++++ pubspec.lock | 2 +- pubspec.yaml | 2 +- test/footer/footer_test.dart | 20 +- ...t.dart => bumper_noise_behavior_test.dart} | 26 ++- .../camera_focusing_behavior_test.dart | 41 ++-- .../game/behaviors/scoring_behavior_test.dart | 89 ++++---- .../android_acres/android_acres_test.dart | 86 ++++---- ...android_spaceship_bonus_behavior_test.dart | 87 ++++---- .../ball_spawning_behavior_test.dart | 37 +++- .../behaviors/ramp_bonus_behavior_test.dart | 111 +++++----- .../behaviors/ramp_shot_behavior_test.dart | 99 +++++---- .../game/components/backbox/backbox_test.dart | 77 ++++--- .../displays/initials_input_display_test.dart | 83 +++++--- ...tials_submission_failure_display_test.dart | 5 +- ...tials_submission_success_display_test.dart | 5 +- .../displays/loading_display_test.dart | 48 +++-- test/game/components/bottom_group_test.dart | 71 +++++-- .../game/components/controlled_ball_test.dart | 116 +++------- .../components/controlled_flipper_test.dart | 91 +++++--- .../components/controlled_plunger_test.dart | 83 ++++---- .../chrome_dino_bonus_behavior_test.dart | 59 +++-- .../dino_desert/dino_desert_test.dart | 62 ++++-- .../behaviors/draining_behavior_test.dart | 69 +++--- test/game/components/drain/drain_test.dart | 4 +- .../flutter_forest_bonus_behavior_test.dart | 70 +++--- .../flutter_forest/flutter_forest_test.dart | 83 +++++--- .../game_bloc_status_listener_test.dart | 186 ++++++++-------- .../google_word_bonus_behavior_test.dart | 72 ++++--- .../google_word/google_word_test.dart | 52 +++-- test/game/components/launcher_test.dart | 73 ++++--- .../behaviors/multiballs_behavior_test.dart | 92 +++++--- .../multiballs/multiballs_test.dart | 67 +++--- .../behaviors/multipliers_behavior_test.dart | 55 +++-- .../multipliers/multipliers_test.dart | 90 ++++---- test/game/components/sparky_scorch_test.dart | 37 ++-- test/game/pinball_game_test.dart | 201 ++++++------------ test/game/view/pinball_game_page_test.dart | 33 ++- test/helpers/builders.dart | 34 --- test/helpers/fakes.dart | 7 - test/helpers/forge2d.dart | 13 -- test/helpers/helpers.dart | 5 - test/helpers/test_games.dart | 122 ----------- test/helpers/text_span.dart | 17 -- 72 files changed, 1808 insertions(+), 1474 deletions(-) create mode 100644 packages/pinball_flame/lib/src/flame_provider.dart create mode 100644 packages/pinball_flame/test/src/flame_provider_test.dart rename test/game/behaviors/{bumper_noisy_behavior_test.dart => bumper_noise_behavior_test.dart} (70%) delete mode 100644 test/helpers/builders.dart delete mode 100644 test/helpers/fakes.dart delete mode 100644 test/helpers/forge2d.dart delete mode 100644 test/helpers/test_games.dart delete mode 100644 test/helpers/text_span.dart diff --git a/lib/game/behaviors/ball_spawning_behavior.dart b/lib/game/behaviors/ball_spawning_behavior.dart index 3602615b..c074fe52 100644 --- a/lib/game/behaviors/ball_spawning_behavior.dart +++ b/lib/game/behaviors/ball_spawning_behavior.dart @@ -3,11 +3,12 @@ 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 ParentIsA, BlocComponent { + with FlameBlocListenable, HasGameRef { @override bool listenWhen(GameState? previousState, GameState newState) { if (!newState.status.isPlaying) return false; @@ -20,9 +21,10 @@ class BallSpawningBehavior extends Component @override void onNewState(GameState state) { - final plunger = parent.descendants().whereType().single; - final canvas = parent.descendants().whereType().single; - final ball = ControlledBall.launch(characterTheme: parent.characterTheme) + 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, diff --git a/lib/game/behaviors/bumper_noise_behavior.dart b/lib/game/behaviors/bumper_noise_behavior.dart index e89ec23a..9c5da701 100644 --- a/lib/game/behaviors/bumper_noise_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 BumperNoiseBehavior 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/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/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 2356e0d8..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'; @@ -41,14 +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); /// 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 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 bcebbacc..c8cb90fb 100644 --- a/lib/game/components/controlled_plunger.dart +++ b/lib/game/components/controlled_plunger.dart @@ -24,13 +24,13 @@ class ControlledPlunger extends Plunger with Controls { } } -/// A behavior attached to the plunger when it launches the ball -/// which plays the related sound effects. -class PlungerNoiseBehavior extends Component with HasGameRef { +/// 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(); - gameRef.player.play(PinballAudio.launcher); + readProvider().play(PinballAudio.launcher); } @override @@ -44,7 +44,7 @@ class PlungerNoiseBehavior extends Component with HasGameRef { /// 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); @@ -62,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/behaviors/draining_behavior.dart b/lib/game/components/drain/behaviors/draining_behavior.dart index 36512efa..630d04af 100644 --- a/lib/game/components/drain/behaviors/draining_behavior.dart +++ b/lib/game/components/drain/behaviors/draining_behavior.dart @@ -1,12 +1,12 @@ 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 { +class DrainingBehavior extends ContactBehavior with HasGameRef { @override void beginContact(Object other, Contact contact) { super.beginContact(other, contact); @@ -15,7 +15,11 @@ class DrainingBehavior extends ContactBehavior other.removeFromParent(); final ballsLeft = gameRef.descendants().whereType().length; if (ballsLeft - 1 == 0) { - gameRef.read().add(const RoundLost()); + ancestors() + .whereType>() + .first + .bloc + .add(const RoundLost()); } } } diff --git a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart index c06e6f87..532e8eff 100644 --- a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -1,7 +1,9 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart'; /// Bonus obtained at the [FlutterForest]. /// @@ -9,7 +11,10 @@ import 'package:pinball_flame/pinball_flame.dart'; /// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest] /// is awarded, and the [DashNestBumper.main] releases a new [Ball]. class FlutterForestBonusBehavior extends Component - with ParentIsA, HasGameRef { + with + ParentIsA, + HasGameRef, + FlameBlocReader { @override void onMount() { super.onMount(); @@ -35,12 +40,11 @@ class FlutterForestBonusBehavior extends Component } if (signpost.bloc.isFullyProgressed()) { - gameRef - .read() - .add(const BonusActivated(GameBonus.dashNest)); + bloc.add(const BonusActivated(GameBonus.dashNest)); canvas.add( - ControlledBall.bonus(characterTheme: gameRef.characterTheme) - ..initialPosition = Vector2(29.5, -24.5), + ControlledBall.bonus( + characterTheme: readProvider(), + )..initialPosition = Vector2(29.5, -24.5), ); animatronic.playing = true; signpost.bloc.onProgressed(); 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/pinball_game.dart b/lib/game/pinball_game.dart index bbab932b..899ec45d 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -17,13 +17,20 @@ import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart'; class PinballGame extends PinballForge2DGame - with FlameBloc, HasKeyboardHandlerComponents, 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 = ''; } @@ -33,63 +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.25, 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.25, 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(), - BallSpawningBehavior(), - 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(); @@ -149,11 +161,13 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { required LeaderboardRepository leaderboardRepository, required AppLocalizations l10n, required PinballPlayer player, + required GameBloc gameBloc, }) : super( characterTheme: characterTheme, player: player, leaderboardRepository: leaderboardRepository, l10n: l10n, + gameBloc: gameBloc, ); Vector2? lineStart; 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_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_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.lock b/pubspec.lock index ffbd3899..96f9f2a6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -238,7 +238,7 @@ packages: name: flame_bloc url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.4.0" flame_forge2d: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b98c84a6..fcee1e6e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: firebase_auth: ^3.3.16 firebase_core: ^1.15.0 flame: ^1.1.1 - flame_bloc: ^1.2.0 + flame_bloc: ^1.4.0 flame_forge2d: git: url: https://github.com/flame-engine/flame/ diff --git a/test/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 70% rename from test/game/behaviors/bumper_noisy_behavior_test.dart rename to test/game/behaviors/bumper_noise_behavior_test.dart index e860a094..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 {} @@ -26,9 +36,7 @@ void main() { group('BumperNoiseBehavior', () {}); late PinballPlayer player; - final flameTester = FlameTester( - () => EmptyPinballTestGame(player: player), - ); + final flameTester = FlameTester(_TestGame.new); setUp(() { player = _MockPinballPlayer(); @@ -39,7 +47,7 @@ void main() { setUp: (game, _) async { 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 5de7576b..e88d1608 100644 --- a/test/game/components/android_acres/android_acres_test.dart +++ b/test/game/components/android_acres/android_acres_test.dart @@ -1,5 +1,7 @@ // 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_noise_behavior.dart'; @@ -7,50 +9,62 @@ 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), @@ -105,7 +119,7 @@ void main() { flameTester.test( '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( @@ -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 index 41c3e301..f41487cd 100644 --- a/test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart +++ b/test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart @@ -1,3 +1,6 @@ +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'; @@ -7,7 +10,30 @@ 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/test_games.dart'; +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 {} @@ -17,7 +43,7 @@ void main() { group( 'BallSpawningBehavior', () { - final flameTester = FlameTester(EmptyPinballTestGame.new); + final flameTester = FlameTester(_TestGame.new); test('can be instantiated', () { expect( @@ -30,8 +56,8 @@ void main() { 'loads', (game) async { final behavior = BallSpawningBehavior(); - await game.ensureAdd(behavior); - expect(game.contains(behavior), isTrue); + await game.pump([behavior]); + expect(game.descendants(), contains(behavior)); }, ); @@ -97,9 +123,8 @@ void main() { flameTester.test( 'onNewState adds a ball', (game) async { - await game.images.load(theme.Assets.images.dash.ball.keyName); final behavior = BallSpawningBehavior(); - await game.ensureAddAll([ + await game.pump([ behavior, ZCanvasComponent(), Plunger.test(compressionDistance: 10), 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 04ac0e0f..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( @@ -63,59 +51,21 @@ void main() { ); }); - 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); - }, - ); - }); + flameBlocTester.testGameWidget( + 'turboCharge adds TurboChargeActivated', + setUp: (game, tester) async { + await game.onLoad(); + + final controller = BallController(ball); + await ball.add(controller); + await game.pump(ball, gameBloc: gameBloc); + + await controller.turboCharge(); + }, + verify: (game, tester) async { + verify(() => gameBloc.add(const SparkyTurboChargeActivated())) + .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 f772b39a..25b1f739 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -4,6 +4,9 @@ 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'; @@ -11,26 +14,49 @@ 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 {} -class _MockPinballGame extends Mock implements PinballGame {} - void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballTestGame.new); + final flameTester = FlameTester(_TestGame.new); group('PlungerController', () { late GameBloc gameBloc; - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - ); + final flameBlocTester = FlameTester(_TestGame.new); late Plunger plunger; late PlungerController controller; @@ -54,13 +80,7 @@ void main() { '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); @@ -75,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, {}); @@ -96,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); @@ -123,7 +131,7 @@ void main() { ), ); - await game.ensureAdd(plunger); + await game.pump(plunger, gameBloc: gameBloc); controller.onKeyEvent(event, {}); }, verify: (game, tester) async { @@ -137,7 +145,7 @@ void main() { flameTester.test( 'adds the PlungerNoiseBehavior plunger is released', (game) async { - await game.ensureAdd(plunger); + await game.pump(plunger); plunger.body.setTransform(Vector2(0, 1), 0); plunger.release(); @@ -150,27 +158,22 @@ void main() { }); group('PlungerNoiseBehavior', () { - late PinballGame game; late PinballPlayer player; - late PlungerNoiseBehavior behavior; setUp(() { - game = _MockPinballGame(); player = _MockPinballPlayer(); - - when(() => game.player).thenReturn(player); - behavior = PlungerNoiseBehavior(); - behavior.mockGameRef(game); }); - test('plays the correct sound on load', () async { - await behavior.onLoad(); - + 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 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 index dbc62006..d25a7da6 100644 --- a/test/game/components/drain/behaviors/draining_behavior_test.dart +++ b/test/game/components/drain/behaviors/draining_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,25 @@ 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'; +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 {} @@ -40,32 +58,26 @@ void main() { ); group('beginContact', () { - final asset = theme.Assets.images.dash.ball.keyName; late GameBloc gameBloc; setUp(() { gameBloc = _MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - ); }); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - ); + final flameBlocTester = FlameTester(_TestGame.new); - flameBlocTester.testGameWidget( + flameBlocTester.test( 'adds RoundLost when no balls left', - setUp: (game, tester) async { - await game.images.load(asset); - + (game) async { final drain = Drain.test(); final behavior = DrainingBehavior(); final ball = Ball.test(); await drain.add(behavior); - await game.ensureAddAll([drain, ball]); + await game.pump( + drain, + gameBloc: gameBloc, + ); + await game.ensureAdd(ball); behavior.beginContact(ball, _MockContact()); await game.ready(); @@ -75,21 +87,19 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameBlocTester.test( "doesn't add RoundLost when there are balls left", - setUp: (game, tester) async { - await game.images.load(asset); - + (game) async { final drain = Drain.test(); final behavior = DrainingBehavior(); final ball1 = Ball.test(); final ball2 = Ball.test(); await drain.add(behavior); - await game.ensureAddAll([ + await game.pump( drain, - ball1, - ball2, - ]); + gameBloc: gameBloc, + ); + await game.ensureAddAll([ball1, ball2]); behavior.beginContact(ball1, _MockContact()); await game.ready(); @@ -99,15 +109,18 @@ void main() { }, ); - flameBlocTester.testGameWidget( + flameBlocTester.test( 'removes the Ball', - setUp: (game, tester) async { - await game.images.load(asset); + (game) async { final drain = Drain.test(); final behavior = DrainingBehavior(); final ball = Ball.test(); await drain.add(behavior); - await game.ensureAddAll([drain, ball]); + await game.pump( + drain, + gameBloc: gameBloc, + ); + await game.ensureAdd(ball); behavior.beginContact(ball, _MockContact()); await game.ready(); diff --git a/test/game/components/drain/drain_test.dart b/test/game/components/drain/drain_test.dart index 98c55ca1..b10c55e3 100644 --- a/test/game/components/drain/drain_test.dart +++ b/test/game/components/drain/drain_test.dart @@ -6,11 +6,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; -import '../../../helpers/helpers.dart'; - void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final flameTester = FlameTester(Forge2DGame.new); group('Drain', () { flameTester.test( 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 bc0e5ff4..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), @@ -78,8 +105,8 @@ void main() { flameTester.test( '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( 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 0eeb9b45..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 { diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index e2998f5d..b983b0b8 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -7,17 +7,58 @@ 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 _MockAppLocalizations extends Mock implements AppLocalizations {} + class _MockEventPosition extends Mock implements EventPosition {} class _MockTapDownDetails extends Mock implements TapDownDetails {} @@ -34,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; @@ -156,17 +95,10 @@ 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', () { - flameBlocTester.test( + flameTester.test( 'has only one BallSpawningBehavior', (game) async { await game.ready(); @@ -177,7 +109,7 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'has only one Drain', (game) async { await game.ready(); @@ -188,7 +120,7 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'has only one BottomGroup', (game) async { await game.ready(); @@ -199,7 +131,7 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'has only one Launcher', (game) async { await game.ready(); @@ -210,7 +142,7 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'has one FlutterForest', (game) async { await game.ready(); @@ -221,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), @@ -233,7 +164,7 @@ void main() { }, ); - flameBlocTester.test( + flameTester.test( 'one GoogleWord', (game) async { await game.ready(); @@ -244,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, @@ -252,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(); @@ -459,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(); @@ -494,7 +425,7 @@ void main() { }, ); - debugModeFlameTester.test( + flameTester.test( 'set lineStart on pan start', (game) async { final startPosition = Vector2.all(10); @@ -514,7 +445,7 @@ void main() { }, ); - debugModeFlameTester.test( + flameTester.test( 'set lineEnd on pan update', (game) async { final endPosition = Vector2.all(10); @@ -534,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; -}