diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index c8a41a4b..82d8fae0 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -10,13 +10,21 @@ jobs: runs-on: ubuntu-latest name: Deploy Development steps: - - uses: actions/checkout@v2 - - uses: subosito/flutter-action@v2 + - name: Checkout Repo + uses: actions/checkout@v2 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 with: channel: stable - - run: flutter packages get - - run: flutter build web --target lib/main_development.dart --web-renderer canvaskit --release - - uses: FirebaseExtended/action-hosting-deploy@v0 + + - name: Build Flutter App + run: | + flutter packages get + flutter build web --target lib/main_development.dart --web-renderer canvaskit --release + + - name: Deploy to Firebase + uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}" diff --git a/assets/images/components/background.png b/assets/images/components/background.png deleted file mode 100644 index 77a8542c..00000000 Binary files a/assets/images/components/background.png and /dev/null differ diff --git a/lib/game/components/android_acres.dart b/lib/game/components/android_acres/android_acres.dart similarity index 69% rename from lib/game/components/android_acres.dart rename to lib/game/components/android_acres/android_acres.dart index e7330c1f..3d1a8154 100644 --- a/lib/game/components/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -1,6 +1,8 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -16,6 +18,11 @@ class AndroidAcres extends Component { SpaceshipRamp(), SpaceshipRail(), AndroidSpaceship(position: Vector2(-26.5, -28.5)), + AndroidAnimatronic( + children: [ + ScoringBehavior(points: Points.twoHundredThousand), + ], + )..initialPosition = Vector2(-26, -28.25), AndroidBumper.a( children: [ ScoringBehavior(points: Points.twentyThousand), @@ -31,6 +38,13 @@ class AndroidAcres extends Component { ScoringBehavior(points: Points.twentyThousand), ], )..initialPosition = Vector2(-20.5, -13.8), + AndroidSpaceshipBonusBehavior(), ], ); + + /// Creates [AndroidAcres] without any children. + /// + /// This can be used for testing [AndroidAcres]'s behaviors in isolation. + @visibleForTesting + AndroidAcres.test(); } 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 new file mode 100644 index 00000000..833ac8e4 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart @@ -0,0 +1,27 @@ +import 'package:flame/components.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 { + @override + void onMount() { + super.onMount(); + final androidSpaceship = parent.firstChild()!; + + // TODO(alestiago): Refactor subscription management once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1538 + androidSpaceship.bloc.stream.listen((state) { + final listenWhen = state == AndroidSpaceshipState.withBonus; + if (!listenWhen) return; + + gameRef + .read() + .add(const BonusActivated(GameBonus.androidSpaceship)); + androidSpaceship.bloc.onBonusAwarded(); + }); + } +} diff --git a/lib/game/components/android_acres/behaviors/behaviors.dart b/lib/game/components/android_acres/behaviors/behaviors.dart new file mode 100644 index 00000000..e4ac5981 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'android_spaceship_bonus_behavior.dart'; diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 5af4efc0..b0b81239 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,15 +1,16 @@ -export 'android_acres.dart'; +export 'android_acres/android_acres.dart'; export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; export 'controlled_plunger.dart'; -export 'dino_desert.dart'; +export 'dino_desert/dino_desert.dart'; export 'drain.dart'; export 'flutter_forest/flutter_forest.dart'; export 'game_flow_controller.dart'; export 'google_word/google_word.dart'; export 'launcher.dart'; +export 'multiballs/multiballs.dart'; export 'multipliers/multipliers.dart'; export 'scoring_behavior.dart'; export 'sparky_scorch.dart'; diff --git a/lib/game/components/dino_desert/behaviors/behaviors.dart b/lib/game/components/dino_desert/behaviors/behaviors.dart new file mode 100644 index 00000000..fe802c88 --- /dev/null +++ b/lib/game/components/dino_desert/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'chrome_dino_bonus_behavior.dart'; 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 new file mode 100644 index 00000000..e4d69f9c --- /dev/null +++ b/lib/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior.dart @@ -0,0 +1,24 @@ +import 'package:flame/components.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 { + @override + void onMount() { + super.onMount(); + final chromeDino = parent.firstChild()!; + + // TODO(alestiago): Refactor subscription management once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1538 + chromeDino.bloc.stream.listen((state) { + final listenWhen = state.status == ChromeDinoStatus.chomping; + if (!listenWhen) return; + + gameRef.read().add(const BonusActivated(GameBonus.dinoChomp)); + }); + } +} diff --git a/lib/game/components/dino_desert.dart b/lib/game/components/dino_desert/dino_desert.dart similarity index 73% rename from lib/game/components/dino_desert.dart rename to lib/game/components/dino_desert/dino_desert.dart index b3ae4ab9..4d8cd7b6 100644 --- a/lib/game/components/dino_desert.dart +++ b/lib/game/components/dino_desert/dino_desert.dart @@ -1,11 +1,13 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template dino_desert} -/// Area located next to the [Launcher] containing the [ChromeDino] and -/// [DinoWalls]. +/// Area located next to the [Launcher] containing the [ChromeDino], +/// [DinoWalls], and the [Slingshots]. /// {@endtemplate} class DinoDesert extends Component { /// {@macro dino_desert} @@ -21,8 +23,15 @@ class DinoDesert extends Component { _BarrierBehindDino(), DinoWalls(), Slingshots(), + ChromeDinoBonusBehavior(), ], ); + + /// Creates [DinoDesert] without any children. + /// + /// This can be used for testing [DinoDesert]'s behaviors in isolation. + @visibleForTesting + DinoDesert.test(); } class _BarrierBehindDino extends BodyComponent { 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 b884410e..86857ee4 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 @@ -3,7 +3,10 @@ import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] +/// Bonus obtained at the [FlutterForest]. +/// +/// When all [DashNestBumper]s are hit at least once three times, the [Signpost] +/// 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 { @@ -12,28 +15,36 @@ class FlutterForestBonusBehavior extends Component super.onMount(); final bumpers = parent.children.whereType(); + final signpost = parent.firstChild()!; + final animatronic = parent.firstChild()!; + final canvas = gameRef.firstChild()!; + for (final bumper in bumpers) { // TODO(alestiago): Refactor subscription management once the following is // merged: // https://github.com/flame-engine/flame/pull/1538 bumper.bloc.stream.listen((state) { - final achievedBonus = bumpers.every( + final activatedAllBumpers = bumpers.every( (bumper) => bumper.bloc.state == DashNestBumperState.active, ); - if (achievedBonus) { - gameRef - .read() - .add(const BonusActivated(GameBonus.dashNest)); - gameRef.firstChild()!.add( - ControlledBall.bonus(characterTheme: gameRef.characterTheme) - ..initialPosition = Vector2(17.2, -52.7), - ); - parent.firstChild()?.playing = true; - + if (activatedAllBumpers) { + signpost.bloc.onProgressed(); for (final bumper in bumpers) { bumper.bloc.onReset(); } + + if (signpost.bloc.isFullyProgressed()) { + gameRef + .read() + .add(const BonusActivated(GameBonus.dashNest)); + canvas.add( + ControlledBall.bonus(characterTheme: gameRef.characterTheme) + ..initialPosition = Vector2(17.2, -52.7), + ); + animatronic.playing = true; + signpost.bloc.onProgressed(); + } } }); } diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index 48dd5518..edc65329 100644 --- a/lib/game/components/game_flow_controller.dart +++ b/lib/game/components/game_flow_controller.dart @@ -39,6 +39,7 @@ class GameFlowController extends ComponentController /// Puts the game on a playing state void start() { + component.audio.backgroundMusic(); component.firstChild()?.waitingMode(); component.firstChild()?.focusOnGame(); component.overlays.remove(PinballGame.playButtonOverlay); diff --git a/lib/game/components/multiballs/behaviors/behaviors.dart b/lib/game/components/multiballs/behaviors/behaviors.dart new file mode 100644 index 00000000..921063dc --- /dev/null +++ b/lib/game/components/multiballs/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multiballs_behavior.dart'; diff --git a/lib/game/components/multiballs/behaviors/multiballs_behavior.dart b/lib/game/components/multiballs/behaviors/multiballs_behavior.dart new file mode 100644 index 00000000..8b323ff4 --- /dev/null +++ b/lib/game/components/multiballs/behaviors/multiballs_behavior.dart @@ -0,0 +1,28 @@ +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'; + +/// Toggle each [Multiball] when there is a bonus ball. +class MultiballsBehavior extends Component + with + HasGameRef, + ParentIsA, + BlocComponent { + @override + bool listenWhen(GameState? previousState, GameState newState) { + final hasChanged = previousState?.bonusHistory != newState.bonusHistory; + final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty && + newState.bonusHistory.last == GameBonus.dashNest; + + return hasChanged && lastBonusIsMultiball; + } + + @override + void onNewState(GameState state) { + parent.children.whereType().forEach((multiball) { + multiball.bloc.onAnimate(); + }); + } +} diff --git a/lib/game/components/multiballs/multiballs.dart b/lib/game/components/multiballs/multiballs.dart new file mode 100644 index 00000000..04f6525a --- /dev/null +++ b/lib/game/components/multiballs/multiballs.dart @@ -0,0 +1,30 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template multiballs_component} +/// A [SpriteGroupComponent] for the multiball over the board. +/// {@endtemplate} +class Multiballs extends Component with ZIndex { + /// {@macro multiballs_component} + Multiballs() + : super( + children: [ + Multiball.a(), + Multiball.b(), + Multiball.c(), + Multiball.d(), + MultiballsBehavior(), + ], + ) { + zIndex = ZIndexes.decal; + } + + /// Creates a [Multiballs] without any children. + /// + /// This can be used for testing [Multiballs]'s behaviors in isolation. + @visibleForTesting + Multiballs.test(); +} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index d26c452b..abf5c9a5 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -1,5 +1,4 @@ import 'package:pinball/game/game.dart'; -import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' as components; import 'package:pinball_theme/pinball_theme.dart' hide Assets; @@ -113,6 +112,8 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.googleWord.letter6.lit.keyName), images.load(components.Assets.images.googleWord.letter6.dimmed.keyName), images.load(components.Assets.images.backboard.display.keyName), + images.load(components.Assets.images.multiball.lit.keyName), + images.load(components.Assets.images.multiball.dimmed.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.multiplier.x2.dimmed.keyName), images.load(components.Assets.images.multiplier.x3.lit.keyName), @@ -135,7 +136,6 @@ extension PinballGameAssetsX on PinballGame { images.load(dashTheme.ball.keyName), images.load(dinoTheme.ball.keyName), images.load(sparkyTheme.ball.keyName), - images.load(Assets.images.components.background.path), ]; } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index cd5a1c7f..f9018ee5 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -53,6 +53,7 @@ class PinballGame extends Forge2DGame final decals = [ GoogleWord(position: Vector2(-4.25, 1.8)), Multipliers(), + Multiballs(), ]; final characterAreas = [ AndroidAcres(), @@ -87,7 +88,7 @@ class PinballGame extends Forge2DGame // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. if (bounds.contains(info.eventPosition.game.toOffset())) { - descendants().whereType().single.pull(); + descendants().whereType().single.pullFor(2); } else { final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; @@ -103,21 +104,12 @@ class PinballGame extends Forge2DGame @override void onTapUp(TapUpInfo info) { - final rocket = descendants().whereType().first; - final bounds = rocket.topLeftPosition & rocket.size; - - if (bounds.contains(info.eventPosition.game.toOffset())) { - descendants().whereType().single.release(); - } else { - _moveFlippersDown(); - } + _moveFlippersDown(); super.onTapUp(info); } @override void onTapCancel() { - descendants().whereType().single.release(); - _moveFlippersDown(); super.onTapCancel(); } diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index c855f776..1d4a10fb 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:pinball/game/pinball_game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_ui/pinball_ui.dart'; /// {@template play_button_overlay} /// [Widget] that renders the button responsible to starting the game @@ -20,14 +21,12 @@ class PlayButtonOverlay extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; - return Center( - child: ElevatedButton( - onPressed: () async { - _game.gameFlowController.start(); - await showCharacterSelectionDialog(context); - }, - child: Text(l10n.play), - ), + return PinballButton( + text: l10n.play, + onTap: () async { + _game.gameFlowController.start(); + await showCharacterSelectionDialog(context); + }, ); } } diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 9559fd45..147d8ab1 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -44,10 +44,6 @@ class $AssetsImagesBonusAnimationGen { class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); - /// File path: assets/images/components/background.png - AssetGenImage get background => - const AssetGenImage('assets/images/components/background.png'); - /// File path: assets/images/components/key.png AssetGenImage get key => const AssetGenImage('assets/images/components/key.png'); diff --git a/lib/how_to_play/widgets/how_to_play_dialog.dart b/lib/how_to_play/widgets/how_to_play_dialog.dart index 766944b9..3dc2c62b 100644 --- a/lib/how_to_play/widgets/how_to_play_dialog.dart +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -52,7 +52,6 @@ extension on Control { Future showHowToPlayDialog(BuildContext context) { return showDialog( context: context, - barrierDismissible: false, builder: (_) => HowToPlayDialog(), ); } diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 5fb0f594..3b00829b 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -138,8 +138,8 @@ class _Character extends StatelessWidget { return Expanded( child: Opacity( opacity: isSelected ? 1 : 0.3, - child: InkWell( - onTap: () => + child: TextButton( + onPressed: () => context.read().characterSelected(character), child: character.icon.image(fit: BoxFit.contain), ), diff --git a/packages/authentication_repository/test/src/authentication_repository_test.dart b/packages/authentication_repository/test/src/authentication_repository_test.dart index a179bb68..0efe9ecc 100644 --- a/packages/authentication_repository/test/src/authentication_repository_test.dart +++ b/packages/authentication_repository/test/src/authentication_repository_test.dart @@ -3,9 +3,9 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class MockFirebaseAuth extends Mock implements FirebaseAuth {} +class _MockFirebaseAuth extends Mock implements FirebaseAuth {} -class MockUserCredential extends Mock implements UserCredential {} +class _MockUserCredential extends Mock implements UserCredential {} void main() { late FirebaseAuth firebaseAuth; @@ -14,8 +14,8 @@ void main() { group('AuthenticationRepository', () { setUp(() { - firebaseAuth = MockFirebaseAuth(); - userCredential = MockUserCredential(); + firebaseAuth = _MockFirebaseAuth(); + userCredential = _MockUserCredential(); authenticationRepository = AuthenticationRepository(firebaseAuth); }); diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart index af3c5fa3..d13a9940 100644 --- a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -5,23 +5,23 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; -class MockFirebaseFirestore extends Mock implements FirebaseFirestore {} +class _MockFirebaseFirestore extends Mock implements FirebaseFirestore {} -class MockCollectionReference extends Mock +class _MockCollectionReference extends Mock implements CollectionReference> {} -class MockQuery extends Mock implements Query> {} +class _MockQuery extends Mock implements Query> {} -class MockQuerySnapshot extends Mock +class _MockQuerySnapshot extends Mock implements QuerySnapshot> {} -class MockQueryDocumentSnapshot extends Mock +class _MockQueryDocumentSnapshot extends Mock implements QueryDocumentSnapshot> {} -class MockDocumentReference extends Mock +class _MockDocumentReference extends Mock implements DocumentReference> {} -class MockDocumentSnapshot extends Mock +class _MockDocumentSnapshot extends Mock implements DocumentSnapshot> {} void main() { @@ -29,7 +29,7 @@ void main() { late FirebaseFirestore firestore; setUp(() { - firestore = MockFirebaseFirestore(); + firestore = _MockFirebaseFirestore(); }); test('can be instantiated', () { @@ -70,11 +70,11 @@ void main() { setUp(() { leaderboardRepository = LeaderboardRepository(firestore); - collectionReference = MockCollectionReference(); - query = MockQuery(); - querySnapshot = MockQuerySnapshot(); + collectionReference = _MockCollectionReference(); + query = _MockQuery(); + querySnapshot = _MockQuerySnapshot(); queryDocumentSnapshots = top10Scores.map((score) { - final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + final queryDocumentSnapshot = _MockQueryDocumentSnapshot(); when(queryDocumentSnapshot.data).thenReturn({ 'character': 'dash', 'playerInitials': 'user$score', @@ -119,7 +119,7 @@ void main() { 'playerInitials': 'ABC', 'score': 1500, }; - final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + final queryDocumentSnapshot = _MockQueryDocumentSnapshot(); when(() => querySnapshot.docs).thenReturn([queryDocumentSnapshot]); when(queryDocumentSnapshot.data) .thenReturn(top10LeaderboardDataMalformed); @@ -156,12 +156,12 @@ void main() { setUp(() { leaderboardRepository = LeaderboardRepository(firestore); - collectionReference = MockCollectionReference(); - documentReference = MockDocumentReference(); - query = MockQuery(); - querySnapshot = MockQuerySnapshot(); + collectionReference = _MockCollectionReference(); + documentReference = _MockDocumentReference(); + query = _MockQuery(); + querySnapshot = _MockQuerySnapshot(); queryDocumentSnapshots = leaderboardScores.map((score) { - final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + final queryDocumentSnapshot = _MockQueryDocumentSnapshot(); when(queryDocumentSnapshot.data).thenReturn({ 'character': 'dash', 'playerInitials': 'AAA', @@ -228,7 +228,7 @@ void main() { 5000 ]; final queryDocumentSnapshots = leaderboardScores.map((score) { - final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + final queryDocumentSnapshot = _MockQueryDocumentSnapshot(); when(queryDocumentSnapshot.data).thenReturn({ 'character': 'dash', 'playerInitials': 'AAA', @@ -248,8 +248,8 @@ void main() { test( 'throws DeleteLeaderboardException ' 'when deleting scores outside the top 10 fails', () async { - final deleteQuery = MockQuery(); - final deleteQuerySnapshot = MockQuerySnapshot(); + final deleteQuery = _MockQuery(); + final deleteQuerySnapshot = _MockQuerySnapshot(); final newScore = LeaderboardEntryData( playerInitials: 'ABC', score: 15000, @@ -269,7 +269,7 @@ void main() { 5000, ]; final deleteDocumentSnapshots = [5500, 5000].map((score) { - final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + final queryDocumentSnapshot = _MockQueryDocumentSnapshot(); when(queryDocumentSnapshot.data).thenReturn({ 'character': 'dash', 'playerInitials': 'AAA', @@ -284,7 +284,7 @@ void main() { when(() => deleteQuerySnapshot.docs) .thenReturn(deleteDocumentSnapshots); final queryDocumentSnapshots = leaderboardScores.map((score) { - final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + final queryDocumentSnapshot = _MockQueryDocumentSnapshot(); when(queryDocumentSnapshot.data).thenReturn({ 'character': 'dash', 'playerInitials': 'AAA', @@ -310,8 +310,8 @@ void main() { 'saves the new score when there are more than 10 scores in the ' 'leaderboard and the new score is higher than the lowest top 10, and ' 'deletes the scores that are not in the top 10 anymore', () async { - final deleteQuery = MockQuery(); - final deleteQuerySnapshot = MockQuerySnapshot(); + final deleteQuery = _MockQuery(); + final deleteQuerySnapshot = _MockQuerySnapshot(); final newScore = LeaderboardEntryData( playerInitials: 'ABC', score: 15000, @@ -331,7 +331,7 @@ void main() { 5000, ]; final deleteDocumentSnapshots = [5500, 5000].map((score) { - final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + final queryDocumentSnapshot = _MockQueryDocumentSnapshot(); when(queryDocumentSnapshot.data).thenReturn({ 'character': 'dash', 'playerInitials': 'AAA', @@ -346,7 +346,7 @@ void main() { when(() => deleteQuerySnapshot.docs) .thenReturn(deleteDocumentSnapshots); final queryDocumentSnapshots = leaderboardScores.map((score) { - final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + final queryDocumentSnapshot = _MockQueryDocumentSnapshot(); when(queryDocumentSnapshot.data).thenReturn({ 'character': 'dash', 'playerInitials': 'AAA', @@ -376,9 +376,9 @@ void main() { late DocumentSnapshot> documentSnapshot; setUp(() async { - collectionReference = MockCollectionReference(); - documentReference = MockDocumentReference(); - documentSnapshot = MockDocumentSnapshot(); + collectionReference = _MockCollectionReference(); + documentReference = _MockDocumentReference(); + documentSnapshot = _MockDocumentSnapshot(); leaderboardRepository = LeaderboardRepository(firestore); when(() => firestore.collection('prohibitedInitials')) diff --git a/packages/pinball_audio/assets/music/background.mp3 b/packages/pinball_audio/assets/music/background.mp3 new file mode 100644 index 00000000..605631b5 Binary files /dev/null and b/packages/pinball_audio/assets/music/background.mp3 differ diff --git a/packages/pinball_audio/assets/sfx/google.mp3 b/packages/pinball_audio/assets/sfx/google.mp3 new file mode 100644 index 00000000..34167d44 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/google.mp3 differ diff --git a/packages/pinball_audio/assets/sfx/google.ogg b/packages/pinball_audio/assets/sfx/google.ogg deleted file mode 100644 index dafaa8d4..00000000 Binary files a/packages/pinball_audio/assets/sfx/google.ogg and /dev/null differ diff --git a/packages/pinball_audio/assets/sfx/plim.mp3 b/packages/pinball_audio/assets/sfx/plim.mp3 new file mode 100644 index 00000000..a726024d Binary files /dev/null and b/packages/pinball_audio/assets/sfx/plim.mp3 differ diff --git a/packages/pinball_audio/assets/sfx/plim.ogg b/packages/pinball_audio/assets/sfx/plim.ogg deleted file mode 100644 index 137c22b7..00000000 Binary files a/packages/pinball_audio/assets/sfx/plim.ogg and /dev/null differ diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart index 3609b939..1b3bdfb9 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -5,16 +5,23 @@ import 'package:flutter/widgets.dart'; +class $AssetsMusicGen { + const $AssetsMusicGen(); + + String get background => 'assets/music/background.mp3'; +} + class $AssetsSfxGen { const $AssetsSfxGen(); - String get google => 'assets/sfx/google.ogg'; - String get plim => 'assets/sfx/plim.ogg'; + String get google => 'assets/sfx/google.mp3'; + String get plim => 'assets/sfx/plim.mp3'; } class Assets { Assets._(); + static const $AssetsMusicGen music = $AssetsMusicGen(); static const $AssetsSfxGen sfx = $AssetsSfxGen(); } diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index b2875084..8bda14e5 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -17,6 +17,14 @@ typedef CreateAudioPool = Future Function( /// audio typedef PlaySingleAudio = Future Function(String); +/// Function that defines the contract for looping a single +/// audio +typedef LoopSingleAudio = Future Function(String); + +/// Function that defines the contract for pre fetching an +/// audio +typedef PreCacheSingleAudio = Future Function(String); + /// Function that defines the contract for configuring /// an [AudioCache] instance typedef ConfigureAudioCache = void Function(AudioCache); @@ -29,9 +37,14 @@ class PinballAudio { PinballAudio({ CreateAudioPool? createAudioPool, PlaySingleAudio? playSingleAudio, + LoopSingleAudio? loopSingleAudio, + PreCacheSingleAudio? preCacheSingleAudio, ConfigureAudioCache? configureAudioCache, }) : _createAudioPool = createAudioPool ?? AudioPool.create, _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play, + _loopSingleAudio = loopSingleAudio ?? FlameAudio.audioCache.loop, + _preCacheSingleAudio = + preCacheSingleAudio ?? FlameAudio.audioCache.load, _configureAudioCache = configureAudioCache ?? ((AudioCache a) { a.prefix = ''; @@ -41,6 +54,10 @@ class PinballAudio { final PlaySingleAudio _playSingleAudio; + final LoopSingleAudio _loopSingleAudio; + + final PreCacheSingleAudio _preCacheSingleAudio; + final ConfigureAudioCache _configureAudioCache; late AudioPool _scorePool; @@ -48,11 +65,17 @@ class PinballAudio { /// Loads the sounds effects into the memory Future load() async { _configureAudioCache(FlameAudio.audioCache); + _scorePool = await _createAudioPool( _prefixFile(Assets.sfx.plim), maxPlayers: 4, prefix: '', ); + + await Future.wait([ + _preCacheSingleAudio(_prefixFile(Assets.sfx.google)), + _preCacheSingleAudio(_prefixFile(Assets.music.background)), + ]); } /// Plays the basic score sound @@ -65,6 +88,11 @@ class PinballAudio { _playSingleAudio(_prefixFile(Assets.sfx.google)); } + /// Plays the background music + void backgroundMusic() { + _loopSingleAudio(_prefixFile(Assets.music.background)); + } + String _prefixFile(String file) { return 'packages/pinball_audio/$file'; } diff --git a/packages/pinball_audio/pubspec.yaml b/packages/pinball_audio/pubspec.yaml index a34ba5b5..74713dfa 100644 --- a/packages/pinball_audio/pubspec.yaml +++ b/packages/pinball_audio/pubspec.yaml @@ -26,3 +26,4 @@ flutter_gen: flutter: assets: - assets/sfx/ + - assets/music/ diff --git a/packages/pinball_audio/test/helpers/helpers.dart b/packages/pinball_audio/test/helpers/helpers.dart deleted file mode 100644 index efe914f6..00000000 --- a/packages/pinball_audio/test/helpers/helpers.dart +++ /dev/null @@ -1 +0,0 @@ -export 'mocks.dart'; diff --git a/packages/pinball_audio/test/helpers/mocks.dart b/packages/pinball_audio/test/helpers/mocks.dart deleted file mode 100644 index c80fe65b..00000000 --- a/packages/pinball_audio/test/helpers/mocks.dart +++ /dev/null @@ -1,34 +0,0 @@ -// ignore_for_file: one_member_abstracts - -import 'package:audioplayers/audioplayers.dart'; -import 'package:flame_audio/audio_pool.dart'; -import 'package:mocktail/mocktail.dart'; - -abstract class _CreateAudioPoolStub { - Future onCall( - String sound, { - bool? repeating, - int? maxPlayers, - int? minPlayers, - String? prefix, - }); -} - -class CreateAudioPoolStub extends Mock implements _CreateAudioPoolStub {} - -abstract class _ConfigureAudioCacheStub { - void onCall(AudioCache cache); -} - -class ConfigureAudioCacheStub extends Mock implements _ConfigureAudioCacheStub { -} - -abstract class _PlaySingleAudioStub { - Future onCall(String url); -} - -class PlaySingleAudioStub extends Mock implements _PlaySingleAudioStub {} - -class MockAudioPool extends Mock implements AudioPool {} - -class MockAudioCache extends Mock implements AudioCache {} diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 2efe9553..c92b876d 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -1,50 +1,92 @@ -// ignore_for_file: prefer_const_constructors +// ignore_for_file: prefer_const_constructors, one_member_abstracts +import 'package:audioplayers/audioplayers.dart'; +import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/flame_audio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/pinball_audio.dart'; -import '../helpers/helpers.dart'; +class _MockAudioPool extends Mock implements AudioPool {} + +class _MockAudioCache extends Mock implements AudioCache {} + +class _MockCreateAudioPool extends Mock { + Future onCall( + String sound, { + bool? repeating, + int? maxPlayers, + int? minPlayers, + String? prefix, + }); +} + +class _MockConfigureAudioCache extends Mock { + void onCall(AudioCache cache); +} + +class _MockPlaySingleAudio extends Mock { + Future onCall(String url); +} + +class _MockLoopSingleAudio extends Mock { + Future onCall(String url); +} + +abstract class _PreCacheSingleAudio { + Future onCall(String url); +} + +class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {} void main() { group('PinballAudio', () { - test('can be instantiated', () { - expect(PinballAudio(), isNotNull); - }); - - late CreateAudioPoolStub createAudioPool; - late ConfigureAudioCacheStub configureAudioCache; - late PlaySingleAudioStub playSingleAudio; + late _MockCreateAudioPool createAudioPool; + late _MockConfigureAudioCache configureAudioCache; + late _MockPlaySingleAudio playSingleAudio; + late _MockLoopSingleAudio loopSingleAudio; + late _PreCacheSingleAudio preCacheSingleAudio; late PinballAudio audio; setUpAll(() { - registerFallbackValue(MockAudioCache()); + registerFallbackValue(_MockAudioCache()); }); setUp(() { - createAudioPool = CreateAudioPoolStub(); + createAudioPool = _MockCreateAudioPool(); when( () => createAudioPool.onCall( any(), maxPlayers: any(named: 'maxPlayers'), prefix: any(named: 'prefix'), ), - ).thenAnswer((_) async => MockAudioPool()); + ).thenAnswer((_) async => _MockAudioPool()); - configureAudioCache = ConfigureAudioCacheStub(); + configureAudioCache = _MockConfigureAudioCache(); when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); - playSingleAudio = PlaySingleAudioStub(); + playSingleAudio = _MockPlaySingleAudio(); when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {}); + loopSingleAudio = _MockLoopSingleAudio(); + when(() => loopSingleAudio.onCall(any())).thenAnswer((_) async {}); + + preCacheSingleAudio = _MockPreCacheSingleAudio(); + when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {}); + audio = PinballAudio( configureAudioCache: configureAudioCache.onCall, createAudioPool: createAudioPool.onCall, playSingleAudio: playSingleAudio.onCall, + loopSingleAudio: loopSingleAudio.onCall, + preCacheSingleAudio: preCacheSingleAudio.onCall, ); }); + test('can be instantiated', () { + expect(PinballAudio(), isNotNull); + }); + group('load', () { test('creates the score pool', () async { await audio.load(); @@ -69,16 +111,30 @@ void main() { audio = PinballAudio( createAudioPool: createAudioPool.onCall, playSingleAudio: playSingleAudio.onCall, + preCacheSingleAudio: preCacheSingleAudio.onCall, ); await audio.load(); expect(FlameAudio.audioCache.prefix, equals('')); }); + + test('pre cache the assets', () async { + await audio.load(); + + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/google.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/music/background.mp3'), + ).called(1); + }); }); group('score', () { test('plays the score sound pool', () async { - final audioPool = MockAudioPool(); + final audioPool = _MockAudioPool(); when(audioPool.start).thenAnswer((_) async => () {}); when( () => createAudioPool.onCall( @@ -106,5 +162,17 @@ void main() { ).called(1); }); }); + + group('backgroundMusic', () { + test('plays the correct file', () async { + await audio.load(); + audio.backgroundMusic(); + + verify( + () => loopSingleAudio + .onCall('packages/pinball_audio/${Assets.music.background}'), + ).called(1); + }); + }); }); } diff --git a/packages/pinball_components/assets/images/dash/bumper/a/inactive.png b/packages/pinball_components/assets/images/dash/bumper/a/inactive.png index aead95ec..bd37498d 100644 Binary files a/packages/pinball_components/assets/images/dash/bumper/a/inactive.png and b/packages/pinball_components/assets/images/dash/bumper/a/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/b/inactive.png b/packages/pinball_components/assets/images/dash/bumper/b/inactive.png index 3d53b743..81cd775a 100644 Binary files a/packages/pinball_components/assets/images/dash/bumper/b/inactive.png and b/packages/pinball_components/assets/images/dash/bumper/b/inactive.png differ diff --git a/packages/pinball_components/assets/images/dash/bumper/main/inactive.png b/packages/pinball_components/assets/images/dash/bumper/main/inactive.png index b1d0ae7d..51df02ee 100644 Binary files a/packages/pinball_components/assets/images/dash/bumper/main/inactive.png and b/packages/pinball_components/assets/images/dash/bumper/main/inactive.png differ diff --git a/packages/pinball_components/assets/images/multiball/dimmed.png b/packages/pinball_components/assets/images/multiball/dimmed.png new file mode 100644 index 00000000..f7d9407a Binary files /dev/null and b/packages/pinball_components/assets/images/multiball/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiball/lit.png b/packages/pinball_components/assets/images/multiball/lit.png new file mode 100644 index 00000000..3444309c Binary files /dev/null and b/packages/pinball_components/assets/images/multiball/lit.png differ diff --git a/packages/pinball_components/assets/images/signpost/active1.png b/packages/pinball_components/assets/images/signpost/active1.png index 1addb228..78997bf6 100644 Binary files a/packages/pinball_components/assets/images/signpost/active1.png and b/packages/pinball_components/assets/images/signpost/active1.png differ diff --git a/packages/pinball_components/assets/images/signpost/active2.png b/packages/pinball_components/assets/images/signpost/active2.png index 081a936c..39caa821 100644 Binary files a/packages/pinball_components/assets/images/signpost/active2.png and b/packages/pinball_components/assets/images/signpost/active2.png differ diff --git a/packages/pinball_components/assets/images/signpost/active3.png b/packages/pinball_components/assets/images/signpost/active3.png index 8d781dfb..f43c190c 100644 Binary files a/packages/pinball_components/assets/images/signpost/active3.png and b/packages/pinball_components/assets/images/signpost/active3.png differ diff --git a/packages/pinball_components/assets/images/signpost/inactive.png b/packages/pinball_components/assets/images/signpost/inactive.png index 6043454b..9fa23330 100644 Binary files a/packages/pinball_components/assets/images/signpost/inactive.png and b/packages/pinball_components/assets/images/signpost/inactive.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index d526909e..3e2faad1 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -28,6 +28,7 @@ class $AssetsImagesGen { $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); + $AssetsImagesMultiballGen get multiball => const $AssetsImagesMultiballGen(); $AssetsImagesMultiplierGen get multiplier => const $AssetsImagesMultiplierGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); @@ -180,6 +181,18 @@ class $AssetsImagesLaunchRampGen { const AssetGenImage('assets/images/launch_ramp/ramp.png'); } +class $AssetsImagesMultiballGen { + const $AssetsImagesMultiballGen(); + + /// File path: assets/images/multiball/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiball/dimmed.png'); + + /// File path: assets/images/multiball/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiball/lit.png'); +} + class $AssetsImagesMultiplierGen { const $AssetsImagesMultiplierGen(); diff --git a/packages/pinball_components/lib/src/components/android_animatronic.dart b/packages/pinball_components/lib/src/components/android_animatronic.dart new file mode 100644 index 00000000..772d88c4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_animatronic.dart @@ -0,0 +1,71 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template android_animatronic} +/// Animated Android that sits on top of the [AndroidSpaceship]. +/// {@endtemplate} +class AndroidAnimatronic extends BodyComponent + with InitialPosition, Layered, ZIndex { + /// {@macro android_animatronic} + AndroidAnimatronic({Iterable? children}) + : super( + children: [ + _AndroidAnimatronicSpriteAnimationComponent(), + ...?children, + ], + renderBody: false, + ) { + layer = Layer.spaceship; + zIndex = ZIndexes.androidHead; + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: 3.1, + minorRadius: 2, + )..rotate(1.4); + final bodyDef = BodyDef(position: initialPosition); + + return world.createBody(bodyDef)..createFixtureFromShape(shape); + } +} + +class _AndroidAnimatronicSpriteAnimationComponent + extends SpriteAnimationComponent with HasGameRef { + _AndroidAnimatronicSpriteAnimationComponent() + : super( + anchor: Anchor.center, + position: Vector2(-0.24, -2.6), + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.android.spaceship.animatronic.keyName, + ); + + const amountPerRow = 18; + const amountPerColumn = 4; + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/android_spaceship.dart b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart similarity index 72% rename from packages/pinball_components/lib/src/components/android_spaceship.dart rename to packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart index aa592d1d..4d98b419 100644 --- a/packages/pinball_components/lib/src/components/android_spaceship.dart +++ b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart @@ -5,17 +5,25 @@ import 'dart:math' as math; 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_components/src/components/android_spaceship/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'cubit/android_spaceship_cubit.dart'; + class AndroidSpaceship extends Component { - AndroidSpaceship({required Vector2 position}) - : super( + AndroidSpaceship({ + required Vector2 position, + }) : bloc = AndroidSpaceshipCubit(), + super( children: [ _SpaceshipSaucer()..initialPosition = position, _SpaceshipSaucerSpriteAnimationComponent()..position = position, _LightBeamSpriteComponent()..position = position + Vector2(2.5, 5), - _AndroidHead()..initialPosition = position + Vector2(0.5, 0.25), + AndroidSpaceshipEntrance( + children: [AndroidSpaceshipEntranceBallContactBehavior()], + ), _SpaceshipHole( outsideLayer: Layer.spaceshipExitRail, outsidePriority: ZIndexes.ballOnSpaceshipRail, @@ -26,6 +34,27 @@ class AndroidSpaceship extends Component { )..initialPosition = position - Vector2(-7.5, -1.1), ], ); + + /// Creates an [AndroidSpaceship] without any children. + /// + /// This can be used for testing [AndroidSpaceship]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + AndroidSpaceship.test({ + required this.bloc, + Iterable? children, + }) : super(children: children); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + final AndroidSpaceshipCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } } class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { @@ -123,62 +152,32 @@ class _LightBeamSpriteComponent extends SpriteComponent } } -class _AndroidHead extends BodyComponent with InitialPosition, Layered, ZIndex { - _AndroidHead() +class AndroidSpaceshipEntrance extends BodyComponent + with ParentIsA, Layered { + AndroidSpaceshipEntrance({Iterable? children}) : super( - children: [_AndroidHeadSpriteAnimationComponent()], + children: children, renderBody: false, ) { layer = Layer.spaceship; - zIndex = ZIndexes.androidHead; } @override Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: 3.1, - minorRadius: 2, - )..rotate(1.4); - final bodyDef = BodyDef(position: initialPosition); - - return world.createBody(bodyDef)..createFixtureFromShape(shape); - } -} - -class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent - with HasGameRef { - _AndroidHeadSpriteAnimationComponent() - : super( - anchor: Anchor.center, - position: Vector2(-0.24, -2.6), - ); - - @override - Future onLoad() async { - await super.onLoad(); - - final spriteSheet = gameRef.images.fromCache( - Assets.images.android.spaceship.animatronic.keyName, + final shape = PolygonShape() + ..setAsBox( + 2, + 0.1, + Vector2(-27.4, -37.2), + -0.12, + ); + final fixtureDef = FixtureDef( + shape, + isSensor: true, ); + final bodyDef = BodyDef(); - const amountPerRow = 18; - const amountPerColumn = 4; - final textureSize = Vector2( - spriteSheet.width / amountPerRow, - spriteSheet.height / amountPerColumn, - ); - size = textureSize / 10; - - animation = SpriteAnimation.fromFrameData( - spriteSheet, - SpriteAnimationData.sequenced( - amount: amountPerRow * amountPerColumn, - amountPerRow: amountPerRow, - stepTime: 1 / 24, - textureSize: textureSize, - ), - ); + return world.createBody(bodyDef)..createFixture(fixtureDef); } } diff --git a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart new file mode 100644 index 00000000..58a8b3c3 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart @@ -0,0 +1,16 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class AndroidSpaceshipEntranceBallContactBehavior + extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + parent.parent.bloc.onBallEntered(); + } +} diff --git a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/behaviors.dart new file mode 100644 index 00000000..cbf54e5d --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'android_spaceship_entrance_ball_contact_behavior.dart.dart'; diff --git a/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart new file mode 100644 index 00000000..ad9de251 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart @@ -0,0 +1,13 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'android_spaceship_state.dart'; + +class AndroidSpaceshipCubit extends Cubit { + AndroidSpaceshipCubit() : super(AndroidSpaceshipState.withoutBonus); + + void onBallEntered() => emit(AndroidSpaceshipState.withBonus); + + void onBonusAwarded() => emit(AndroidSpaceshipState.withoutBonus); +} diff --git a/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_state.dart b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_state.dart new file mode 100644 index 00000000..aae41c17 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_state.dart @@ -0,0 +1,8 @@ +// ignore_for_file: public_member_api_docs + +part of 'android_spaceship_cubit.dart'; + +enum AndroidSpaceshipState { + withoutBonus, + withBonus, +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart index b98a4093..649e804b 100644 --- a/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart +++ b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart @@ -18,10 +18,17 @@ class ChromeDinoCubit extends Cubit { } void onChomp(Ball ball) { - emit(state.copyWith(status: ChromeDinoStatus.chomping, ball: ball)); + if (ball != state.ball) { + emit(state.copyWith(status: ChromeDinoStatus.chomping, ball: ball)); + } } void onSpit() { - emit(state.copyWith(status: ChromeDinoStatus.idle)); + emit( + ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: state.isMouthOpen, + ), + ); } } diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 394f32ed..c5ea7f9f 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,5 +1,6 @@ +export 'android_animatronic.dart'; export 'android_bumper/android_bumper.dart'; -export 'android_spaceship.dart'; +export 'android_spaceship/android_spaceship.dart'; export 'backboard/backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; @@ -21,12 +22,13 @@ export 'kicker/kicker.dart'; export 'launch_ramp.dart'; export 'layer.dart'; export 'layer_sensor.dart'; +export 'multiball/multiball.dart'; export 'multiplier/multiplier.dart'; export 'plunger.dart'; export 'rocket.dart'; export 'score_component.dart'; export 'shapes/shapes.dart'; -export 'signpost.dart'; +export 'signpost/signpost.dart'; export 'slingshot.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; diff --git a/packages/pinball_components/lib/src/components/kicker/kicker.dart b/packages/pinball_components/lib/src/components/kicker/kicker.dart index 570f2990..1a45ad60 100644 --- a/packages/pinball_components/lib/src/components/kicker/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker/kicker.dart @@ -36,7 +36,7 @@ class Kicker extends BodyComponent with InitialPosition { }) : _side = side, super( children: [ - BumpingBehavior(strength: 20)..applyTo(['bouncy_edge']), + BumpingBehavior(strength: 25)..applyTo(['bouncy_edge']), KickerBallContactBehavior()..applyTo(['bouncy_edge']), KickerBlinkingBehavior(), _KickerSpriteGroupComponent( diff --git a/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart new file mode 100644 index 00000000..052b4a4e --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multiball_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart new file mode 100644 index 00000000..48c90552 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/behaviors/multiball_blinking_behavior.dart @@ -0,0 +1,78 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template multiball_blinking_behavior} +/// Makes a [Multiball] blink back to [MultiballLightState.lit] when +/// [MultiballLightState.dimmed]. +/// {@endtemplate} +class MultiballBlinkingBehavior extends TimerComponent + with ParentIsA { + /// {@macro multiball_blinking_behavior} + MultiballBlinkingBehavior() : super(period: 0.1); + + final _maxBlinks = 10; + + int _blinksCounter = 0; + + bool _isAnimating = false; + + void _onNewState(MultiballState state) { + final animationEnabled = + state.animationState == MultiballAnimationState.blinking; + final canBlink = _blinksCounter < _maxBlinks; + + if (animationEnabled && canBlink) { + _start(); + } else { + _stop(); + } + } + + void _start() { + if (!_isAnimating) { + _isAnimating = true; + timer + ..reset() + ..start(); + _animate(); + } + } + + void _animate() { + parent.bloc.onBlink(); + _blinksCounter++; + } + + void _stop() { + if (_isAnimating) { + _isAnimating = false; + timer.stop(); + _blinksCounter = 0; + parent.bloc.onStop(); + } + } + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + if (!_isAnimating) { + timer.stop(); + } else { + if (_blinksCounter < _maxBlinks) { + _animate(); + timer + ..reset() + ..start(); + } else { + timer.stop(); + } + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart new file mode 100644 index 00000000..9d943c9d --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_cubit.dart @@ -0,0 +1,37 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'multiball_state.dart'; + +class MultiballCubit extends Cubit { + MultiballCubit() : super(const MultiballState.initial()); + + void onAnimate() { + emit( + state.copyWith(animationState: MultiballAnimationState.blinking), + ); + } + + void onStop() { + emit( + state.copyWith(animationState: MultiballAnimationState.idle), + ); + } + + void onBlink() { + switch (state.lightState) { + case MultiballLightState.lit: + emit( + state.copyWith(lightState: MultiballLightState.dimmed), + ); + break; + case MultiballLightState.dimmed: + emit( + state.copyWith(lightState: MultiballLightState.lit), + ); + break; + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart new file mode 100644 index 00000000..bbc66fd5 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/cubit/multiball_state.dart @@ -0,0 +1,44 @@ +// ignore_for_file: comment_references, public_member_api_docs + +part of 'multiball_cubit.dart'; + +/// Indicates the different sprite states for [MultiballSpriteGroupComponent]. +enum MultiballLightState { + lit, + dimmed, +} + +// Indicates if the blinking animation is running. +enum MultiballAnimationState { + idle, + blinking, +} + +class MultiballState extends Equatable { + const MultiballState({ + required this.lightState, + required this.animationState, + }); + + const MultiballState.initial() + : this( + lightState: MultiballLightState.dimmed, + animationState: MultiballAnimationState.idle, + ); + + final MultiballLightState lightState; + final MultiballAnimationState animationState; + + MultiballState copyWith({ + MultiballLightState? lightState, + MultiballAnimationState? animationState, + }) { + return MultiballState( + lightState: lightState ?? this.lightState, + animationState: animationState ?? this.animationState, + ); + } + + @override + List get props => [lightState, animationState]; +} diff --git a/packages/pinball_components/lib/src/components/multiball/multiball.dart b/packages/pinball_components/lib/src/components/multiball/multiball.dart new file mode 100644 index 00000000..ca348604 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiball/multiball.dart @@ -0,0 +1,138 @@ +import 'dart:math' as math; +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart'; +import 'package:pinball_components/src/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/multiball_cubit.dart'; + +/// {@template multiball} +/// A [Component] for the multiball lighting decals on the board. +/// {@endtemplate} +class Multiball extends Component { + /// {@macro multiball} + Multiball._({ + required Vector2 position, + double rotation = 0, + Iterable? children, + required this.bloc, + }) : super( + children: [ + MultiballBlinkingBehavior(), + MultiballSpriteGroupComponent( + position: position, + litAssetPath: Assets.images.multiball.lit.keyName, + dimmedAssetPath: Assets.images.multiball.dimmed.keyName, + rotation: rotation, + state: bloc.state.lightState, + ), + ...?children, + ], + ); + + /// {@macro multiball} + Multiball.a({ + Iterable? children, + }) : this._( + position: Vector2(-23, 7.5), + rotation: -24 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.b({ + Iterable? children, + }) : this._( + position: Vector2(-7.2, -6.2), + rotation: -5 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.c({ + Iterable? children, + }) : this._( + position: Vector2(-0.7, -9.3), + rotation: 2.7 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// {@macro multiball} + Multiball.d({ + Iterable? children, + }) : this._( + position: Vector2(15, 7), + rotation: 24 * math.pi / 180, + bloc: MultiballCubit(), + children: children, + ); + + /// Creates an [Multiball] without any children. + /// + /// This can be used for testing [Multiball]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + Multiball.test({ + required this.bloc, + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final MultiballCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } +} + +/// {@template multiball_sprite_group_component} +/// A [SpriteGroupComponent] for the multiball over the board. +/// {@endtemplate} +@visibleForTesting +class MultiballSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + /// {@macro multiball_sprite_group_component} + MultiballSpriteGroupComponent({ + required Vector2 position, + required String litAssetPath, + required String dimmedAssetPath, + required double rotation, + required MultiballLightState state, + }) : _litAssetPath = litAssetPath, + _dimmedAssetPath = dimmedAssetPath, + super( + anchor: Anchor.center, + position: position, + angle: rotation, + current: state, + ); + + final String _litAssetPath; + final String _dimmedAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state.lightState); + + final sprites = { + MultiballLightState.lit: Sprite( + gameRef.images.fromCache(_litAssetPath), + ), + MultiballLightState.dimmed: + Sprite(gameRef.images.fromCache(_dimmedAssetPath)), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart index fa81c783..79b370a0 100644 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -68,6 +68,14 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { return body; } + var _pullingDownTime = 0.0; + + /// Pulls the plunger down for the given amount of [seconds]. + // ignore: use_setters_to_change_properties + void pullFor(double seconds) { + _pullingDownTime = seconds; + } + /// Set a constant downward velocity on the [Plunger]. void pull() { body.linearVelocity = Vector2(0, 7); @@ -79,11 +87,26 @@ 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() { + _pullingDownTime = 0; final velocity = (initialPosition.y - body.position.y) * 11; body.linearVelocity = Vector2(0, velocity); _spriteComponent.release(); } + @override + void update(double dt) { + // Ensure that we only pull or release when the time is greater than zero. + if (_pullingDownTime > 0) { + _pullingDownTime -= dt; + if (_pullingDownTime <= 0) { + release(); + } else { + pull(); + } + } + super.update(dt); + } + /// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical /// motion. Future _anchorToJoint() async { diff --git a/packages/pinball_components/lib/src/components/signpost.dart b/packages/pinball_components/lib/src/components/signpost.dart deleted file mode 100644 index 13425342..00000000 --- a/packages/pinball_components/lib/src/components/signpost.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// Represents the [Signpost]'s current [Sprite] state. -@visibleForTesting -enum SignpostSpriteState { - /// Signpost with no active dashes. - inactive, - - /// Signpost with a single sign of active dashes. - active1, - - /// Signpost with two signs of active dashes. - active2, - - /// Signpost with all signs of active dashes. - active3, -} - -extension on SignpostSpriteState { - String get path { - switch (this) { - case SignpostSpriteState.inactive: - return Assets.images.signpost.inactive.keyName; - case SignpostSpriteState.active1: - return Assets.images.signpost.active1.keyName; - case SignpostSpriteState.active2: - return Assets.images.signpost.active2.keyName; - case SignpostSpriteState.active3: - return Assets.images.signpost.active3.keyName; - } - } - - SignpostSpriteState get next { - return SignpostSpriteState - .values[(index + 1) % SignpostSpriteState.values.length]; - } -} - -/// {@template signpost} -/// A sign, found in the Flutter Forest. -/// -/// Lights up a new sign whenever all three [DashNestBumper]s are hit. -/// {@endtemplate} -class Signpost extends BodyComponent with InitialPosition { - /// {@macro signpost} - Signpost({ - Iterable? children, - }) : super( - renderBody: false, - children: [ - _SignpostSpriteComponent(), - ...?children, - ], - ); - - /// Forwards the sprite to the next [SignpostSpriteState]. - /// - /// If the current state is the last one it cycles back to the initial state. - void progress() => firstChild<_SignpostSpriteComponent>()!.progress(); - - @override - Body createBody() { - final shape = CircleShape()..radius = 0.25; - final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef( - position: initialPosition, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -class _SignpostSpriteComponent extends SpriteGroupComponent - with HasGameRef { - _SignpostSpriteComponent() - : super( - anchor: Anchor.bottomCenter, - position: Vector2(0.65, 0.45), - ); - - void progress() => current = current?.next; - - @override - Future onLoad() async { - await super.onLoad(); - - final sprites = {}; - this.sprites = sprites; - for (final spriteState in SignpostSpriteState.values) { - sprites[spriteState] = Sprite( - gameRef.images.fromCache(spriteState.path), - ); - } - - current = SignpostSpriteState.inactive; - size = sprites[current]!.originalSize / 10; - } -} diff --git a/packages/pinball_components/lib/src/components/signpost/cubit/signpost_cubit.dart b/packages/pinball_components/lib/src/components/signpost/cubit/signpost_cubit.dart new file mode 100644 index 00000000..f94feebe --- /dev/null +++ b/packages/pinball_components/lib/src/components/signpost/cubit/signpost_cubit.dart @@ -0,0 +1,18 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'signpost_state.dart'; + +class SignpostCubit extends Cubit { + SignpostCubit() : super(SignpostState.inactive); + + void onProgressed() { + final index = SignpostState.values.indexOf(state); + emit( + SignpostState.values[(index + 1) % SignpostState.values.length], + ); + } + + bool isFullyProgressed() => state == SignpostState.active3; +} diff --git a/packages/pinball_components/lib/src/components/signpost/cubit/signpost_state.dart b/packages/pinball_components/lib/src/components/signpost/cubit/signpost_state.dart new file mode 100644 index 00000000..72173bf1 --- /dev/null +++ b/packages/pinball_components/lib/src/components/signpost/cubit/signpost_state.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +part of 'signpost_cubit.dart'; + +enum SignpostState { + /// Signpost with no active eggs. + inactive, + + /// Signpost with a single sign of lit up eggs. + active1, + + /// Signpost with two signs of lit up eggs. + active2, + + /// Signpost with all signs of lit up eggs. + active3, +} diff --git a/packages/pinball_components/lib/src/components/signpost/signpost.dart b/packages/pinball_components/lib/src/components/signpost/signpost.dart new file mode 100644 index 00000000..d22f46f3 --- /dev/null +++ b/packages/pinball_components/lib/src/components/signpost/signpost.dart @@ -0,0 +1,109 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/foundation.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/signpost_cubit.dart'; + +/// {@template signpost} +/// A sign, found in the Flutter Forest. +/// +/// Lights up a new sign whenever all three [DashNestBumper]s are hit. +/// {@endtemplate} +class Signpost extends BodyComponent with InitialPosition { + /// {@macro signpost} + Signpost({ + Iterable? children, + }) : this._( + children: children, + bloc: SignpostCubit(), + ); + + Signpost._({ + Iterable? children, + required this.bloc, + }) : super( + renderBody: false, + children: [ + _SignpostSpriteComponent( + current: bloc.state, + ), + ...?children, + ], + ); + + /// Creates a [Signpost] without any children. + /// + /// This can be used for testing [Signpost]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + Signpost.test({ + required this.bloc, + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final SignpostCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = CircleShape()..radius = 0.25; + final bodyDef = BodyDef( + position: initialPosition, + ); + + return world.createBody(bodyDef)..createFixtureFromShape(shape); + } +} + +class _SignpostSpriteComponent extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _SignpostSpriteComponent({ + required SignpostState current, + }) : super( + anchor: Anchor.bottomCenter, + position: Vector2(0.65, 0.45), + current: current, + ); + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state); + + final sprites = {}; + this.sprites = sprites; + for (final spriteState in SignpostState.values) { + sprites[spriteState] = Sprite( + gameRef.images.fromCache(spriteState.path), + ); + } + + current = SignpostState.inactive; + size = sprites[current]!.originalSize / 10; + } +} + +extension on SignpostState { + String get path { + switch (this) { + case SignpostState.inactive: + return Assets.images.signpost.inactive.keyName; + case SignpostState.active1: + return Assets.images.signpost.active1.keyName; + case SignpostState.active2: + return Assets.images.signpost.active2.keyName; + case SignpostState.active3: + return Assets.images.signpost.active3.keyName; + } + } +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 1d2232e0..61e62386 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -81,6 +81,7 @@ flutter: - assets/images/google_word/letter5/ - assets/images/google_word/letter6/ - assets/images/signpost/ + - assets/images/multiball/ - assets/images/multiplier/x2/ - assets/images/multiplier/x3/ - assets/images/multiplier/x4/ diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 25473f02..9fdee65a 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -27,6 +27,7 @@ void main() { addScoreStories(dashbook); addBackboardStories(dashbook); addDinoWallStories(dashbook); + addMultiballStories(dashbook); addMultipliersStories(dashbook); runApp(dashbook); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart index 6799ef29..976f4894 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart @@ -17,7 +17,7 @@ class AndroidSpaceshipGame extends BallGame { ); static const description = ''' - Shows how the AndroidSpaceship is rendered. + Shows how the AndroidSpaceship and AndroidAnimatronic are rendered. - Activate the "trace" parameter to overlay the body. - Tap anywhere on the screen to spawn a Ball into the game. @@ -28,8 +28,11 @@ class AndroidSpaceshipGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2.zero()); - await add( - AndroidSpaceship(position: Vector2.zero()), + await addAll( + [ + AndroidSpaceship(position: Vector2.zero()), + AndroidAnimatronic(), + ], ); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart index 349dd811..020311d3 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart @@ -34,6 +34,6 @@ class SignpostGame extends BallGame { @override void onTap() { super.onTap(); - firstChild()!.progress(); + firstChild()!.bloc.onProgressed(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart b/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart new file mode 100644 index 00000000..83b53785 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multiball/multiball_game.dart @@ -0,0 +1,56 @@ +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class MultiballGame extends BallGame with KeyboardEvents { + MultiballGame() + : super( + imagesFileNames: [ + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, + ], + ); + + static const description = ''' + Shows how the Multiball are rendered. + + - Tap anywhere on the screen to spawn a ball into the game. + - Press space bar to animate multiballs. +'''; + + final List multiballs = [ + Multiball.a(), + Multiball.b(), + Multiball.c(), + Multiball.d(), + ]; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + + await addAll(multiballs); + await traceAllBodies(); + } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (event is RawKeyDownEvent && + event.logicalKey == LogicalKeyboardKey.space) { + for (final multiball in multiballs) { + multiball.bloc.onBlink(); + } + + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/multiball/stories.dart b/packages/pinball_components/sandbox/lib/stories/multiball/stories.dart new file mode 100644 index 00000000..6993ed92 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multiball/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/multiball/multiball_game.dart'; + +void addMultiballStories(Dashbook dashbook) { + dashbook.storiesOf('Multiball').addGame( + title: 'Assets', + description: MultiballGame.description, + gameBuilder: (_) => MultiballGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 89bf5d96..8cdd38b1 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -10,6 +10,7 @@ export 'flutter_forest/stories.dart'; export 'google_word/stories.dart'; export 'launch_ramp/stories.dart'; export 'layer/stories.dart'; +export 'multiball/stories.dart'; export 'multipliers/stories.dart'; export 'plunger/stories.dart'; export 'score/stories.dart'; diff --git a/packages/pinball_components/test/helpers/helpers.dart b/packages/pinball_components/test/helpers/helpers.dart index 312f42ec..a8b9f7ff 100644 --- a/packages/pinball_components/test/helpers/helpers.dart +++ b/packages/pinball_components/test/helpers/helpers.dart @@ -1,2 +1 @@ -export 'mocks.dart'; export 'test_game.dart'; diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart deleted file mode 100644 index ab867e3b..00000000 --- a/packages/pinball_components/test/helpers/mocks.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball_components/pinball_components.dart'; - -class MockFilter extends Mock implements Filter {} - -class MockFixture extends Mock implements Fixture {} - -class MockBody extends Mock implements Body {} - -class MockBall extends Mock implements Ball {} - -class MockGame extends Mock implements Forge2DGame {} - -class MockContact extends Mock implements Contact {} - -class MockComponent extends Mock implements Component {} - -class MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {} - -class MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {} - -class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} - -class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} - -class MockMultiplierCubit extends Mock implements MultiplierCubit {} - -class MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} diff --git a/packages/pinball_components/test/src/components/android_animatronic_test.dart b/packages/pinball_components/test/src/components/android_animatronic_test.dart new file mode 100644 index 00000000..65114778 --- /dev/null +++ b/packages/pinball_components/test/src/components/android_animatronic_test.dart @@ -0,0 +1,70 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final asset = Assets.images.android.spaceship.animatronic.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + group('AndroidAnimatronic', () { + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.load(asset); + await game.ensureAdd(AndroidAnimatronic()); + game.camera.followVector2(Vector2.zero()); + await tester.pump(); + }, + verify: (game, tester) async { + final animationDuration = game + .firstChild()! + .firstChild()! + .animation! + .totalDuration(); + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/android_animatronic/start.png'), + ); + + game.update(animationDuration * 0.5); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/android_animatronic/middle.png'), + ); + + game.update(animationDuration * 0.5); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/android_animatronic/end.png'), + ); + }, + ); + + flameTester.test( + 'loads correctly', + (game) async { + final androidAnimatronic = AndroidAnimatronic(); + await game.ensureAdd(androidAnimatronic); + expect(game.contains(androidAnimatronic), isTrue); + }, + ); + + flameTester.test('adds new children', (game) async { + final component = Component(); + final androidAnimatronic = AndroidAnimatronic( + children: [component], + ); + await game.ensureAdd(androidAnimatronic); + expect(androidAnimatronic.children, contains(component)); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart b/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart index e2fed1d2..aaca08fc 100644 --- a/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart +++ b/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart @@ -11,6 +11,8 @@ import 'package:pinball_components/src/components/bumping_behavior.dart'; import '../../../helpers/helpers.dart'; +class _MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -46,7 +48,7 @@ void main() { // https://github.com/flame-engine/flame/pull/1538 // ignore: public_member_api_docs flameTester.test('closes bloc when removed', (game) async { - final bloc = MockAndroidBumperCubit(); + final bloc = _MockAndroidBumperCubit(); whenListen( bloc, const Stream.empty(), diff --git a/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior_test.dart index 69e6ce43..8c44a199 100644 --- a/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior_test.dart @@ -1,6 +1,7 @@ // 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'; @@ -9,6 +10,12 @@ import 'package:pinball_components/src/components/android_bumper/behaviors/behav import '../../../../helpers/helpers.dart'; +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + +class _MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -27,7 +34,7 @@ void main() { 'beginContact emits onBallContacted when contacts with a ball', (game) async { final behavior = AndroidBumperBallContactBehavior(); - final bloc = MockAndroidBumperCubit(); + final bloc = _MockAndroidBumperCubit(); whenListen( bloc, const Stream.empty(), @@ -38,7 +45,7 @@ void main() { await androidBumper.add(behavior); await game.ensureAdd(androidBumper); - behavior.beginContact(MockBall(), MockContact()); + behavior.beginContact(_MockBall(), _MockContact()); verify(androidBumper.bloc.onBallContacted).called(1); }, diff --git a/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_blinking_behavior_test.dart index f7b09dfb..37cb93ba 100644 --- a/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_blinking_behavior_test.dart +++ b/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_blinking_behavior_test.dart @@ -9,6 +9,8 @@ import 'package:pinball_components/src/components/android_bumper/behaviors/behav import '../../../../helpers/helpers.dart'; +class _MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -20,7 +22,7 @@ void main() { 'calls onBlinked after 0.05 seconds when dimmed', setUp: (game, tester) async { final behavior = AndroidBumperBlinkingBehavior(); - final bloc = MockAndroidBumperCubit(); + final bloc = _MockAndroidBumperCubit(); final streamController = StreamController(); whenListen( bloc, diff --git a/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart b/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart new file mode 100644 index 00000000..1b672be4 --- /dev/null +++ b/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart @@ -0,0 +1,109 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +import '../../../helpers/helpers.dart'; + +class _MockAndroidSpaceshipCubit extends Mock implements AndroidSpaceshipCubit { +} + +void main() { + group('AndroidSpaceship', () { + final assets = [ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('loads correctly', (game) async { + final component = AndroidSpaceship(position: Vector2.zero()); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final canvas = ZCanvasComponent( + children: [AndroidSpaceship(position: Vector2.zero())], + ); + await game.ensureAdd(canvas); + game.camera.followVector2(Vector2.zero()); + await game.ready(); + await tester.pump(); + }, + verify: (game, tester) async { + const goldenFilePath = '../golden/android_spaceship/'; + final animationDuration = game + .descendants() + .whereType() + .single + .animation! + .totalDuration(); + + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenFilePath}start.png'), + ); + + game.update(animationDuration * 0.5); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenFilePath}middle.png'), + ); + + game.update(animationDuration * 0.5); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenFilePath}end.png'), + ); + }, + ); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = _MockAndroidSpaceshipCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: AndroidSpaceshipState.withoutBonus, + ); + when(bloc.close).thenAnswer((_) async {}); + final androidSpaceship = AndroidSpaceship.test(bloc: bloc); + + await game.ensureAdd(androidSpaceship); + game.remove(androidSpaceship); + await game.ready(); + + verify(bloc.close).called(1); + }); + + flameTester.test( + 'AndroidSpaceshipEntrance has an ' + 'AndroidSpaceshipEntranceBallContactBehavior', (game) async { + final androidSpaceship = AndroidSpaceship(position: Vector2.zero()); + await game.ensureAdd(androidSpaceship); + + final androidSpaceshipEntrance = + androidSpaceship.firstChild(); + expect( + androidSpaceshipEntrance!.children + .whereType() + .single, + isNotNull, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart new file mode 100644 index 00000000..d6056beb --- /dev/null +++ b/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart @@ -0,0 +1,60 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockAndroidSpaceshipCubit extends Mock implements AndroidSpaceshipCubit { +} + +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'AndroidSpaceshipEntranceBallContactBehavior', + () { + test('can be instantiated', () { + expect( + AndroidSpaceshipEntranceBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact calls onBallEntered when entrance contacts with a ball', + (game) async { + final behavior = AndroidSpaceshipEntranceBallContactBehavior(); + final bloc = _MockAndroidSpaceshipCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: AndroidSpaceshipState.withoutBonus, + ); + + final entrance = AndroidSpaceshipEntrance(); + final androidSpaceship = AndroidSpaceship.test( + bloc: bloc, + children: [entrance], + ); + await entrance.add(behavior); + await game.ensureAdd(androidSpaceship); + + behavior.beginContact(_MockBall(), _MockContact()); + + verify(androidSpaceship.bloc.onBallEntered).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart b/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart new file mode 100644 index 00000000..47b763af --- /dev/null +++ b/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'AndroidSpaceshipCubit', + () { + blocTest( + 'onBallEntered emits withBonus', + build: AndroidSpaceshipCubit.new, + act: (bloc) => bloc.onBallEntered(), + expect: () => [AndroidSpaceshipState.withBonus], + ); + + blocTest( + 'onBonusAwarded emits withoutBonus', + build: AndroidSpaceshipCubit.new, + act: (bloc) => bloc.onBonusAwarded(), + expect: () => [AndroidSpaceshipState.withoutBonus], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/android_spaceship_test.dart b/packages/pinball_components/test/src/components/android_spaceship_test.dart deleted file mode 100644 index 7e7eda96..00000000 --- a/packages/pinball_components/test/src/components/android_spaceship_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame/components.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('AndroidSpaceship', () { - final assets = [ - Assets.images.android.spaceship.saucer.keyName, - Assets.images.android.spaceship.animatronic.keyName, - Assets.images.android.spaceship.lightBeam.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); - - flameTester.test('loads correctly', (game) async { - final component = AndroidSpaceship(position: Vector2.zero()); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); - }); - - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final canvas = ZCanvasComponent( - children: [AndroidSpaceship(position: Vector2.zero())], - ); - await game.ensureAdd(canvas); - game.camera.followVector2(Vector2.zero()); - await game.ready(); - await tester.pump(); - }, - verify: (game, tester) async { - final animationDuration = game - .descendants() - .whereType() - .last - .animation! - .totalDuration(); - - await expectLater( - find.byGame(), - matchesGoldenFile('golden/android_spaceship/start.png'), - ); - - game.update(animationDuration * 0.5); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/android_spaceship/middle.png'), - ); - - game.update(animationDuration * 0.5); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/android_spaceship/end.png'), - ); - }, - ); - }); -} 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 50407770..dfc33967 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart @@ -2,6 +2,7 @@ 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'; @@ -11,6 +12,12 @@ import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; +class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} + +class _MockContact extends Mock implements Contact {} + +class _MockFixture extends Mock implements Fixture {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -33,7 +40,7 @@ void main() { (game) async { final ball = Ball(); final behavior = ChromeDinoChompingBehavior(); - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); whenListen( bloc, const Stream.empty(), @@ -47,8 +54,8 @@ void main() { await chromeDino.add(behavior); await game.ensureAddAll([chromeDino, ball]); - final contact = MockContact(); - final fixture = MockFixture(); + final contact = _MockContact(); + final fixture = _MockFixture(); when(() => contact.fixtureA).thenReturn(fixture); when(() => fixture.userData).thenReturn('inside_mouth'); diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior_test.dart index 31e49cd4..69f57114 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior_test.dart @@ -10,6 +10,14 @@ import 'package:pinball_components/src/components/chrome_dino/behaviors/behavior import '../../../../helpers/helpers.dart'; +class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} + +class _MockContact extends Mock implements Contact {} + +class _MockFixture extends Mock implements Fixture {} + +class _MockBall extends Mock implements Ball {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -29,7 +37,7 @@ void main() { 'and there is not ball in the mouth', (game) async { final behavior = ChromeDinoMouthOpeningBehavior(); - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); whenListen( bloc, const Stream.empty(), @@ -43,12 +51,12 @@ void main() { await chromeDino.add(behavior); await game.ensureAdd(chromeDino); - final contact = MockContact(); - final fixture = MockFixture(); + final contact = _MockContact(); + final fixture = _MockFixture(); when(() => contact.fixtureA).thenReturn(fixture); when(() => fixture.userData).thenReturn('mouth_opening'); - behavior.preSolve(MockBall(), contact, Manifold()); + behavior.preSolve(_MockBall(), contact, Manifold()); verify(() => contact.setEnabled(false)).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 a3852cc2..8c2cbe57 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart @@ -13,6 +13,8 @@ import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; +class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -36,7 +38,7 @@ void main() { (game) async { final ball = Ball(); final behavior = ChromeDinoSpittingBehavior(); - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); final streamController = StreamController(); final chompingState = ChromeDinoState( status: ChromeDinoStatus.chomping, @@ -74,7 +76,7 @@ void main() { (game) async { final ball = Ball(); final behavior = ChromeDinoSpittingBehavior(); - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); final streamController = StreamController(); final chompingState = ChromeDinoState( status: ChromeDinoStatus.chomping, diff --git a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart index 5dd6c06d..9b6a05b6 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart @@ -10,6 +10,8 @@ import 'package:pinball_components/src/components/chrome_dino/behaviors/behavior import '../../../../helpers/helpers.dart'; +class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -30,7 +32,7 @@ void main() { 'creates a RevoluteJoint', (game) async { final behavior = ChromeDinoSwivelingBehavior(); - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); whenListen( bloc, const Stream.empty(), @@ -52,7 +54,7 @@ void main() { 'reverses swivel direction on each timer tick', (game) async { final behavior = ChromeDinoSwivelingBehavior(); - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); whenListen( bloc, const Stream.empty(), @@ -84,7 +86,7 @@ void main() { 'and mouth is open', setUp: (game, tester) async { final behavior = ChromeDinoSwivelingBehavior(); - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); whenListen( bloc, const Stream.empty(), @@ -113,7 +115,7 @@ void main() { 'and mouth is closed', setUp: (game, tester) async { final behavior = ChromeDinoSwivelingBehavior(); - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); whenListen( bloc, const Stream.empty(), @@ -141,7 +143,7 @@ void main() { 'and mouth is closed', setUp: (game, tester) async { final behavior = ChromeDinoSwivelingBehavior(); - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); whenListen( bloc, const Stream.empty(), diff --git a/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart b/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart index 9826cf95..4c1802ef 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart @@ -10,6 +10,8 @@ import 'package:pinball_components/src/components/chrome_dino/behaviors/behavior import '../../../helpers/helpers.dart'; +class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -73,7 +75,7 @@ void main() { // https://github.com/flame-engine/flame/pull/1538 // ignore: public_member_api_docs flameTester.test('closes bloc when removed', (game) async { - final bloc = MockChromeDinoCubit(); + final bloc = _MockChromeDinoCubit(); whenListen( bloc, const Stream.empty(), diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart index 96f00dd4..79375a6e 100644 --- a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart @@ -35,7 +35,8 @@ void main() { ); blocTest( - 'onChomp emits ChromeDinoStatus.chomping and chomped ball', + 'onChomp emits ChromeDinoStatus.chomping and chomped ball ' + 'when the ball is not in the mouth', build: ChromeDinoCubit.new, act: (bloc) => bloc.onChomp(ball), expect: () => [ @@ -54,7 +55,15 @@ void main() { ); blocTest( - 'onSpit emits ChromeDinoStatus.idle', + 'onChomp emits nothing when the ball is already in the mouth', + build: ChromeDinoCubit.new, + seed: () => const ChromeDinoState.inital().copyWith(ball: ball), + act: (bloc) => bloc.onChomp(ball), + expect: () => [], + ); + + blocTest( + 'onSpit emits ChromeDinoStatus.idle and removes ball', build: ChromeDinoCubit.new, act: (bloc) => bloc.onSpit(), expect: () => [ @@ -62,7 +71,11 @@ void main() { (state) => state.status, 'status', ChromeDinoStatus.idle, - ) + )..having( + (state) => state.ball, + 'ball', + null, + ) ], ); }, diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart index bf7513bd..10627df6 100644 --- a/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart @@ -1,6 +1,7 @@ // 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'; @@ -9,6 +10,12 @@ import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/beh import '../../../../helpers/helpers.dart'; +class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} + +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -27,7 +34,7 @@ void main() { 'beginContact emits onBallContacted when contacts with a ball', (game) async { final behavior = DashNestBumperBallContactBehavior(); - final bloc = MockDashNestBumperCubit(); + final bloc = _MockDashNestBumperCubit(); whenListen( bloc, const Stream.empty(), @@ -38,7 +45,7 @@ void main() { await dashNestBumper.add(behavior); await game.ensureAdd(dashNestBumper); - behavior.beginContact(MockBall(), MockContact()); + behavior.beginContact(_MockBall(), _MockContact()); verify(dashNestBumper.bloc.onBallContacted).called(1); }, diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart index df3ee36f..195231bf 100644 --- a/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart +++ b/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart @@ -11,6 +11,8 @@ import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/beh import '../../../helpers/helpers.dart'; +class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -48,7 +50,7 @@ void main() { // https://github.com/flame-engine/flame/pull/1538 // ignore: public_member_api_docs flameTester.test('closes bloc when removed', (game) async { - final bloc = MockDashNestBumperCubit(); + final bloc = _MockDashNestBumperCubit(); whenListen( bloc, const Stream.empty(), diff --git a/packages/pinball_components/test/src/components/golden/android_animatronic/end.png b/packages/pinball_components/test/src/components/golden/android_animatronic/end.png new file mode 100644 index 00000000..3d54999f Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_animatronic/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png b/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png new file mode 100644 index 00000000..44916338 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_animatronic/start.png b/packages/pinball_components/test/src/components/golden/android_animatronic/start.png new file mode 100644 index 00000000..95580e91 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_animatronic/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/end.png b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png index c2a0631a..a64b4724 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_spaceship/end.png and b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png index c6651abd..90361e22 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png and b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/start.png b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png index 25e8863a..649a8654 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_spaceship/start.png and b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png b/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png index 1d3daa81..035a152f 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png b/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png index f0312ae5..23c1142d 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png b/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png index 5fd65077..200ab49f 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active1.png b/packages/pinball_components/test/src/components/golden/signpost/active1.png index f11af5a8..0e0f9e79 100644 Binary files a/packages/pinball_components/test/src/components/golden/signpost/active1.png and b/packages/pinball_components/test/src/components/golden/signpost/active1.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active2.png b/packages/pinball_components/test/src/components/golden/signpost/active2.png index 6ddf8786..9dfae564 100644 Binary files a/packages/pinball_components/test/src/components/golden/signpost/active2.png and b/packages/pinball_components/test/src/components/golden/signpost/active2.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active3.png b/packages/pinball_components/test/src/components/golden/signpost/active3.png index 5e9b0005..a99c9e48 100644 Binary files a/packages/pinball_components/test/src/components/golden/signpost/active3.png and b/packages/pinball_components/test/src/components/golden/signpost/active3.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/inactive.png b/packages/pinball_components/test/src/components/golden/signpost/inactive.png index 7ed00fba..7f089716 100644 Binary files a/packages/pinball_components/test/src/components/golden/signpost/inactive.png and b/packages/pinball_components/test/src/components/golden/signpost/inactive.png differ diff --git a/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart index 12c596e6..6a6fd437 100644 --- a/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart @@ -1,6 +1,7 @@ // 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'; @@ -9,6 +10,12 @@ import 'package:pinball_components/src/components/google_letter/behaviors/behavi import '../../../../helpers/helpers.dart'; +class _MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {} + +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -27,7 +34,7 @@ void main() { 'beginContact emits onBallContacted when contacts with a ball', (game) async { final behavior = GoogleLetterBallContactBehavior(); - final bloc = MockGoogleLetterCubit(); + final bloc = _MockGoogleLetterCubit(); whenListen( bloc, const Stream.empty(), @@ -38,7 +45,7 @@ void main() { await googleLetter.add(behavior); await game.ensureAdd(googleLetter); - behavior.beginContact(MockBall(), MockContact()); + behavior.beginContact(_MockBall(), _MockContact()); verify(googleLetter.bloc.onBallContacted).called(1); }, diff --git a/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart index be1f9743..afd4c130 100644 --- a/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart +++ b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart @@ -10,6 +10,8 @@ import 'package:pinball_components/src/components/google_letter/behaviors/behavi import '../../../helpers/helpers.dart'; +class _MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -104,7 +106,7 @@ void main() { // https://github.com/flame-engine/flame/pull/1538 // ignore: public_member_api_docs flameTester.test('closes bloc when removed', (game) async { - final bloc = MockGoogleLetterCubit(); + final bloc = _MockGoogleLetterCubit(); whenListen( bloc, const Stream.empty(), diff --git a/packages/pinball_components/test/src/components/kicker/behaviors/kicker_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/kicker/behaviors/kicker_ball_contact_behavior_test.dart index 6c04cdcb..6fa6d4a7 100644 --- a/packages/pinball_components/test/src/components/kicker/behaviors/kicker_ball_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/kicker/behaviors/kicker_ball_contact_behavior_test.dart @@ -1,6 +1,7 @@ // 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'; @@ -11,6 +12,10 @@ import '../../../../helpers/helpers.dart'; class _MockKickerCubit extends Mock implements KickerCubit {} +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -43,7 +48,7 @@ void main() { await kicker.add(behavior); await game.ensureAdd(kicker); - behavior.beginContact(MockBall(), MockContact()); + behavior.beginContact(_MockBall(), _MockContact()); verify(kicker.bloc.onBallContacted).called(1); }, diff --git a/packages/pinball_components/test/src/components/layer_sensor_test.dart b/packages/pinball_components/test/src/components/layer_sensor_test.dart index 9103a966..cfd19bb0 100644 --- a/packages/pinball_components/test/src/components/layer_sensor_test.dart +++ b/packages/pinball_components/test/src/components/layer_sensor_test.dart @@ -7,6 +7,12 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; +class _MockBall extends Mock implements Ball {} + +class _MockBody extends Mock implements Body {} + +class _MockContact extends Mock implements Contact {} + class TestLayerSensor extends LayerSensor { TestLayerSensor({ required LayerEntranceOrientation orientation, @@ -115,8 +121,8 @@ void main() { late Layer insideLayer; setUp(() { - ball = MockBall(); - body = MockBody(); + ball = _MockBall(); + body = _MockBody(); insideZIndex = 1; insideLayer = Layer.spaceshipEntranceRamp; @@ -136,13 +142,13 @@ void main() { when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); - sensor.beginContact(ball, MockContact()); + sensor.beginContact(ball, _MockContact()); verify(() => ball.layer = insideLayer).called(1); verify(() => ball.zIndex = insideZIndex).called(1); when(() => ball.layer).thenReturn(insideLayer); - sensor.beginContact(ball, MockContact()); + sensor.beginContact(ball, _MockContact()); verify(() => ball.layer = Layer.board); verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); }); @@ -159,13 +165,13 @@ void main() { when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - sensor.beginContact(ball, MockContact()); + sensor.beginContact(ball, _MockContact()); verify(() => ball.layer = insideLayer).called(1); verify(() => ball.zIndex = insidePriority).called(1); when(() => ball.layer).thenReturn(insideLayer); - sensor.beginContact(ball, MockContact()); + sensor.beginContact(ball, _MockContact()); verify(() => ball.layer = Layer.board); verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); }); diff --git a/packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart new file mode 100644 index 00000000..379f8610 --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/behaviors/multiball_blinking_behavior_test.dart @@ -0,0 +1,160 @@ +// ignore_for_file: prefer_const_constructors, cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockMultiballCubit extends Mock implements MultiballCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'MultiballBlinkingBehavior', + () { + flameTester.testGameWidget( + 'calls onBlink every 0.1 seconds when animation state is animated', + setUp: (game, tester) async { + final behavior = MultiballBlinkingBehavior(); + final bloc = _MockMultiballCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: MultiballState.initial(), + ); + + final multiball = Multiball.test(bloc: bloc); + await multiball.add(behavior); + await game.ensureAdd(multiball); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.lit, + ), + ); + await tester.pump(); + game.update(0); + + verify(bloc.onBlink).called(1); + + await tester.pump(); + game.update(0.1); + + await streamController.close(); + verify(bloc.onBlink).called(1); + }, + ); + + flameTester.testGameWidget( + 'calls onStop when animation state is stopped', + setUp: (game, tester) async { + final behavior = MultiballBlinkingBehavior(); + final bloc = _MockMultiballCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: MultiballState.initial(), + ); + when(bloc.onBlink).thenAnswer((_) async {}); + + final multiball = Multiball.test(bloc: bloc); + await multiball.add(behavior); + await game.ensureAdd(multiball); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.lit, + ), + ); + await tester.pump(); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.lit, + ), + ); + + await streamController.close(); + verify(bloc.onStop).called(1); + }, + ); + + flameTester.testGameWidget( + 'onTick stops when there is no animation', + setUp: (game, tester) async { + final behavior = MultiballBlinkingBehavior(); + final bloc = _MockMultiballCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: MultiballState.initial(), + ); + when(bloc.onBlink).thenAnswer((_) async {}); + + final multiball = Multiball.test(bloc: bloc); + await multiball.add(behavior); + await game.ensureAdd(multiball); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.lit, + ), + ); + await tester.pump(); + + behavior.onTick(); + + expect(behavior.timer.isRunning(), false); + }, + ); + + flameTester.testGameWidget( + 'onTick stops after 10 blinks repetitions', + setUp: (game, tester) async { + final behavior = MultiballBlinkingBehavior(); + final bloc = _MockMultiballCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: MultiballState.initial(), + ); + when(bloc.onBlink).thenAnswer((_) async {}); + + final multiball = Multiball.test(bloc: bloc); + await multiball.add(behavior); + await game.ensureAdd(multiball); + + streamController.add( + MultiballState( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.dimmed, + ), + ); + await tester.pump(); + + for (var i = 0; i < 10; i++) { + behavior.onTick(); + } + + expect(behavior.timer.isRunning(), false); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart b/packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart new file mode 100644 index 00000000..2fcb5ccc --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/cubit/multiball_cubit_test.dart @@ -0,0 +1,67 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'MultiballCubit', + () { + blocTest( + 'onAnimate emits animationState [animate]', + build: MultiballCubit.new, + act: (bloc) => bloc.onAnimate(), + expect: () => [ + isA() + ..having( + (state) => state.animationState, + 'animationState', + MultiballAnimationState.blinking, + ) + ], + ); + + blocTest( + 'onStop emits animationState [stopped]', + build: MultiballCubit.new, + act: (bloc) => bloc.onStop(), + expect: () => [ + isA() + ..having( + (state) => state.animationState, + 'animationState', + MultiballAnimationState.idle, + ) + ], + ); + + blocTest( + 'onBlink emits lightState [lit, dimmed, lit]', + build: MultiballCubit.new, + act: (bloc) => bloc + ..onBlink() + ..onBlink() + ..onBlink(), + expect: () => [ + isA() + ..having( + (state) => state.lightState, + 'lightState', + MultiballLightState.lit, + ), + isA() + ..having( + (state) => state.lightState, + 'lightState', + MultiballLightState.dimmed, + ), + isA() + ..having( + (state) => state.lightState, + 'lightState', + MultiballLightState.lit, + ) + ], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart b/packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart new file mode 100644 index 00000000..69789be9 --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/cubit/multiball_state_test.dart @@ -0,0 +1,76 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/pinball_components.dart'; + +void main() { + group('MultiballState', () { + test('supports value equality', () { + expect( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ), + equals( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ), + isNotNull, + ); + }); + }); + + group('copyWith', () { + test( + 'copies correctly ' + 'when no argument specified', + () { + final multiballState = MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ); + expect( + multiballState.copyWith(), + equals(multiballState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + final multiballState = MultiballState( + animationState: MultiballAnimationState.idle, + lightState: MultiballLightState.dimmed, + ); + final otherMultiballState = MultiballState( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.lit, + ); + expect(multiballState, isNot(equals(otherMultiballState))); + + expect( + multiballState.copyWith( + animationState: MultiballAnimationState.blinking, + lightState: MultiballLightState.lit, + ), + equals(otherMultiballState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/multiball/multiball_test.dart b/packages/pinball_components/test/src/components/multiball/multiball_test.dart new file mode 100644 index 00000000..26dcf8a8 --- /dev/null +++ b/packages/pinball_components/test/src/components/multiball/multiball_test.dart @@ -0,0 +1,92 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/multiball/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +class _MockMultiballCubit extends Mock implements MultiballCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('Multiball', () { + group('loads correctly', () { + flameTester.test('"a"', (game) async { + final multiball = Multiball.a(); + await game.ensureAdd(multiball); + + expect(game.contains(multiball), isTrue); + }); + + flameTester.test('"b"', (game) async { + final multiball = Multiball.b(); + await game.ensureAdd(multiball); + expect(game.contains(multiball), isTrue); + }); + + flameTester.test('"c"', (game) async { + final multiball = Multiball.c(); + await game.ensureAdd(multiball); + + expect(game.contains(multiball), isTrue); + }); + + flameTester.test('"d"', (game) async { + final multiball = Multiball.d(); + await game.ensureAdd(multiball); + expect(game.contains(multiball), isTrue); + }); + }); + + flameTester.test( + 'closes bloc when removed', + (game) async { + final bloc = _MockMultiballCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: MultiballLightState.dimmed, + ); + when(bloc.close).thenAnswer((_) async {}); + final multiball = Multiball.test(bloc: bloc); + + await game.ensureAdd(multiball); + game.remove(multiball); + await game.ready(); + + verify(bloc.close).called(1); + }, + ); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final multiball = Multiball.a( + children: [component], + ); + await game.ensureAdd(multiball); + expect(multiball.children, contains(component)); + }); + + flameTester.test('a MultiballBlinkingBehavior', (game) async { + final multiball = Multiball.a(); + await game.ensureAdd(multiball); + expect( + multiball.children.whereType().single, + isNotNull, + ); + }); + }); + }); +} 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 edc2735f..deb69a44 100644 --- a/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart +++ b/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart @@ -9,9 +9,9 @@ import 'package:pinball_components/pinball_components.dart'; import '../../../helpers/helpers.dart'; -void main() { - final bloc = MockMultiplierCubit(); +class _MockMultiplierCubit extends Mock implements MultiplierCubit {} +void main() { group('Multiplier', () { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -27,6 +27,11 @@ void main() { Assets.images.multiplier.x6.dimmed.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); + late MultiplierCubit bloc; + + setUp(() { + bloc = _MockMultiplierCubit(); + }); flameTester.test('"x2" loads correctly', (game) async { final multiplier = Multiplier.x2( diff --git a/packages/pinball_components/test/src/components/plunger_test.dart b/packages/pinball_components/test/src/components/plunger_test.dart index eafc15d5..abb42d68 100644 --- a/packages/pinball_components/test/src/components/plunger_test.dart +++ b/packages/pinball_components/test/src/components/plunger_test.dart @@ -121,6 +121,33 @@ void main() { ); }); + group('pullFor', () { + late Plunger plunger; + + setUp(() { + plunger = Plunger( + compressionDistance: compressionDistance, + ); + }); + + flameTester.testGameWidget( + 'moves downwards for given period when pullFor is called', + setUp: (game, tester) async { + await game.ensureAdd(plunger); + }, + verify: (game, tester) async { + plunger.pullFor(2); + game.update(0); + + expect(plunger.body.linearVelocity.y, isPositive); + + await tester.pump(const Duration(seconds: 2)); + + expect(plunger.body.linearVelocity.y, isZero); + }, + ); + }); + group('pull', () { late Plunger plunger; diff --git a/packages/pinball_components/test/src/components/signpost/cubit/signpost_cubit_test.dart b/packages/pinball_components/test/src/components/signpost/cubit/signpost_cubit_test.dart new file mode 100644 index 00000000..081beab2 --- /dev/null +++ b/packages/pinball_components/test/src/components/signpost/cubit/signpost_cubit_test.dart @@ -0,0 +1,39 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('SignpostCubit', () { + blocTest( + 'onProgressed progresses', + build: SignpostCubit.new, + act: (bloc) { + bloc + ..onProgressed() + ..onProgressed() + ..onProgressed() + ..onProgressed(); + }, + expect: () => [ + SignpostState.active1, + SignpostState.active2, + SignpostState.active3, + SignpostState.inactive, + ], + ); + + test('isFullyProgressed when on active3', () { + final bloc = SignpostCubit(); + expect(bloc.isFullyProgressed(), isFalse); + + bloc.onProgressed(); + expect(bloc.isFullyProgressed(), isFalse); + + bloc.onProgressed(); + expect(bloc.isFullyProgressed(), isFalse); + + bloc.onProgressed(); + expect(bloc.isFullyProgressed(), isTrue); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/signpost_test.dart b/packages/pinball_components/test/src/components/signpost/signpost_test.dart similarity index 64% rename from packages/pinball_components/test/src/components/signpost_test.dart rename to packages/pinball_components/test/src/components/signpost/signpost_test.dart index 23aa6bd0..6aecd0bd 100644 --- a/packages/pinball_components/test/src/components/signpost_test.dart +++ b/packages/pinball_components/test/src/components/signpost/signpost_test.dart @@ -1,11 +1,15 @@ // ignore_for_file: cascade_invocations +import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; + +class _MockSignpostCubit extends Mock implements SignpostCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -18,13 +22,13 @@ void main() { final flameTester = FlameTester(() => TestGame(assets)); group('Signpost', () { + const goldenPath = '../golden/signpost/'; + flameTester.test( 'loads correctly', (game) async { final signpost = Signpost(); - await game.ready(); await game.ensureAdd(signpost); - expect(game.contains(signpost), isTrue); }, ); @@ -39,8 +43,8 @@ void main() { await tester.pump(); expect( - signpost.firstChild()!.current, - SignpostSpriteState.inactive, + signpost.bloc.state, + equals(SignpostState.inactive), ); game.camera.followVector2(Vector2.zero()); @@ -48,7 +52,7 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/signpost/inactive.png'), + matchesGoldenFile('${goldenPath}inactive.png'), ); }, ); @@ -59,12 +63,12 @@ void main() { await game.images.loadAll(assets); final signpost = Signpost(); await game.ensureAdd(signpost); - signpost.progress(); + signpost.bloc.onProgressed(); await tester.pump(); expect( - signpost.firstChild()!.current, - SignpostSpriteState.active1, + signpost.bloc.state, + equals(SignpostState.active1), ); game.camera.followVector2(Vector2.zero()); @@ -72,7 +76,7 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/signpost/active1.png'), + matchesGoldenFile('${goldenPath}active1.png'), ); }, ); @@ -83,14 +87,14 @@ void main() { await game.images.loadAll(assets); final signpost = Signpost(); await game.ensureAdd(signpost); - signpost - ..progress() - ..progress(); + signpost.bloc + ..onProgressed() + ..onProgressed(); await tester.pump(); expect( - signpost.firstChild()!.current, - SignpostSpriteState.active2, + signpost.bloc.state, + equals(SignpostState.active2), ); game.camera.followVector2(Vector2.zero()); @@ -98,7 +102,7 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/signpost/active2.png'), + matchesGoldenFile('${goldenPath}active2.png'), ); }, ); @@ -109,15 +113,16 @@ void main() { await game.images.loadAll(assets); final signpost = Signpost(); await game.ensureAdd(signpost); - signpost - ..progress() - ..progress() - ..progress(); + + signpost.bloc + ..onProgressed() + ..onProgressed() + ..onProgressed(); await tester.pump(); expect( - signpost.firstChild()!.current, - SignpostSpriteState.active3, + signpost.bloc.state, + equals(SignpostState.active3), ); game.camera.followVector2(Vector2.zero()); @@ -125,33 +130,12 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/signpost/active3.png'), + matchesGoldenFile('${goldenPath}active3.png'), ); }, ); }); - flameTester.test( - 'progress correctly cycles through all sprites', - (game) async { - final signpost = Signpost(); - await game.ready(); - await game.ensureAdd(signpost); - - final spriteComponent = signpost.firstChild()!; - - expect(spriteComponent.current, SignpostSpriteState.inactive); - signpost.progress(); - expect(spriteComponent.current, SignpostSpriteState.active1); - signpost.progress(); - expect(spriteComponent.current, SignpostSpriteState.active2); - signpost.progress(); - expect(spriteComponent.current, SignpostSpriteState.active3); - signpost.progress(); - expect(spriteComponent.current, SignpostSpriteState.inactive); - }, - ); - flameTester.test('adds new children', (game) async { final component = Component(); final signpost = Signpost( @@ -160,5 +144,22 @@ void main() { await game.ensureAdd(signpost); expect(signpost.children, contains(component)); }); + + flameTester.test('closes bloc when removed', (game) async { + final bloc = _MockSignpostCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SignpostState.inactive, + ); + when(bloc.close).thenAnswer((_) async {}); + final component = Signpost.test(bloc: bloc); + + await game.ensureAdd(component); + game.remove(component); + await game.ready(); + + verify(bloc.close).called(1); + }); }); } diff --git a/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart index f67b28d7..1d10d0aa 100644 --- a/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart @@ -1,6 +1,7 @@ // 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'; @@ -9,6 +10,12 @@ import 'package:pinball_components/src/components/sparky_bumper/behaviors/behavi import '../../../../helpers/helpers.dart'; +class _MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} + +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -27,7 +34,7 @@ void main() { 'beginContact emits onBallContacted when contacts with a ball', (game) async { final behavior = SparkyBumperBallContactBehavior(); - final bloc = MockSparkyBumperCubit(); + final bloc = _MockSparkyBumperCubit(); whenListen( bloc, const Stream.empty(), @@ -38,7 +45,7 @@ void main() { await sparkyBumper.add(behavior); await game.ensureAdd(sparkyBumper); - behavior.beginContact(MockBall(), MockContact()); + behavior.beginContact(_MockBall(), _MockContact()); verify(sparkyBumper.bloc.onBallContacted).called(1); }, diff --git a/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart index 2210754f..e48998ed 100644 --- a/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart +++ b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart @@ -9,6 +9,8 @@ import 'package:pinball_components/src/components/sparky_bumper/behaviors/behavi import '../../../../helpers/helpers.dart'; +class _MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -20,7 +22,7 @@ void main() { 'calls onBlinked after 0.05 seconds when dimmed', setUp: (game, tester) async { final behavior = SparkyBumperBlinkingBehavior(); - final bloc = MockSparkyBumperCubit(); + final bloc = _MockSparkyBumperCubit(); final streamController = StreamController(); whenListen( bloc, diff --git a/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart index 709b3dc5..7544fdd2 100644 --- a/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart +++ b/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart @@ -11,6 +11,8 @@ import 'package:pinball_components/src/components/sparky_bumper/behaviors/behavi import '../../../helpers/helpers.dart'; +class _MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -46,7 +48,7 @@ void main() { // https://github.com/flame-engine/flame/pull/1538 // ignore: public_member_api_docs flameTester.test('closes bloc when removed', (game) async { - final bloc = MockSparkyBumperCubit(); + final bloc = _MockSparkyBumperCubit(); whenListen( bloc, const Stream.empty(), diff --git a/packages/pinball_flame/test/helpers/helpers.dart b/packages/pinball_flame/test/helpers/helpers.dart deleted file mode 100644 index efe914f6..00000000 --- a/packages/pinball_flame/test/helpers/helpers.dart +++ /dev/null @@ -1 +0,0 @@ -export 'mocks.dart'; diff --git a/packages/pinball_flame/test/helpers/mocks.dart b/packages/pinball_flame/test/helpers/mocks.dart deleted file mode 100644 index 1c5042ff..00000000 --- a/packages/pinball_flame/test/helpers/mocks.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockForge2DGame extends Mock implements Forge2DGame {} - -class MockComponent extends Mock implements Component {} diff --git a/packages/pinball_flame/test/src/keyboard_input_controller_test.dart b/packages/pinball_flame/test/src/keyboard_input_controller_test.dart index 99a0006b..7b554e8c 100644 --- a/packages/pinball_flame/test/src/keyboard_input_controller_test.dart +++ b/packages/pinball_flame/test/src/keyboard_input_controller_test.dart @@ -6,13 +6,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_flame/pinball_flame.dart'; -abstract class _KeyCallStub { +abstract class _KeyCall { bool onCall(); } -class KeyCallStub extends Mock implements _KeyCallStub {} +class _MockKeyCall extends Mock implements _KeyCall {} -class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { +class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { return super.toString(); @@ -20,7 +20,7 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { } RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) { - final event = MockRawKeyUpEvent(); + final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn(key); return event; } @@ -28,7 +28,7 @@ RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) { void main() { group('KeyboardInputController', () { test('calls registered handlers', () { - final stub = KeyCallStub(); + final stub = _MockKeyCall(); when(stub.onCall).thenReturn(true); final input = KeyboardInputController( @@ -44,7 +44,7 @@ void main() { test( 'returns false the handler return value', () { - final stub = KeyCallStub(); + final stub = _MockKeyCall(); when(stub.onCall).thenReturn(false); final input = KeyboardInputController( @@ -63,7 +63,7 @@ void main() { test( 'returns true (allowing event to bubble) when no handler is registered', () { - final stub = KeyCallStub(); + final stub = _MockKeyCall(); when(stub.onCall).thenReturn(true); final input = KeyboardInputController(); diff --git a/packages/pinball_flame/test/src/sprite_animation_test.dart b/packages/pinball_flame/test/src/sprite_animation_test.dart index e3b287de..dc37d983 100644 --- a/packages/pinball_flame/test/src/sprite_animation_test.dart +++ b/packages/pinball_flame/test/src/sprite_animation_test.dart @@ -3,12 +3,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_flame/pinball_flame.dart'; -class MockSpriteAnimationController extends Mock +class _MockSpriteAnimationController extends Mock implements SpriteAnimationController {} -class MockSpriteAnimation extends Mock implements SpriteAnimation {} +class _MockSpriteAnimation extends Mock implements SpriteAnimation {} -class MockSprite extends Mock implements Sprite {} +class _MockSprite extends Mock implements Sprite {} // TODO(arturplaczek): Remove when this PR will be merged. // https://github.com/flame-engine/flame/pull/1552 @@ -20,9 +20,9 @@ void main() { late Sprite sprite; setUp(() { - controller = MockSpriteAnimationController(); - animation = MockSpriteAnimation(); - sprite = MockSprite(); + controller = _MockSpriteAnimationController(); + animation = _MockSpriteAnimation(); + sprite = _MockSprite(); when(() => controller.animation).thenAnswer((_) => animation); diff --git a/packages/pinball_ui/lib/src/widgets/pinball_button.dart b/packages/pinball_ui/lib/src/widgets/pinball_button.dart index 585a8d54..dd4685c1 100644 --- a/packages/pinball_ui/lib/src/widgets/pinball_button.dart +++ b/packages/pinball_ui/lib/src/widgets/pinball_button.dart @@ -21,26 +21,29 @@ class PinballButton extends StatelessWidget { @override Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(Assets.images.button.pinballButton.keyName), + return Material( + color: PinballColors.transparent, + child: DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(Assets.images.button.pinballButton.keyName), + ), ), - ), - child: Center( - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - child: Text( - text, - style: Theme.of(context) - .textTheme - .headline3! - .copyWith(color: PinballColors.white), + child: Center( + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: Text( + text, + style: Theme.of(context) + .textTheme + .headline3! + .copyWith(color: PinballColors.white), + ), ), ), ), diff --git a/packages/pinball_ui/test/src/external_links/external_links_test.dart b/packages/pinball_ui/test/src/external_links/external_links_test.dart index 83cc2d63..a1b11a26 100644 --- a/packages/pinball_ui/test/src/external_links/external_links_test.dart +++ b/packages/pinball_ui/test/src/external_links/external_links_test.dart @@ -3,7 +3,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:pinball_ui/pinball_ui.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -class MockUrlLauncher extends Mock +class _MockUrlLauncher extends Mock with MockPlatformInterfaceMixin implements UrlLauncherPlatform {} @@ -11,7 +11,7 @@ void main() { late UrlLauncherPlatform urlLauncher; setUp(() { - urlLauncher = MockUrlLauncher(); + urlLauncher = _MockUrlLauncher(); UrlLauncherPlatform.instance = urlLauncher; }); diff --git a/storage.rules b/storage.rules new file mode 100644 index 00000000..03ab51c6 --- /dev/null +++ b/storage.rules @@ -0,0 +1,9 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{folder}/{imageId} { + allow read: if imageId.matches(".*\\.png") || imageId.matches(".*\\.jpg"); + allow write: if false; + } + } +} \ No newline at end of file diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 83e37499..5ba5aca7 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -13,7 +13,13 @@ import 'package:pinball/app/app.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_audio/pinball_audio.dart'; -import '../../helpers/mocks.dart'; +class _MockAuthenticationRepository extends Mock + implements AuthenticationRepository {} + +class _MockPinballAudio extends Mock implements PinballAudio {} + +class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { +} void main() { group('App', () { @@ -22,9 +28,9 @@ void main() { late PinballAudio pinballAudio; setUp(() { - authenticationRepository = MockAuthenticationRepository(); - leaderboardRepository = MockLeaderboardRepository(); - pinballAudio = MockPinballAudio(); + authenticationRepository = _MockAuthenticationRepository(); + leaderboardRepository = _MockLeaderboardRepository(); + pinballAudio = _MockPinballAudio(); when(pinballAudio.load).thenAnswer((_) => Future.value()); }); diff --git a/test/footer/footer_test.dart b/test/footer/footer_test.dart index c18d76e7..f8f69259 100644 --- a/test/footer/footer_test.dart +++ b/test/footer/footer_test.dart @@ -3,15 +3,20 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/footer/footer.dart'; import 'package:pinball_ui/pinball_ui.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import '../helpers/helpers.dart'; +class _MockUrlLauncher extends Mock + with MockPlatformInterfaceMixin + implements UrlLauncherPlatform {} + void main() { group('Footer', () { late UrlLauncherPlatform urlLauncher; setUp(() async { - urlLauncher = MockUrlLauncher(); + urlLauncher = _MockUrlLauncher(); UrlLauncherPlatform.instance = urlLauncher; }); testWidgets('renders "Made with..." and "Google I/O"', (tester) async { diff --git a/test/game/components/android_acres_test.dart b/test/game/components/android_acres/android_acres_test.dart similarity index 79% rename from test/game/components/android_acres_test.dart rename to test/game/components/android_acres/android_acres_test.dart index 4c5e4cb7..73025551 100644 --- a/test/game/components/android_acres_test.dart +++ b/test/game/components/android_acres/android_acres_test.dart @@ -2,10 +2,11 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -45,7 +46,7 @@ void main() { group('loads', () { flameTester.test( - 'a Spaceship', + 'an AndroidSpaceship', (game) async { await game.ensureAdd(AndroidAcres()); expect( @@ -55,6 +56,17 @@ void main() { }, ); + flameTester.test( + 'an AndroidAnimatronic', + (game) async { + await game.ensureAdd(AndroidAcres()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameTester.test( 'a SpaceshipRamp', (game) async { @@ -88,5 +100,14 @@ void main() { }, ); }); + + flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async { + final androidAcres = AndroidAcres(); + await game.ensureAdd(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 new file mode 100644 index 00000000..6be120d5 --- /dev/null +++ b/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart @@ -0,0 +1,81 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +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, + ); + + flameBlocTester.testGameWidget( + 'adds GameBonus.androidSpaceship to the game ' + 'when android spacehship has a bonus', + setUp: (game, tester) async { + final behavior = AndroidSpaceshipBonusBehavior(); + final parent = AndroidAcres.test(); + final androidSpaceship = AndroidSpaceship(position: Vector2.zero()); + + await parent.add(androidSpaceship); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + androidSpaceship.bloc.onBallEntered(); + await tester.pump(); + + verify( + () => gameBloc.add(const BonusActivated(GameBonus.androidSpaceship)), + ).called(1); + }, + ); + }); +} diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index 13852a99..d689f799 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -13,8 +13,8 @@ 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); +class _WrappedBallController extends BallController { + _WrappedBallController(Ball ball, this._gameRef) : super(ball); final PinballGame _gameRef; @@ -22,6 +22,14 @@ class WrappedBallController extends BallController { PinballGame get gameRef => _gameRef; } +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 = [ @@ -35,7 +43,7 @@ void main() { setUp(() { ball = Ball(); - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -51,7 +59,7 @@ void main() { test('can be instantiated', () { expect( - BallController(MockBall()), + BallController(_MockBall()), isA(), ); }); @@ -117,9 +125,9 @@ void main() { flameBlocTester.test( 'initially stops the ball', (game) async { - final gameRef = MockPinballGame(); - final ball = MockControlledBall(); - final controller = WrappedBallController(ball, gameRef); + final gameRef = _MockPinballGame(); + final ball = _MockControlledBall(); + final controller = _WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); when(() => ball.boost(any())).thenAnswer((_) async {}); @@ -133,9 +141,9 @@ void main() { flameBlocTester.test( 'resumes the ball', (game) async { - final gameRef = MockPinballGame(); - final ball = MockControlledBall(); - final controller = WrappedBallController(ball, gameRef); + final gameRef = _MockPinballGame(); + final ball = _MockControlledBall(); + final controller = _WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); when(() => ball.boost(any())).thenAnswer((_) async {}); @@ -149,9 +157,9 @@ void main() { flameBlocTester.test( 'boosts the ball', (game) async { - final gameRef = MockPinballGame(); - final ball = MockControlledBall(); - final controller = WrappedBallController(ball, gameRef); + final gameRef = _MockPinballGame(); + final ball = _MockControlledBall(); + final controller = _WrappedBallController(ball, gameRef); when(() => gameRef.read()).thenReturn(gameBloc); when(() => ball.controller).thenReturn(controller); when(() => ball.boost(any())).thenAnswer((_) async {}); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index 36a8161b..e8b7aaf3 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -4,11 +4,14 @@ import 'package:bloc_test/bloc_test.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_components/pinball_components.dart'; import '../../helpers/helpers.dart'; +class _MockGameBloc extends Mock implements GameBloc {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -22,7 +25,7 @@ void main() { final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () { - final bloc = MockGameBloc(); + final bloc = _MockGameBloc(); const state = GameState( score: 0, multiplier: 1, diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index a39bdef6..c832e24a 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -5,11 +5,14 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; +class _MockGameBloc extends Mock implements GameBloc {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(EmptyPinballTestGame.new); @@ -17,7 +20,7 @@ void main() { final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () { - final bloc = MockGameBloc(); + final bloc = _MockGameBloc(); const state = GameState( score: 0, multiplier: 1, 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 new file mode 100644 index 00000000..22b6313b --- /dev/null +++ b/test/game/components/dino_desert/behaviors/chrome_dino_bonus_behavior_test.dart @@ -0,0 +1,67 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/dino_desert/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockGameBloc extends Mock implements GameBloc {} + +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, + ); + + flameBlocTester.testGameWidget( + 'adds GameBonus.dinoChomp to the game ' + 'when ChromeDinoStatus.chomping is emitted', + setUp: (game, tester) async { + final behavior = ChromeDinoBonusBehavior(); + final parent = DinoDesert.test(); + final chromeDino = ChromeDino(); + + await parent.add(chromeDino); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + chromeDino.bloc.onChomp(_MockBall()); + await tester.pump(); + + verify( + () => gameBloc.add(const BonusActivated(GameBonus.dinoChomp)), + ).called(1); + }, + ); + }); +} diff --git a/test/game/components/dino_desert_test.dart b/test/game/components/dino_desert/dino_desert_test.dart similarity index 70% rename from test/game/components/dino_desert_test.dart rename to test/game/components/dino_desert/dino_desert_test.dart index 35a2d25b..262ddc2d 100644 --- a/test/game/components/dino_desert_test.dart +++ b/test/game/components/dino_desert/dino_desert_test.dart @@ -2,10 +2,11 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.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'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -63,17 +64,28 @@ void main() { ); }); - flameTester.test( - 'adds ScoringBehavior to ChromeDino', - (game) async { - await game.ensureAdd(DinoDesert()); + group('adds', () { + flameTester.test( + 'ScoringBehavior to ChromeDino', + (game) async { + await game.ensureAdd(DinoDesert()); - final chromeDino = game.descendants().whereType().single; + final chromeDino = game.descendants().whereType().single; + expect( + chromeDino.firstChild(), + isNotNull, + ); + }, + ); + + flameTester.test('a ChromeDinoBonusBehavior', (game) async { + final dinoDesert = DinoDesert(); + await game.ensureAdd(dinoDesert); expect( - chromeDino.firstChild(), + dinoDesert.children.whereType().single, isNotNull, ); - }, - ); + }); + }); }); } diff --git a/test/game/components/drain_test.dart b/test/game/components/drain_test.dart index f1875a56..984abce3 100644 --- a/test/game/components/drain_test.dart +++ b/test/game/components/drain_test.dart @@ -8,6 +8,12 @@ import 'package:pinball/game/game.dart'; import '../../helpers/helpers.dart'; +class _MockControlledBall extends Mock implements ControlledBall {} + +class _MockBallController extends Mock implements BallController {} + +class _MockContact extends Mock implements Contact {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -18,7 +24,6 @@ void main() { (game) async { final drain = Drain(); await game.ensureAdd(drain); - expect(game.contains(drain), isTrue); }, ); @@ -28,7 +33,6 @@ void main() { (game) async { final drain = Drain(); await game.ensureAdd(drain); - expect(drain.body.bodyType, equals(BodyType.static)); }, ); @@ -38,7 +42,6 @@ void main() { (game) async { final drain = Drain(); await game.ensureAdd(drain); - expect(drain.body.fixtures.first.isSensor, isTrue); }, ); @@ -47,11 +50,11 @@ void main() { 'calls lost on contact with ball', () async { final drain = Drain(); - final ball = MockControlledBall(); - final controller = MockBallController(); + final ball = _MockControlledBall(); + final controller = _MockBallController(); when(() => ball.controller).thenReturn(controller); - drain.beginContact(ball, MockContact()); + drain.beginContact(ball, _MockContact()); verify(controller.lost).called(1); }, diff --git a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart index 995572c4..09e38482 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 @@ -14,6 +14,8 @@ import 'package:pinball_theme/pinball_theme.dart' as theme; import '../../../../helpers/helpers.dart'; +class _MockGameBloc extends Mock implements GameBloc {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -23,9 +25,10 @@ void main() { group('FlutterForestBonusBehavior', () { late GameBloc gameBloc; + final assets = [Assets.images.dash.animatronic.keyName]; setUp(() { - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -39,9 +42,14 @@ void main() { assets: assets, ); + void _contactedBumper(DashNestBumper bumper) => + bumper.bloc.onBallContacted(); + flameBlocTester.testGameWidget( - 'adds GameBonus.dashNest to the game when all bumpers are active', + 'adds GameBonus.dashNest to the game ' + 'when bumpers are activated three times', setUp: (game, tester) async { + await game.images.loadAll(assets); final behavior = FlutterForestBonusBehavior(); final parent = FlutterForest.test(); final bumpers = [ @@ -49,12 +57,18 @@ void main() { DashNestBumper.test(bloc: DashNestBumperCubit()), DashNestBumper.test(bloc: DashNestBumperCubit()), ]; + final animatronic = DashAnimatronic(); + final signpost = Signpost.test(bloc: SignpostCubit()); await game.ensureAdd(ZCanvasComponent(children: [parent])); - await parent.ensureAddAll([...bumpers, behavior]); + await parent.ensureAddAll([...bumpers, animatronic, signpost]); + await parent.ensureAdd(behavior); - for (final bumper in bumpers) { - bumper.bloc.onBallContacted(); - } + expect(game.descendants().whereType(), equals(bumpers)); + bumpers.forEach(_contactedBumper); + await tester.pump(); + bumpers.forEach(_contactedBumper); + await tester.pump(); + bumpers.forEach(_contactedBumper); await tester.pump(); verify( @@ -64,8 +78,10 @@ void main() { ); flameBlocTester.testGameWidget( - 'adds a new ball to the game when all bumpers are active', + 'adds a new Ball to the game ' + 'when bumpers are activated three times', setUp: (game, tester) async { + await game.images.loadAll(assets); final behavior = FlutterForestBonusBehavior(); final parent = FlutterForest.test(); final bumpers = [ @@ -73,18 +89,68 @@ void main() { DashNestBumper.test(bloc: DashNestBumperCubit()), DashNestBumper.test(bloc: DashNestBumperCubit()), ]; + final animatronic = DashAnimatronic(); + final signpost = Signpost.test(bloc: SignpostCubit()); await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAddAll([...bumpers, animatronic, signpost]); await parent.ensureAdd(behavior); - for (final bumper in bumpers) { - bumper.bloc.onBallContacted(); - } + expect(game.descendants().whereType(), equals(bumpers)); + bumpers.forEach(_contactedBumper); + await tester.pump(); + bumpers.forEach(_contactedBumper); + await tester.pump(); + bumpers.forEach(_contactedBumper); + await tester.pump(); + await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); - // expect( - // game.descendants().whereType().single, - // isNotNull, - // ); + flameBlocTester.testGameWidget( + 'progress the signpost ' + 'when bumpers are activated', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final behavior = FlutterForestBonusBehavior(); + final parent = FlutterForest.test(); + final bumpers = [ + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + ]; + final animatronic = DashAnimatronic(); + final signpost = Signpost.test(bloc: SignpostCubit()); + await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAddAll([...bumpers, animatronic, signpost]); + await parent.ensureAdd(behavior); + + expect(game.descendants().whereType(), equals(bumpers)); + + bumpers.forEach(_contactedBumper); + await tester.pump(); + expect( + signpost.bloc.state, + equals(SignpostState.active1), + ); + + bumpers.forEach(_contactedBumper); + await tester.pump(); + expect( + signpost.bloc.state, + equals(SignpostState.active2), + ); + + bumpers.forEach(_contactedBumper); + await tester.pump(); + expect( + signpost.bloc.state, + equals(SignpostState.inactive), + ); }, ); }); diff --git a/test/game/components/flutter_forest/flutter_forest_test.dart b/test/game/components/flutter_forest/flutter_forest_test.dart index 4f32e0f4..5761a9eb 100644 --- a/test/game/components/flutter_forest/flutter_forest_test.dart +++ b/test/game/components/flutter_forest/flutter_forest_test.dart @@ -4,6 +4,7 @@ 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 'package:pinball_flame/pinball_flame.dart'; import '../../../helpers/helpers.dart'; @@ -31,8 +32,8 @@ void main() { 'loads correctly', (game) async { final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - expect(game.contains(flutterForest), isTrue); + await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); + expect(game.descendants(), contains(flutterForest)); }, ); @@ -41,10 +42,9 @@ void main() { 'a Signpost', (game) async { final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - + await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); expect( - flutterForest.descendants().whereType().length, + game.descendants().whereType().length, equals(1), ); }, @@ -54,11 +54,10 @@ void main() { 'a DashAnimatronic', (game) async { final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - + await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); expect( - flutterForest.firstChild(), - isNotNull, + game.descendants().whereType().length, + equals(1), ); }, ); @@ -67,10 +66,9 @@ void main() { 'three DashNestBumper', (game) async { final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - + await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); expect( - flutterForest.descendants().whereType().length, + game.descendants().whereType().length, equals(3), ); }, diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index ef93892c..c85d0b52 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -4,10 +4,20 @@ import 'package:flame/game.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_theme/pinball_theme.dart'; -import '../../helpers/helpers.dart'; +class _MockPinballGame extends Mock implements PinballGame {} + +class _MockBackboard extends Mock implements Backboard {} + +class _MockCameraController extends Mock implements CameraController {} + +class _MockActiveOverlaysNotifier extends Mock + implements ActiveOverlaysNotifier {} + +class _MockPinballAudio extends Mock implements PinballAudio {} void main() { group('GameFlowController', () { @@ -22,7 +32,7 @@ void main() { final previous = GameState.initial(); expect( - GameFlowController(MockPinballGame()).listenWhen(previous, state), + GameFlowController(_MockPinballGame()).listenWhen(previous, state), isTrue, ); }); @@ -33,14 +43,16 @@ void main() { late Backboard backboard; late CameraController cameraController; late GameFlowController gameFlowController; + late PinballAudio pinballAudio; late ActiveOverlaysNotifier overlays; setUp(() { - game = MockPinballGame(); - backboard = MockBackboard(); - cameraController = MockCameraController(); + game = _MockPinballGame(); + backboard = _MockBackboard(); + cameraController = _MockCameraController(); gameFlowController = GameFlowController(game); - overlays = MockActiveOverlaysNotifier(); + overlays = _MockActiveOverlaysNotifier(); + pinballAudio = _MockPinballAudio(); when( () => backboard.gameOverMode( @@ -59,6 +71,7 @@ void main() { when(game.firstChild).thenReturn(cameraController); when(() => game.overlays).thenReturn(overlays); when(() => game.characterTheme).thenReturn(DashTheme()); + when(() => game.audio).thenReturn(pinballAudio); }); test( @@ -95,6 +108,15 @@ void main() { .called(1); }, ); + + test( + 'plays the background music on start', + () { + gameFlowController.onNewState(GameState.initial()); + + verify(pinballAudio.backgroundMusic).called(1); + }, + ); }); }); } diff --git a/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart b/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart index 305a0c1f..c9910fd7 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 @@ -10,6 +10,8 @@ import 'package:pinball_components/pinball_components.dart'; import '../../../../helpers/helpers.dart'; +class _MockGameBloc extends Mock implements GameBloc {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -31,7 +33,7 @@ void main() { late GameBloc gameBloc; setUp(() { - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); whenListen( gameBloc, const Stream.empty(), diff --git a/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart new file mode 100644 index 00000000..5f8b1400 --- /dev/null +++ b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart @@ -0,0 +1,140 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/multiballs/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockGameBloc extends Mock implements GameBloc {} + +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 flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + 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], + ); + + expect( + MultiballsBehavior().listenWhen(previous, state), + isTrue, + ); + }); + + test( + 'is false when the bonusHistory has changed ' + 'with a bonus different than GameBonus.dashNest', () { + final previous = + GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]); + final state = previous.copyWith( + bonusHistory: [...previous.bonusHistory, GameBonus.androidSpaceship], + ); + + expect( + MultiballsBehavior().listenWhen(previous, state), + isFalse, + ); + }); + + test('is false when the bonusHistory state is the same', () { + final previous = GameState.initial(); + final state = GameState( + score: 10, + multiplier: 1, + rounds: 0, + bonusHistory: const [], + ); + + expect( + MultiballsBehavior().listenWhen(previous, state), + isFalse, + ); + }); + }); + + group('onNewState', () { + flameBlocTester.testGameWidget( + "calls 'onAnimate' once for every multiball", + setUp: (game, tester) async { + final behavior = MultiballsBehavior(); + final parent = Multiballs.test(); + final multiballCubit = _MockMultiballCubit(); + final otherMultiballCubit = _MockMultiballCubit(); + final multiballs = [ + Multiball.test( + bloc: multiballCubit, + ), + Multiball.test( + bloc: otherMultiballCubit, + ), + ]; + + whenListen( + multiballCubit, + const Stream.empty(), + initialState: MultiballState.initial(), + ); + when(multiballCubit.onAnimate).thenAnswer((_) async {}); + + whenListen( + otherMultiballCubit, + const Stream.empty(), + initialState: MultiballState.initial(), + ); + when(otherMultiballCubit.onAnimate).thenAnswer((_) async {}); + + await parent.addAll(multiballs); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + await tester.pump(); + + behavior.onNewState( + GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]), + ); + + for (final multiball in multiballs) { + verify( + multiball.bloc.onAnimate, + ).called(1); + } + }, + ); + }); + }); +} diff --git a/test/game/components/multiballs/multiballs_test.dart b/test/game/components/multiballs/multiballs_test.dart new file mode 100644 index 00000000..c1a328b1 --- /dev/null +++ b/test/game/components/multiballs/multiballs_test.dart @@ -0,0 +1,54 @@ +// ignore_for_file: cascade_invocations + +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'; + +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, + ); + + group('Multiballs', () { + flameBlocTester.testGameWidget( + 'loads correctly', + setUp: (game, tester) async { + final multiballs = Multiballs(); + await game.ensureAdd(multiballs); + + expect(game.contains(multiballs), isTrue); + }, + ); + + group('loads', () { + flameBlocTester.testGameWidget( + 'four Multiball', + setUp: (game, tester) async { + final multiballs = Multiballs(); + await game.ensureAdd(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 c4f2bd33..40a952f1 100644 --- a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart +++ b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -12,6 +13,12 @@ import 'package:pinball_components/pinball_components.dart'; import '../../../../helpers/helpers.dart'; +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockComponent extends Mock implements Component {} + +class _MockMultiplierCubit extends Mock implements MultiplierCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -31,8 +38,8 @@ void main() { late GameBloc gameBloc; setUp(() { - registerFallbackValue(MockComponent()); - gameBloc = MockGameBloc(); + registerFallbackValue(_MockComponent()); + gameBloc = _MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -84,8 +91,8 @@ void main() { setUp: (game, tester) async { final behavior = MultipliersBehavior(); final parent = Multipliers.test(); - final multiplierX2Cubit = MockMultiplierCubit(); - final multiplierX3Cubit = MockMultiplierCubit(); + final multiplierX2Cubit = _MockMultiplierCubit(); + final multiplierX3Cubit = _MockMultiplierCubit(); final multipliers = [ Multiplier.test( value: MultiplierValue.x2, diff --git a/test/game/components/scoring_behavior_test.dart b/test/game/components/scoring_behavior_test.dart index 2afa862d..485183aa 100644 --- a/test/game/components/scoring_behavior_test.dart +++ b/test/game/components/scoring_behavior_test.dart @@ -17,6 +17,16 @@ class _TestBodyComponent extends BodyComponent { Body createBody() => world.createBody(BodyDef()); } +class _MockPinballAudio extends Mock implements PinballAudio {} + +class _MockBall extends Mock implements Ball {} + +class _MockBody extends Mock implements Body {} + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockContact extends Mock implements Contact {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -34,10 +44,9 @@ void main() { late BodyComponent parent; setUp(() { - audio = MockPinballAudio(); - - ball = MockBall(); - final ballBody = MockBody(); + audio = _MockPinballAudio(); + ball = _MockBall(); + final ballBody = _MockBody(); when(() => ball.body).thenReturn(ballBody); when(() => ballBody.position).thenReturn(Vector2.all(4)); @@ -49,7 +58,7 @@ void main() { audio: audio, ), blocBuilder: () { - bloc = MockGameBloc(); + bloc = _MockGameBloc(); const state = GameState( score: 0, multiplier: 1, @@ -71,7 +80,7 @@ void main() { final canvas = ZCanvasComponent(children: [parent]); await game.ensureAdd(canvas); - scoringBehavior.beginContact(ball, MockContact()); + scoringBehavior.beginContact(ball, _MockContact()); verify( () => bloc.add( @@ -89,7 +98,7 @@ void main() { final canvas = ZCanvasComponent(children: [parent]); await game.ensureAdd(canvas); - scoringBehavior.beginContact(ball, MockContact()); + scoringBehavior.beginContact(ball, _MockContact()); verify(audio.score).called(1); }, @@ -104,7 +113,7 @@ void main() { final canvas = ZCanvasComponent(children: [parent]); await game.ensureAdd(canvas); - scoringBehavior.beginContact(ball, MockContact()); + scoringBehavior.beginContact(ball, _MockContact()); await game.ready(); final scoreText = game.descendants().whereType(); diff --git a/test/game/components/sparky_scorch_test.dart b/test/game/components/sparky_scorch_test.dart index d331e340..7d9c8c77 100644 --- a/test/game/components/sparky_scorch_test.dart +++ b/test/game/components/sparky_scorch_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations +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'; @@ -8,6 +9,12 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; +class _MockControlledBall extends Mock implements ControlledBall {} + +class _MockBallController extends Mock implements BallController {} + +class _MockContact extends Mock implements Contact {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -73,8 +80,8 @@ void main() { group('SparkyComputerSensor', () { flameTester.test('calls turboCharge', (game) async { final sensor = SparkyComputerSensor(); - final ball = MockControlledBall(); - final controller = MockBallController(); + final ball = _MockControlledBall(); + final controller = _MockBallController(); when(() => ball.controller).thenReturn(controller); when(controller.turboCharge).thenAnswer((_) async {}); @@ -83,7 +90,7 @@ void main() { SparkyAnimatronic(), ]); - sensor.beginContact(ball, MockContact()); + sensor.beginContact(ball, _MockContact()); verify(() => ball.controller.turboCharge()).called(1); }); @@ -91,8 +98,8 @@ void main() { flameTester.test('plays SparkyAnimatronic', (game) async { final sensor = SparkyComputerSensor(); final sparkyAnimatronic = SparkyAnimatronic(); - final ball = MockControlledBall(); - final controller = MockBallController(); + final ball = _MockControlledBall(); + final controller = _MockBallController(); when(() => ball.controller).thenReturn(controller); when(controller.turboCharge).thenAnswer((_) async {}); await game.ensureAddAll([ @@ -101,7 +108,7 @@ void main() { ]); expect(sparkyAnimatronic.playing, isFalse); - sensor.beginContact(ball, MockContact()); + sensor.beginContact(ball, _MockContact()); expect(sparkyAnimatronic.playing, isTrue); }); }); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 9643b1da..7d2b9c68 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,8 +1,7 @@ // ignore_for_file: cascade_invocations import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; +import 'package:flame/input.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,6 +12,20 @@ import 'package:pinball_theme/pinball_theme.dart' as theme; 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 {} + +class _MockTapDownInfo extends Mock implements TapDownInfo {} + +class _MockTapUpDetails extends Mock implements TapUpDetails {} + +class _MockTapUpInfo extends Mock implements TapUpInfo {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -68,6 +81,8 @@ void main() { 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, @@ -117,7 +132,7 @@ void main() { late GameBloc gameBloc; setUp(() { - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -182,6 +197,18 @@ void main() { ); }); + flameBlocTester.test( + 'has only one Multiballs', + (game) async { + await game.ready(); + + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameBlocTester.test( 'one GoogleWord', (game) async { @@ -200,7 +227,7 @@ void main() { 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(); + final newState = _MockGameState(); when(() => newState.isGameOver).thenReturn(false); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), @@ -208,7 +235,7 @@ void main() { await game.ready(); expect( - game.controller.listenWhen(MockGameState(), newState), + game.controller.listenWhen(_MockGameState(), newState), isTrue, ); }, @@ -217,7 +244,7 @@ void main() { flameTester.test( "doesn't listen when some balls are left", (game) async { - final newState = MockGameState(); + final newState = _MockGameState(); when(() => newState.isGameOver).thenReturn(false); expect( @@ -225,7 +252,7 @@ void main() { greaterThan(0), ); expect( - game.controller.listenWhen(MockGameState(), newState), + game.controller.listenWhen(_MockGameState(), newState), isFalse, ); }, @@ -236,7 +263,7 @@ void main() { 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(); + final newState = _MockGameState(); when(() => newState.isGameOver).thenReturn(true); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), @@ -248,7 +275,7 @@ void main() { isTrue, ); expect( - game.controller.listenWhen(MockGameState(), newState), + game.controller.listenWhen(_MockGameState(), newState), isFalse, ); }, @@ -264,7 +291,7 @@ void main() { final previousBalls = game.descendants().whereType().toList(); - game.controller.onNewState(MockGameState()); + game.controller.onNewState(_MockGameState()); await game.ready(); final currentBalls = game.descendants().whereType().toList(); @@ -284,14 +311,14 @@ void main() { flameTester.test('tap down moves left flipper up', (game) async { await game.ready(); - final eventPosition = MockEventPosition(); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero()); - final raw = MockTapDownDetails(); + final raw = _MockTapDownDetails(); when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - final tapDownEvent = MockTapDownInfo(); + final tapDownEvent = _MockTapDownInfo(); when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); when(() => tapDownEvent.raw).thenReturn(raw); @@ -307,14 +334,14 @@ void main() { flameTester.test('tap down moves right flipper up', (game) async { await game.ready(); - final eventPosition = MockEventPosition(); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(game.canvasSize); - final raw = MockTapDownDetails(); + final raw = _MockTapDownDetails(); when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - final tapDownEvent = MockTapDownInfo(); + final tapDownEvent = _MockTapDownInfo(); when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); when(() => tapDownEvent.raw).thenReturn(raw); @@ -330,14 +357,14 @@ void main() { flameTester.test('tap up moves flipper down', (game) async { await game.ready(); - final eventPosition = MockEventPosition(); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero()); - final raw = MockTapDownDetails(); + final raw = _MockTapDownDetails(); when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - final tapDownEvent = MockTapDownInfo(); + final tapDownEvent = _MockTapDownInfo(); when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); when(() => tapDownEvent.raw).thenReturn(raw); @@ -349,7 +376,7 @@ void main() { expect(flippers.first.body.linearVelocity.y, isNegative); - final tapUpEvent = MockTapUpInfo(); + final tapUpEvent = _MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); game.onTapUp(tapUpEvent); @@ -361,14 +388,14 @@ void main() { flameTester.test('tap cancel moves flipper down', (game) async { await game.ready(); - final eventPosition = MockEventPosition(); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero()); - final raw = MockTapDownDetails(); + final raw = _MockTapDownDetails(); when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - final tapDownEvent = MockTapDownInfo(); + final tapDownEvent = _MockTapDownInfo(); when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); when(() => tapDownEvent.raw).thenReturn(raw); @@ -390,68 +417,23 @@ void main() { flameTester.test('tap down moves plunger down', (game) async { await game.ready(); - final eventPosition = MockEventPosition(); - when(() => eventPosition.game).thenReturn(Vector2(40, 60)); - - final raw = MockTapDownDetails(); - when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - - final tapDownEvent = MockTapDownInfo(); - when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); - when(() => tapDownEvent.raw).thenReturn(raw); - - final plunger = game.descendants().whereType().first; - - game.onTapDown(tapDownEvent); - - expect(plunger.body.linearVelocity.y, equals(7)); - }); - - flameTester.test('tap up releases plunger', (game) async { - final eventPosition = MockEventPosition(); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2(40, 60)); - final raw = MockTapDownDetails(); + final raw = _MockTapDownDetails(); when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - final tapDownEvent = MockTapDownInfo(); + final tapDownEvent = _MockTapDownInfo(); when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); when(() => tapDownEvent.raw).thenReturn(raw); final plunger = game.descendants().whereType().first; - game.onTapDown(tapDownEvent); - - expect(plunger.body.linearVelocity.y, equals(7)); - - final tapUpEvent = MockTapUpInfo(); - when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); - - game.onTapUp(tapUpEvent); - - expect(plunger.body.linearVelocity.y, equals(0)); - }); - flameTester.test('tap cancel releases plunger', (game) async { - await game.ready(); - - final eventPosition = MockEventPosition(); - when(() => eventPosition.game).thenReturn(Vector2(40, 60)); - - final raw = MockTapDownDetails(); - when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - - final tapDownEvent = MockTapDownInfo(); - when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); - when(() => tapDownEvent.raw).thenReturn(raw); - - final plunger = game.descendants().whereType().first; game.onTapDown(tapDownEvent); - expect(plunger.body.linearVelocity.y, equals(7)); - - game.onTapCancel(); + game.update(1); - expect(plunger.body.linearVelocity.y, equals(0)); + expect(plunger.body.linearVelocity.y, isPositive); }); }); }); @@ -460,13 +442,13 @@ void main() { debugModeFlameTester.test( 'adds a ball on tap up', (game) async { - final eventPosition = MockEventPosition(); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.all(10)); - final raw = MockTapUpDetails(); + final raw = _MockTapUpDetails(); when(() => raw.kind).thenReturn(PointerDeviceKind.mouse); - final tapUpEvent = MockTapUpInfo(); + final tapUpEvent = _MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); when(() => tapUpEvent.raw).thenReturn(raw); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index f8b62d05..d3b32d85 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -4,12 +4,21 @@ 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:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import '../../helpers/helpers.dart'; +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} + +class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} + +class _MockStartGameBloc extends Mock implements StartGameBloc {} + void main() { final game = PinballTestGame(); @@ -19,8 +28,8 @@ void main() { setUp(() async { await Future.wait(game.preLoadAssets()); - characterThemeCubit = MockCharacterThemeCubit(); - gameBloc = MockGameBloc(); + characterThemeCubit = _MockCharacterThemeCubit(); + gameBloc = _MockGameBloc(); whenListen( characterThemeCubit, @@ -47,7 +56,7 @@ void main() { testWidgets( 'renders the loading indicator while the assets load', (tester) async { - final assetsManagerCubit = MockAssetsManagerCubit(); + final assetsManagerCubit = _MockAssetsManagerCubit(); final initialAssetsState = AssetsManagerState( loadables: [Future.value()], loaded: const [], @@ -79,8 +88,8 @@ void main() { testWidgets( 'renders PinballGameLoadedView after resources have been loaded', (tester) async { - final assetsManagerCubit = MockAssetsManagerCubit(); - final startGameBloc = MockStartGameBloc(); + final assetsManagerCubit = _MockAssetsManagerCubit(); + final startGameBloc = _MockStartGameBloc(); final loadedAssetsState = AssetsManagerState( loadables: [Future.value()], @@ -168,8 +177,8 @@ void main() { }); group('PinballGameView', () { - final gameBloc = MockGameBloc(); - final startGameBloc = MockStartGameBloc(); + final gameBloc = _MockGameBloc(); + final startGameBloc = _MockStartGameBloc(); setUp(() async { await Future.wait(game.preLoadAssets()); diff --git a/test/game/view/widgets/bonus_animation_test.dart b/test/game/view/widgets/bonus_animation_test.dart index b726ac83..2284ca8d 100644 --- a/test/game/view/widgets/bonus_animation_test.dart +++ b/test/game/view/widgets/bonus_animation_test.dart @@ -1,7 +1,6 @@ // ignore_for_file: invalid_use_of_protected_member import 'dart:typed_data'; -import 'dart:ui' as ui; import 'package:flame/assets.dart'; import 'package:flame/flame.dart'; @@ -14,11 +13,9 @@ import 'package:pinball_flame/pinball_flame.dart'; import '../../../helpers/helpers.dart'; -class MockImages extends Mock implements Images {} +class _MockImages extends Mock implements Images {} -class MockImage extends Mock implements ui.Image {} - -class MockCallback extends Mock { +class _MockCallback extends Mock { void call(); } @@ -30,7 +27,7 @@ void main() { // TODO(arturplaczek): need to find for a better solution for loading image // or use original images from BonusAnimation.loadAssets() final image = await decodeImageFromList(Uint8List.fromList(fakeImage)); - final images = MockImages(); + final images = _MockImages(); when(() => images.fromCache(any())).thenReturn(image); when(() => images.load(any())).thenAnswer((_) => Future.value(image)); Flame.images = images; @@ -88,7 +85,7 @@ void main() { // https://github.com/flame-engine/flame/issues/1543 testWidgets('called onCompleted callback at the end of animation ', (tester) async { - final callback = MockCallback(); + final callback = _MockCallback(); await tester.runAsync(() async { await tester.pumpWidget( @@ -112,7 +109,7 @@ void main() { }); testWidgets('called onCompleted once when animation changed', (tester) async { - final callback = MockCallback(); + final callback = _MockCallback(); final secondAnimation = BonusAnimation.sparkyTurboCharge( onCompleted: callback.call, ); diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart index 505ce5e0..f8be70c2 100644 --- a/test/game/view/widgets/game_hud_test.dart +++ b/test/game/view/widgets/game_hud_test.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'dart:ui' as ui; import 'package:bloc_test/bloc_test.dart'; import 'package:flame/assets.dart'; @@ -19,9 +18,9 @@ import 'package:pinball_ui/pinball_ui.dart'; import '../../../helpers/helpers.dart'; -class MockImages extends Mock implements Images {} +class _MockImages extends Mock implements Images {} -class MockImage extends Mock implements ui.Image {} +class _MockGameBloc extends Mock implements GameBloc {} void main() { group('GameHud', () { @@ -35,12 +34,12 @@ void main() { ); setUp(() async { - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); // TODO(arturplaczek): need to find for a better solution for loading // image or use original images from BonusAnimation.loadAssets() final image = await decodeImageFromList(Uint8List.fromList(fakeImage)); - final images = MockImages(); + final images = _MockImages(); when(() => images.fromCache(any())).thenReturn(image); when(() => images.load(any())).thenAnswer((_) => Future.value(image)); Flame.images = images; diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index ee9778bc..1d7070e0 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -8,6 +8,12 @@ import 'package:pinball_theme/pinball_theme.dart'; import '../../../helpers/helpers.dart'; +class _MockPinballGame extends Mock implements PinballGame {} + +class _MockGameFlowController extends Mock implements GameFlowController {} + +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} + void main() { group('PlayButtonOverlay', () { late PinballGame game; @@ -20,9 +26,9 @@ void main() { await Flame.images.load(const AndroidTheme().animation.keyName); await Flame.images.load(const DinoTheme().animation.keyName); await Flame.images.load(const SparkyTheme().animation.keyName); - game = MockPinballGame(); - gameFlowController = MockGameFlowController(); - characterThemeCubit = MockCharacterThemeCubit(); + game = _MockPinballGame(); + gameFlowController = _MockGameFlowController(); + characterThemeCubit = _MockCharacterThemeCubit(); whenListen( characterThemeCubit, const Stream.empty(), diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart index 335a1c32..e3a4b887 100644 --- a/test/game/view/widgets/round_count_display_test.dart +++ b/test/game/view/widgets/round_count_display_test.dart @@ -1,11 +1,14 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_ui/pinball_ui.dart'; import '../../../helpers/helpers.dart'; +class _MockGameBloc extends Mock implements GameBloc {} + void main() { group('RoundCountDisplay renders', () { late GameBloc gameBloc; @@ -17,7 +20,7 @@ void main() { ); setUp(() { - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); whenListen( gameBloc, diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart index 63f7d1c5..695dc6e1 100644 --- a/test/game/view/widgets/score_view_test.dart +++ b/test/game/view/widgets/score_view_test.dart @@ -3,12 +3,15 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../../helpers/helpers.dart'; +class _MockGameBloc extends Mock implements GameBloc {} + void main() { late GameBloc gameBloc; late StreamController stateController; @@ -21,7 +24,7 @@ void main() { ); setUp(() { - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); stateController = StreamController()..add(initialState); whenListen( diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index fb27f72a..50bb9bc1 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -8,7 +8,6 @@ export 'builders.dart'; export 'fakes.dart'; export 'forge2d.dart'; export 'key_testers.dart'; -export 'mocks.dart'; export 'pump_app.dart'; export 'test_games.dart'; export 'text_span.dart'; diff --git a/test/helpers/key_testers.dart b/test/helpers/key_testers.dart index 04fed1da..ff870d6c 100644 --- a/test/helpers/key_testers.dart +++ b/test/helpers/key_testers.dart @@ -1,8 +1,21 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; import 'package:mocktail/mocktail.dart'; -import 'helpers.dart'; +class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} @isTest void testRawKeyUpEvents( @@ -15,7 +28,7 @@ void testRawKeyUpEvents( } RawKeyUpEvent _mockKeyUpEvent(LogicalKeyboardKey key) { - final event = MockRawKeyUpEvent(); + final event = _MockRawKeyUpEvent(); when(() => event.logicalKey).thenReturn(key); return event; } @@ -31,7 +44,7 @@ void testRawKeyDownEvents( } RawKeyDownEvent _mockKeyDownEvent(LogicalKeyboardKey key) { - final event = MockRawKeyDownEvent(); + final event = _MockRawKeyDownEvent(); when(() => event.logicalKey).thenReturn(key); return event; } diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart deleted file mode 100644 index 1d3ad3c7..00000000 --- a/test/helpers/mocks.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:authentication_repository/authentication_repository.dart'; -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; -import 'package:flame/input.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.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/start_game/start_game.dart'; -import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_ui/pinball_ui.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockPinballGame extends Mock implements PinballGame {} - -class MockDrain extends Mock implements Drain {} - -class MockBody extends Mock implements Body {} - -class MockBall extends Mock implements Ball {} - -class MockControlledBall extends Mock implements ControlledBall {} - -class MockBallController extends Mock implements BallController {} - -class MockContact extends Mock implements Contact {} - -class MockGameBloc extends Mock implements GameBloc {} - -class MockStartGameBloc extends Mock implements StartGameBloc {} - -class MockGameState extends Mock implements GameState {} - -class MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} - -class MockAuthenticationRepository extends Mock - implements AuthenticationRepository {} - -class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} - -class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { - @override - String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return super.toString(); - } -} - -class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { - @override - String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return super.toString(); - } -} - -class MockTapDownInfo extends Mock implements TapDownInfo {} - -class MockTapDownDetails extends Mock implements TapDownDetails {} - -class MockTapUpInfo extends Mock implements TapUpInfo {} - -class MockTapUpDetails extends Mock implements TapUpDetails {} - -class MockEventPosition extends Mock implements EventPosition {} - -class MockFilter extends Mock implements Filter {} - -class MockFixture extends Mock implements Fixture {} - -class MockComponent extends Mock implements Component {} - -class MockComponentSet extends Mock implements ComponentSet {} - -class MockDashNestBumper extends Mock implements DashNestBumper {} - -class MockPinballAudio extends Mock implements PinballAudio {} - -class MockSparkyComputerSensor extends Mock implements SparkyComputerSensor {} - -class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} - -class MockBackboard extends Mock implements Backboard {} - -class MockCameraController extends Mock implements CameraController {} - -class MockActiveOverlaysNotifier extends Mock - implements ActiveOverlaysNotifier {} - -class MockGameFlowController extends Mock implements GameFlowController {} - -class MockAndroidBumper extends Mock implements AndroidBumper {} - -class MockSparkyBumper extends Mock implements SparkyBumper {} - -class MockMultiplier extends Mock implements Multiplier {} - -class MockMultipliersGroup extends Mock implements Multipliers {} - -class MockMultiplierCubit extends Mock implements MultiplierCubit {} - -class MockUrlLauncher extends Mock - with MockPlatformInterfaceMixin - implements UrlLauncherPlatform {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 672f9b5e..7347989d 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -19,26 +19,31 @@ import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_ui/pinball_ui.dart'; -import 'helpers.dart'; +class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} -PinballAudio _buildDefaultPinballAudio() { - final audio = MockPinballAudio(); +class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { +} - when(audio.load).thenAnswer((_) => Future.value()); +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockStartGameBloc extends Mock implements StartGameBloc {} +class _MockPinballAudio extends Mock implements PinballAudio {} + +PinballAudio _buildDefaultPinballAudio() { + final audio = _MockPinballAudio(); + when(audio.load).thenAnswer((_) => Future.value()); return audio; } -MockAssetsManagerCubit _buildDefaultAssetsManagerCubit() { - final cubit = MockAssetsManagerCubit(); - +AssetsManagerCubit _buildDefaultAssetsManagerCubit() { + final cubit = _MockAssetsManagerCubit(); final state = AssetsManagerState( loadables: [Future.value()], - loaded: [ - Future.value(), - ], + loaded: [Future.value()], ); - whenListen( cubit, Stream.value(state), @@ -63,7 +68,7 @@ extension PumpApp on WidgetTester { MultiRepositoryProvider( providers: [ RepositoryProvider.value( - value: leaderboardRepository ?? MockLeaderboardRepository(), + value: leaderboardRepository ?? _MockLeaderboardRepository(), ), RepositoryProvider.value( value: pinballAudio ?? _buildDefaultPinballAudio(), @@ -72,13 +77,13 @@ extension PumpApp on WidgetTester { child: MultiBlocProvider( providers: [ BlocProvider.value( - value: characterThemeCubit ?? MockCharacterThemeCubit(), + value: characterThemeCubit ?? _MockCharacterThemeCubit(), ), BlocProvider.value( - value: gameBloc ?? MockGameBloc(), + value: gameBloc ?? _MockGameBloc(), ), BlocProvider.value( - value: startGameBloc ?? MockStartGameBloc(), + value: startGameBloc ?? _MockStartGameBloc(), ), BlocProvider.value( value: assetsManagerCubit ?? _buildDefaultAssetsManagerCubit(), diff --git a/test/helpers/test_games.dart b/test/helpers/test_games.dart index baa466b8..5e67532a 100644 --- a/test/helpers/test_games.dart +++ b/test/helpers/test_games.dart @@ -4,11 +4,12 @@ import 'dart:async'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_theme/pinball_theme.dart'; -import 'helpers.dart'; +class _MockPinballAudio extends Mock implements PinballAudio {} class TestGame extends Forge2DGame with FlameBloc { TestGame() { @@ -23,7 +24,7 @@ class PinballTestGame extends PinballGame { CharacterTheme? theme, }) : _assets = assets, super( - audio: audio ?? MockPinballAudio(), + audio: audio ?? _MockPinballAudio(), characterTheme: theme ?? const DashTheme(), ); final List? _assets; @@ -44,7 +45,7 @@ class DebugPinballTestGame extends DebugPinballGame { CharacterTheme? theme, }) : _assets = assets, super( - audio: audio ?? MockPinballAudio(), + audio: audio ?? _MockPinballAudio(), characterTheme: theme ?? const DashTheme(), ); diff --git a/test/how_to_play/how_to_play_dialog_test.dart b/test/how_to_play/how_to_play_dialog_test.dart index 24c683a4..2570c846 100644 --- a/test/how_to_play/how_to_play_dialog_test.dart +++ b/test/how_to_play/how_to_play_dialog_test.dart @@ -7,7 +7,7 @@ import 'package:platform_helper/platform_helper.dart'; import '../helpers/helpers.dart'; -class MockPlatformHelper extends Mock implements PlatformHelper {} +class _MockPlatformHelper extends Mock implements PlatformHelper {} void main() { group('HowToPlayDialog', () { @@ -16,7 +16,7 @@ void main() { setUp(() async { l10n = await AppLocalizations.delegate.load(const Locale('en')); - platformHelper = MockPlatformHelper(); + platformHelper = _MockPlatformHelper(); }); testWidgets( @@ -73,5 +73,25 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(HowToPlayDialog), findsNothing); }); + + testWidgets('can be dismissed', (tester) async { + await tester.pumpApp( + Builder( + builder: (context) { + return TextButton( + onPressed: () => showHowToPlayDialog(context), + child: const Text('test'), + ); + }, + ), + ); + expect(find.byType(HowToPlayDialog), findsNothing); + await tester.tap(find.text('test')); + await tester.pumpAndSettle(); + + await tester.tapAt(Offset.zero); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }); }); } diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index c5cfb494..28033030 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -10,6 +10,8 @@ import 'package:pinball_ui/pinball_ui.dart'; import '../../helpers/helpers.dart'; +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); late CharacterThemeCubit characterThemeCubit; @@ -20,7 +22,7 @@ void main() { await Flame.images.load(const AndroidTheme().animation.keyName); await Flame.images.load(const DinoTheme().animation.keyName); await Flame.images.load(const SparkyTheme().animation.keyName); - characterThemeCubit = MockCharacterThemeCubit(); + characterThemeCubit = _MockCharacterThemeCubit(); whenListen( characterThemeCubit, const Stream.empty(), diff --git a/test/start_game/bloc/start_game_bloc_test.dart b/test/start_game/bloc/start_game_bloc_test.dart index ec1b3ced..6663ff4e 100644 --- a/test/start_game/bloc/start_game_bloc_test.dart +++ b/test/start_game/bloc/start_game_bloc_test.dart @@ -4,18 +4,20 @@ import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/start_game/bloc/start_game_bloc.dart'; -import '../../helpers/helpers.dart'; +class _MockPinballGame extends Mock implements PinballGame {} + +class _MockGameFlowController extends Mock implements GameFlowController {} void main() { late PinballGame pinballGame; setUp(() { - pinballGame = MockPinballGame(); + pinballGame = _MockPinballGame(); when( () => pinballGame.gameFlowController, ).thenReturn( - MockGameFlowController(), + _MockGameFlowController(), ); }); diff --git a/web/favicon.png b/web/favicon.png index 66a69cb1..8aaa46ac 100644 Binary files a/web/favicon.png and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index 69c31fc5..b749bfef 100644 Binary files a/web/icons/Icon-192.png and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png index d920815d..88cfd48d 100644 Binary files a/web/icons/Icon-512.png and b/web/icons/Icon-512.png differ diff --git a/web/icons/favicon.png b/web/icons/favicon.png index 66a69cb1..8aaa46ac 100644 Binary files a/web/icons/favicon.png and b/web/icons/favicon.png differ diff --git a/web/index.html b/web/index.html index 471a2f3f..107b34ba 100644 --- a/web/index.html +++ b/web/index.html @@ -24,27 +24,22 @@ + - - - - - - + content="https://firebasestorage.googleapis.com/v0/b/pinball-dev.appspot.com/o/images%2Fpinball_share_image.png?alt=media"> + + - -