diff --git a/lib/game/behaviors/ball_spawning_behavior.dart b/lib/game/behaviors/ball_spawning_behavior.dart index 75656d8f..8995c16b 100644 --- a/lib/game/behaviors/ball_spawning_behavior.dart +++ b/lib/game/behaviors/ball_spawning_behavior.dart @@ -1,9 +1,9 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.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]. @@ -23,7 +23,9 @@ class BallSpawningBehavior extends Component void onNewState(GameState state) { final plunger = gameRef.descendants().whereType().single; final canvas = gameRef.descendants().whereType().single; - final characterTheme = readProvider(); + final characterTheme = readBloc() + .state + .characterTheme; final ball = Ball(assetPath: characterTheme.ball.keyName) ..initialPosition = Vector2( plunger.body.position.x, diff --git a/lib/game/behaviors/ball_theming_behavior.dart b/lib/game/behaviors/ball_theming_behavior.dart new file mode 100644 index 00000000..5e12a720 --- /dev/null +++ b/lib/game/behaviors/ball_theming_behavior.dart @@ -0,0 +1,20 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// Updates the launch [Ball] to reflect character selections. +class BallThemingBehavior extends Component + with + FlameBlocListenable, + HasGameRef { + @override + void onNewState(CharacterThemeState state) { + gameRef + .descendants() + .whereType() + .single + .bloc + .onThemeChanged(state.characterTheme); + } +} diff --git a/lib/game/behaviors/behaviors.dart b/lib/game/behaviors/behaviors.dart index c7ad6880..301bc61e 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -1,4 +1,5 @@ export 'ball_spawning_behavior.dart'; +export 'ball_theming_behavior.dart'; export 'bonus_noise_behavior.dart'; export 'bumper_noise_behavior.dart'; export 'camera_focusing_behavior.dart'; 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 55902eb7..c94cda60 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,9 +1,9 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.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]. /// @@ -41,11 +41,12 @@ class FlutterForestBonusBehavior extends Component if (signpost.bloc.isFullyProgressed()) { bloc.add(const BonusActivated(GameBonus.dashNest)); - final characterTheme = readProvider(); + final characterTheme = + readBloc() + .state + .characterTheme; canvas.add( - Ball( - assetPath: characterTheme.ball.keyName, - ) + Ball(assetPath: characterTheme.ball.keyName) ..initialPosition = Vector2(29.2, -24.5) ..zIndex = ZIndexes.ballOnBoard, ); diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index 6e11f3d6..81da96d5 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -1,9 +1,9 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.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 @@ -26,7 +26,9 @@ class GameBlocStatusListener extends Component readProvider().play(PinballAudio.gameOverVoiceOver); gameRef.descendants().whereType().first.requestInitials( score: state.displayScore, - character: readProvider(), + character: readBloc() + .state + .characterTheme, ); break; } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 6818f566..6f1aa630 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -11,15 +11,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import 'package:pinball_theme/pinball_theme.dart'; class PinballGame extends PinballForge2DGame with HasKeyboardHandlerComponents, MultiTouchTapDetector { PinballGame({ - required CharacterTheme characterTheme, + required CharacterThemeCubit characterThemeBloc, required this.leaderboardRepository, required GameBloc gameBloc, required AppLocalizations l10n, @@ -27,7 +27,7 @@ class PinballGame extends PinballForge2DGame }) : focusNode = FocusNode(), _gameBloc = gameBloc, _player = player, - _characterTheme = characterTheme, + _characterThemeBloc = characterThemeBloc, _l10n = l10n, super( gravity: Vector2(0, 30), @@ -43,7 +43,7 @@ class PinballGame extends PinballForge2DGame final FocusNode focusNode; - final CharacterTheme _characterTheme; + final CharacterThemeCubit _characterThemeBloc; final PinballPlayer _player; @@ -56,13 +56,19 @@ class PinballGame extends PinballForge2DGame @override Future onLoad() async { await add( - FlameBlocProvider.value( - value: _gameBloc, + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: _gameBloc, + ), + FlameBlocProvider.value( + value: _characterThemeBloc, + ), + ], children: [ MultiFlameProvider( providers: [ FlameProvider.value(_player), - FlameProvider.value(_characterTheme), FlameProvider.value(leaderboardRepository), FlameProvider.value(_l10n), ], @@ -70,6 +76,7 @@ class PinballGame extends PinballForge2DGame BonusNoiseBehavior(), GameBlocStatusListener(), BallSpawningBehavior(), + BallThemingBehavior(), CameraFocusingBehavior(), CanvasComponent( onSpritePainted: (paint) { @@ -161,13 +168,13 @@ class PinballGame extends PinballForge2DGame class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { DebugPinballGame({ - required CharacterTheme characterTheme, + required CharacterThemeCubit characterThemeBloc, required LeaderboardRepository leaderboardRepository, required AppLocalizations l10n, required PinballPlayer player, required GameBloc gameBloc, }) : super( - characterTheme: characterTheme, + characterThemeBloc: characterThemeBloc, player: player, leaderboardRepository: leaderboardRepository, l10n: l10n, diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 1005c728..354d61ed 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -31,21 +31,20 @@ class PinballGamePage extends StatelessWidget { @override Widget build(BuildContext context) { - final characterTheme = - context.read().state.characterTheme; + final characterThemeBloc = context.read(); final player = context.read(); final leaderboardRepository = context.read(); final gameBloc = context.read(); final game = isDebugMode ? DebugPinballGame( - characterTheme: characterTheme, + characterThemeBloc: characterThemeBloc, player: player, leaderboardRepository: leaderboardRepository, l10n: context.l10n, gameBloc: gameBloc, ) : PinballGame( - characterTheme: characterTheme, + characterThemeBloc: characterThemeBloc, player: player, leaderboardRepository: leaderboardRepository, l10n: context.l10n, diff --git a/packages/pinball_components/assets/images/flipper/left.png b/packages/pinball_components/assets/images/flipper/left.png index 3aefa225..190b5bb0 100644 Binary files a/packages/pinball_components/assets/images/flipper/left.png and b/packages/pinball_components/assets/images/flipper/left.png differ diff --git a/packages/pinball_components/assets/images/flipper/right.png b/packages/pinball_components/assets/images/flipper/right.png index 3627c86c..6bc46580 100644 Binary files a/packages/pinball_components/assets/images/flipper/right.png and b/packages/pinball_components/assets/images/flipper/right.png differ diff --git a/packages/pinball_components/lib/src/components/ball/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart index e8cea997..49e4f0e0 100644 --- a/packages/pinball_components/lib/src/components/ball/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -8,27 +9,27 @@ import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; export 'behaviors/behaviors.dart'; +export 'cubit/ball_cubit.dart'; /// {@template ball} /// A solid, [BodyType.dynamic] sphere that rolls and bounces around. /// {@endtemplate} class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// {@macro ball} - Ball({ - String? assetPath, - }) : super( + Ball({String? assetPath}) : this._(bloc: BallCubit(), assetPath: assetPath); + + Ball._({required this.bloc, String? assetPath}) + : super( renderBody: false, children: [ - _BallSpriteComponent(assetPath: assetPath), + FlameBlocProvider.value( + value: bloc, + children: [BallSpriteComponent(assetPath: assetPath)], + ), BallScalingBehavior(), BallGravitatingBehavior(), ], ) { - // TODO(ruimiguel): while developing Ball can be launched by clicking mouse, - // and default layer is Layer.all. But on final game Ball will be always be - // be launched from Plunger and LauncherRamp will modify it to Layer.board. - // We need to see what happens if Ball appears from other place like nest - // bumper, it will need to explicit change layer to Layer.board then. layer = Layer.board; } @@ -36,11 +37,22 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { /// /// This can be used for testing [Ball]'s behaviors in isolation. @visibleForTesting - Ball.test() - : super( - children: [_BallSpriteComponent()], + Ball.test({ + BallCubit? bloc, + String? assetPath, + }) : bloc = bloc ?? BallCubit(), + super( + children: [ + FlameBlocProvider.value( + value: bloc ?? BallCubit(), + children: [BallSpriteComponent(assetPath: assetPath)], + ) + ], ); + /// Bloc to update the ball sprite when a new character is selected. + final BallCubit bloc; + /// The size of the [Ball]. static final Vector2 size = Vector2.all(4.13); @@ -76,21 +88,32 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { } } -class _BallSpriteComponent extends SpriteComponent with HasGameRef { - _BallSpriteComponent({ - this.assetPath, - }) : super( - anchor: Anchor.center, - ); +/// {@template ball_sprite_component} +/// Visual representation of the [Ball]. +/// {@endtemplate} +@visibleForTesting +class BallSpriteComponent extends SpriteComponent + with HasGameRef, FlameBlocListenable { + /// {@macro ball_sprite_component} + BallSpriteComponent({required String? assetPath}) + : _assetPath = assetPath, + super(anchor: Anchor.center); + + final String? _assetPath; - final String? assetPath; + @override + void onNewState(BallState state) { + sprite = Sprite( + gameRef.images.fromCache(state.characterTheme.ball.keyName), + ); + } @override Future onLoad() async { await super.onLoad(); final sprite = Sprite( gameRef.images - .fromCache(assetPath ?? theme.Assets.images.dash.ball.keyName), + .fromCache(_assetPath ?? theme.Assets.images.dash.ball.keyName), ); this.sprite = sprite; size = sprite.originalSize / 12.5; diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart index 7fc06fb1..e1e7c405 100644 --- a/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart @@ -16,9 +16,12 @@ class BallScalingBehavior extends Component with ParentIsA { parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor; - parent.firstChild()!.scale.setValues( - scaleFactor, - scaleFactor, - ); + final ballSprite = parent.descendants().whereType(); + if (ballSprite.isNotEmpty) { + ballSprite.single.scale.setValues( + scaleFactor, + scaleFactor, + ); + } } } diff --git a/packages/pinball_components/lib/src/components/ball/cubit/ball_cubit.dart b/packages/pinball_components/lib/src/components/ball/cubit/ball_cubit.dart new file mode 100644 index 00000000..e3054e33 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/cubit/ball_cubit.dart @@ -0,0 +1,15 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +part 'ball_state.dart'; + +class BallCubit extends Cubit { + BallCubit() : super(const BallState.initial()); + + void onThemeChanged(CharacterTheme characterTheme) { + emit(BallState(characterTheme: characterTheme)); + } +} diff --git a/packages/pinball_components/lib/src/components/ball/cubit/ball_state.dart b/packages/pinball_components/lib/src/components/ball/cubit/ball_state.dart new file mode 100644 index 00000000..17a7aa9d --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/cubit/ball_state.dart @@ -0,0 +1,14 @@ +// ignore_for_file: public_member_api_docs + +part of 'ball_cubit.dart'; + +class BallState extends Equatable { + const BallState({required this.characterTheme}); + + const BallState.initial() : this(characterTheme: const DashTheme()); + + final CharacterTheme characterTheme; + + @override + List get props => [characterTheme]; +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart index eff84ff4..a8435ec0 100644 --- a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart @@ -14,7 +14,7 @@ class ChromeDinoChompingBehavior extends ContactBehavior { super.beginContact(other, contact); if (other is! Ball) return; - other.firstChild()!.setOpacity(0); + other.descendants().whereType().single.setOpacity(0); parent.bloc.onChomp(other); } } diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart index 78a8b9d5..876dd4d6 100644 --- a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart @@ -30,7 +30,7 @@ class ChromeDinoSpittingBehavior extends Component void _spit() { parent.bloc.state.ball! - ..firstChild()!.setOpacity(1) + ..descendants().whereType().single.setOpacity(1) ..body.linearVelocity = Vector2(-50, 0); parent.bloc.onSpit(); } diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 3fffaa88..758ebe37 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: bloc: ^8.0.3 flame: ^1.1.1 + flame_bloc: ^1.4.0 flame_forge2d: git: url: https://github.com/flame-engine/flame diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index 0357656d..b5ac88b7 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -106,6 +106,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" + flame_bloc: + dependency: transitive + description: + name: flame_bloc + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" flame_forge2d: dependency: "direct main" description: @@ -120,6 +127,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: transitive + description: + name: flutter_bloc + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.1" flutter_colorpicker: dependency: transitive description: @@ -214,6 +228,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" ordered_set: dependency: transitive description: @@ -298,6 +319,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + provider: + dependency: transitive + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.2" shared_preferences: dependency: transitive description: diff --git a/packages/pinball_components/test/src/components/ball/ball_test.dart b/packages/pinball_components/test/src/components/ball/ball_test.dart index 9195e0b2..43454342 100644 --- a/packages/pinball_components/test/src/components/ball/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball/ball_test.dart @@ -1,5 +1,6 @@ // 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'; @@ -39,6 +40,41 @@ void main() { }, ); + flameTester.test( + 'has only one SpriteComponent', + (game) async { + final ball = Ball(); + await game.ready(); + await game.ensureAdd(ball); + + expect( + ball.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'BallSpriteComponent changes sprite onNewState', + (game) async { + final ball = Ball(); + await game.ready(); + await game.ensureAdd(ball); + + final ballSprite = + ball.descendants().whereType().single; + final originalSprite = ballSprite.sprite; + + ballSprite.onNewState( + const BallState(characterTheme: theme.DinoTheme()), + ); + await game.ready(); + + final newSprite = ballSprite.sprite; + expect(newSprite != originalSprite, isTrue); + }, + ); + group('adds', () { flameTester.test('a BallScalingBehavior', (game) async { final ball = Ball(); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart index bd0cca49..c4c66d4a 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -62,8 +62,8 @@ void main() { await game.ensureAddAll([ball1, ball2]); game.update(1); - final sprite1 = ball1.firstChild()!; - final sprite2 = ball2.firstChild()!; + final sprite1 = ball1.descendants().whereType().single; + final sprite2 = ball2.descendants().whereType().single; expect( sprite1.scale.x, greaterThan(sprite2.scale.x), diff --git a/packages/pinball_components/test/src/components/ball/cubit/ball_cubit_test.dart b/packages/pinball_components/test/src/components/ball/cubit/ball_cubit_test.dart new file mode 100644 index 00000000..c5a03213 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/cubit/ball_cubit_test.dart @@ -0,0 +1,18 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +void main() { + group( + 'BallCubit', + () { + blocTest( + 'onThemeChanged emits new theme', + build: BallCubit.new, + act: (bloc) => bloc.onThemeChanged(const DinoTheme()), + expect: () => [const BallState(characterTheme: DinoTheme())], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/ball/cubit/ball_state_test.dart b/packages/pinball_components/test/src/components/ball/cubit/ball_state_test.dart new file mode 100644 index 00000000..1163ba1e --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/cubit/ball_state_test.dart @@ -0,0 +1,22 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +void main() { + group('BallState', () { + test('supports value equality', () { + expect( + BallState(characterTheme: DashTheme()), + equals(const BallState(characterTheme: DashTheme())), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect(const BallState(characterTheme: DashTheme()), isNotNull); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart index dfc33967..f5fd2b42 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart @@ -61,7 +61,10 @@ void main() { behavior.beginContact(ball, contact); - expect(ball.firstChild()!.getOpacity(), isZero); + expect( + ball.descendants().whereType().single.getOpacity(), + isZero, + ); verify(() => bloc.onChomp(ball)).called(1); }, diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart index 8c2cbe57..0748040e 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart @@ -66,7 +66,14 @@ void main() { .timer .onTick!(); - expect(ball.firstChild()!.getOpacity(), equals(1)); + expect( + ball + .descendants() + .whereType() + .single + .getOpacity(), + equals(1), + ); expect(ball.body.linearVelocity, equals(Vector2(-50, 0))); }, ); diff --git a/packages/pinball_components/test/src/components/golden/flipper.png b/packages/pinball_components/test/src/components/golden/flipper.png index 07fe81ed..8507618d 100644 Binary files a/packages/pinball_components/test/src/components/golden/flipper.png and b/packages/pinball_components/test/src/components/golden/flipper.png differ diff --git a/packages/pinball_theme/assets/images/android/ball.png b/packages/pinball_theme/assets/images/android/ball.png index b5cfbc3f..ca2eebe3 100644 Binary files a/packages/pinball_theme/assets/images/android/ball.png and b/packages/pinball_theme/assets/images/android/ball.png differ diff --git a/packages/pinball_theme/assets/images/dash/ball.png b/packages/pinball_theme/assets/images/dash/ball.png index fa754cbc..69e8dffa 100644 Binary files a/packages/pinball_theme/assets/images/dash/ball.png and b/packages/pinball_theme/assets/images/dash/ball.png differ diff --git a/packages/pinball_theme/assets/images/dino/ball.png b/packages/pinball_theme/assets/images/dino/ball.png index 02b99c43..1a2102d8 100644 Binary files a/packages/pinball_theme/assets/images/dino/ball.png and b/packages/pinball_theme/assets/images/dino/ball.png differ diff --git a/packages/pinball_theme/assets/images/sparky/ball.png b/packages/pinball_theme/assets/images/sparky/ball.png index 95e5a10b..fe3f4e82 100644 Binary files a/packages/pinball_theme/assets/images/sparky/ball.png and b/packages/pinball_theme/assets/images/sparky/ball.png differ diff --git a/test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart b/test/game/behaviors/ball_spawning_behavior_test.dart similarity index 90% rename from test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart rename to test/game/behaviors/ball_spawning_behavior_test.dart index f41487cd..d723c65e 100644 --- a/test/game/components/android_acres/behaviors/ball_spawning_behavior_test.dart +++ b/test/game/behaviors/ball_spawning_behavior_test.dart @@ -6,6 +6,7 @@ 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/select_character/select_character.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; @@ -18,18 +19,20 @@ class _TestGame extends Forge2DGame { } Future pump( - Iterable children, { + List children, { GameBloc? gameBloc, }) async { await ensureAdd( - FlameBlocProvider.value( - value: gameBloc ?? GameBloc(), - children: [ - FlameProvider.value( - const theme.DashTheme(), - children: children, + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc ?? GameBloc(), + ), + FlameBlocProvider.value( + value: CharacterThemeCubit(), ), ], + children: children, ), ); } diff --git a/test/game/behaviors/ball_theming_behavior_test.dart b/test/game/behaviors/ball_theming_behavior_test.dart new file mode 100644 index 00000000..0c1e0b36 --- /dev/null +++ b/test/game/behaviors/ball_theming_behavior_test.dart @@ -0,0 +1,93 @@ +// 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'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + theme.Assets.images.dash.ball.keyName, + theme.Assets.images.dino.ball.keyName, + ]); + } + + Future pump( + List children, { + CharacterThemeCubit? characterThemeBloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: characterThemeBloc ?? CharacterThemeCubit(), + children: children, + ), + ); + } +} + +class _MockBallCubit extends Mock implements BallCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'BallThemingBehavior', + () { + final flameTester = FlameTester(_TestGame.new); + + test('can be instantiated', () { + expect( + BallThemingBehavior(), + isA(), + ); + }); + + flameTester.test( + 'loads', + (game) async { + final behavior = BallThemingBehavior(); + await game.pump([behavior]); + expect(game.descendants(), contains(behavior)); + }, + ); + + flameTester.test( + 'onNewState calls onThemeChanged on the ball bloc', + (game) async { + final ballBloc = _MockBallCubit(); + whenListen( + ballBloc, + const Stream.empty(), + initialState: const BallState.initial(), + ); + final ball = Ball.test(bloc: ballBloc); + final behavior = BallThemingBehavior(); + await game.pump([ + ball, + behavior, + ZCanvasComponent(), + Plunger.test(compressionDistance: 10), + ]); + + const dinoThemeState = CharacterThemeState(theme.DinoTheme()); + behavior.onNewState(dinoThemeState); + await game.ready(); + + verify(() => ballBloc.onThemeChanged(dinoThemeState.characterTheme)) + .called(1); + }, + ); + }, + ); +} diff --git a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart index 3dcd870b..0d058c70 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 @@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; @@ -26,16 +27,16 @@ class _TestGame extends Forge2DGame { required GameBloc gameBloc, }) async { await ensureAdd( - FlameBlocProvider.value( - value: gameBloc, + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value(value: gameBloc), + FlameBlocProvider.value( + value: CharacterThemeCubit(), + ), + ], children: [ - FlameProvider.value( - const theme.DashTheme(), - children: [ - ZCanvasComponent( - children: [child], - ), - ], + ZCanvasComponent( + children: [child], ), ], ), diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart index 7118aa8d..767ddefa 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -8,10 +8,10 @@ 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/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import 'package:pinball_theme/pinball_theme.dart' as theme; class _TestGame extends Forge2DGame { @override @@ -25,18 +25,18 @@ class _TestGame extends Forge2DGame { PinballPlayer? pinballPlayer, }) async { return ensureAdd( - FlameBlocProvider.value( - value: GameBloc(), + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: GameBloc(), + ), + FlameBlocProvider.value( + value: CharacterThemeCubit(), + ), + ], children: [ - MultiFlameProvider( - providers: [ - FlameProvider.value( - pinballPlayer ?? _MockPinballPlayer(), - ), - FlameProvider.value( - const theme.DashTheme(), - ), - ], + FlameProvider.value( + pinballPlayer ?? _MockPinballPlayer(), children: children, ), ], diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 4130ca77..3b034c8f 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -13,14 +13,14 @@ 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/select_character/select_character.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; class _TestPinballGame extends PinballGame { _TestPinballGame() : super( - characterTheme: const theme.DashTheme(), + characterThemeBloc: CharacterThemeCubit(), leaderboardRepository: _MockLeaderboardRepository(), gameBloc: GameBloc(), l10n: _MockAppLocalizations(), @@ -39,7 +39,7 @@ class _TestPinballGame extends PinballGame { class _TestDebugPinballGame extends DebugPinballGame { _TestDebugPinballGame() : super( - characterTheme: const theme.DashTheme(), + characterThemeBloc: CharacterThemeCubit(), leaderboardRepository: _MockLeaderboardRepository(), gameBloc: GameBloc(), l10n: _MockAppLocalizations(), @@ -109,6 +109,17 @@ void main() { }, ); + flameTester.test( + 'has only one BallThemingBehavior', + (game) async { + await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameTester.test( 'has only one Drain', (game) async { diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index a0ed4c7e..0e23e54d 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -16,14 +16,13 @@ import 'package:pinball/more_information/more_information.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(), + characterThemeBloc: CharacterThemeCubit(), leaderboardRepository: _MockLeaderboardRepository(), gameBloc: GameBloc(), l10n: _MockAppLocalizations(),