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/bonus_animation/google_word.png b/assets/images/bonus_animation/google_word.png index 7adab3b4..c4ab2948 100644 Binary files a/assets/images/bonus_animation/google_word.png and b/assets/images/bonus_animation/google_word.png differ 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/app/app.dart b/lib/app/app.dart index 2b135918..f23ab3c8 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,8 +1 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - export 'view/app.dart'; diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 528954a6..d778b55b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - // ignore_for_file: public_member_api_docs import 'package:authentication_repository/authentication_repository.dart'; diff --git a/lib/assets_manager/assets_manager.dart b/lib/assets_manager/assets_manager.dart new file mode 100644 index 00000000..438b75d1 --- /dev/null +++ b/lib/assets_manager/assets_manager.dart @@ -0,0 +1,2 @@ +export 'cubit/assets_manager_cubit.dart'; +export 'views/views.dart'; diff --git a/lib/game/assets_manager/cubit/assets_manager_cubit.dart b/lib/assets_manager/cubit/assets_manager_cubit.dart similarity index 100% rename from lib/game/assets_manager/cubit/assets_manager_cubit.dart rename to lib/assets_manager/cubit/assets_manager_cubit.dart diff --git a/lib/game/assets_manager/cubit/assets_manager_state.dart b/lib/assets_manager/cubit/assets_manager_state.dart similarity index 100% rename from lib/game/assets_manager/cubit/assets_manager_state.dart rename to lib/assets_manager/cubit/assets_manager_state.dart diff --git a/lib/assets_manager/views/assets_loading_page.dart b/lib/assets_manager/views/assets_loading_page.dart new file mode 100644 index 00000000..ddb76803 --- /dev/null +++ b/lib/assets_manager/views/assets_loading_page.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template assets_loading_page} +/// Widget used to indicate the loading progress of the different assets used +/// in the game +/// {@endtemplate} +class AssetsLoadingPage extends StatelessWidget { + /// {@macro assets_loading_page} + const AssetsLoadingPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final headline1 = Theme.of(context).textTheme.headline1; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.ioPinball, + style: headline1!.copyWith(fontSize: 80), + textAlign: TextAlign.center, + ), + const SizedBox(height: 40), + AnimatedEllipsisText( + l10n.loading, + style: headline1, + ), + const SizedBox(height: 40), + FractionallySizedBox( + widthFactor: 0.8, + child: BlocBuilder( + builder: (context, state) { + return PinballLoadingIndicator(value: state.progress); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/assets_manager/views/views.dart b/lib/assets_manager/views/views.dart new file mode 100644 index 00000000..8c60627f --- /dev/null +++ b/lib/assets_manager/views/views.dart @@ -0,0 +1 @@ +export 'assets_loading_page.dart'; diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index bbd87f0c..c5e42951 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - // ignore_for_file: public_member_api_docs import 'dart:async'; diff --git a/lib/game/components/android_acres.dart b/lib/game/components/android_acres/android_acres.dart similarity index 52% rename from lib/game/components/android_acres.dart rename to lib/game/components/android_acres/android_acres.dart index 2af7335f..3d1a8154 100644 --- a/lib/game/components/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -1,39 +1,50 @@ // ignore_for_file: avoid_renaming_method_parameters -import 'package:flame_forge2d/flame_forge2d.dart'; +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'; -import 'package:pinball_flame/pinball_flame.dart'; /// {@template android_acres} /// Area positioned on the left side of the board containing the /// [AndroidSpaceship], [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s. /// {@endtemplate} -class AndroidAcres extends Blueprint { +class AndroidAcres extends Component { /// {@macro android_acres} AndroidAcres() : super( - components: [ + children: [ + 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: 20000), + ScoringBehavior(points: Points.twentyThousand), ], )..initialPosition = Vector2(-25, 1.3), AndroidBumper.b( children: [ - ScoringBehavior(points: 20000), + ScoringBehavior(points: Points.twentyThousand), ], )..initialPosition = Vector2(-32.8, -9.2), AndroidBumper.cow( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: Points.twentyThousand), ], )..initialPosition = Vector2(-20.5, -13.8), - ], - blueprints: [ - SpaceshipRamp(), - AndroidSpaceship(position: Vector2(-26.5, -28.5)), - SpaceshipRail(), + 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/backbox/backbox.dart b/lib/game/components/backbox/backbox.dart index 8c49e3a5..c23b56db 100644 --- a/lib/game/components/backbox/backbox.dart +++ b/lib/game/components/backbox/backbox.dart @@ -3,21 +3,23 @@ import 'dart:async'; import 'package:flame/components.dart'; import 'package:pinball/game/components/backbox/displays/displays.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template backbox} /// The [Backbox] of the pinball machine. /// {@endtemplate} -class Backbox extends PositionComponent with HasGameRef { +class Backbox extends PositionComponent with HasGameRef, ZIndex { /// {@macro backbox} Backbox() : super( position: Vector2(0, -87), anchor: Anchor.bottomCenter, - priority: RenderPriority.backboardMarquee, children: [ _BackboxSpriteComponent(), ], - ); + ) { + zIndex = ZIndexes.backboardMarquee; + } /// Puts [InitialsInputDisplay] on the [Backbox]. Future initialsInput({ diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart index 921a8e58..c13f21be 100644 --- a/lib/game/components/bottom_group.dart +++ b/lib/game/components/bottom_group.dart @@ -1,6 +1,7 @@ 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'; /// {@template bottom_group} /// Grouping of the board's symmetrical bottom [Component]s. @@ -8,7 +9,7 @@ import 'package:pinball_components/pinball_components.dart'; /// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s. /// {@endtemplate} // TODO(allisonryan0002): Consider renaming. -class BottomGroup extends Component { +class BottomGroup extends Component with ZIndex { /// {@macro bottom_group} BottomGroup() : super( @@ -16,8 +17,9 @@ class BottomGroup extends Component { _BottomGroupSide(side: BoardSide.right), _BottomGroupSide(side: BoardSide.left), ], - priority: RenderPriority.bottomGroup, - ); + ) { + zIndex = ZIndexes.bottomGroup; + } } /// {@template bottom_group_side} @@ -36,7 +38,7 @@ class _BottomGroupSide extends Component { @override Future onLoad() async { final direction = _side.direction; - final centerXAdjustment = _side.isLeft ? 0 : -6.5; + final centerXAdjustment = _side.isLeft ? 0 : -6.66; final flipper = ControlledFlipper( side: _side, @@ -44,16 +46,16 @@ class _BottomGroupSide extends Component { final baseboard = Baseboard(side: _side) ..initialPosition = Vector2( (25.58 * direction) + centerXAdjustment, - 28.69, + 28.71, ); final kicker = Kicker( side: _side, children: [ - ScoringBehavior(points: 5000), + ScoringBehavior(points: Points.fiveThousand)..applyTo(['bouncy_edge']), ], )..initialPosition = Vector2( - (22.4 * direction) + centerXAdjustment, - 25, + (22.64 * direction) + centerXAdjustment, + 25.1, ); await addAll([flipper, baseboard, kicker]); diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 341f0877..d3a1bb71 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,16 +1,17 @@ -export 'android_acres.dart'; +export 'android_acres/android_acres.dart'; export 'backbox/backbox.dart'; export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; export 'controlled_plunger.dart'; -export 'dino_desert.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_fire_zone.dart'; +export 'sparky_scorch.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index ff05ad62..4103bb81 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -19,8 +19,8 @@ class ControlledBall extends Ball with Controls { required CharacterTheme characterTheme, }) : super(baseColor: characterTheme.ballColor) { controller = BallController(this); - priority = RenderPriority.ballOnLaunchRamp; layer = Layer.launcher; + zIndex = ZIndexes.ballOnLaunchRamp; } /// {@template bonus_ball} @@ -30,13 +30,13 @@ class ControlledBall extends Ball with Controls { required CharacterTheme characterTheme, }) : super(baseColor: characterTheme.ballColor) { controller = BallController(this); - priority = RenderPriority.ballOnBoard; + zIndex = ZIndexes.ballOnBoard; } /// [Ball] used in [DebugPinballGame]. ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { controller = BallController(this); - priority = RenderPriority.ballOnBoard; + zIndex = ZIndexes.ballOnBoard; } } diff --git a/lib/game/components/dino_desert.dart b/lib/game/components/dino_desert.dart deleted file mode 100644 index 9e912575..00000000 --- a/lib/game/components/dino_desert.dart +++ /dev/null @@ -1,23 +0,0 @@ -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'; - -/// {@template dino_desert} -/// Area located next to the [Launcher] containing the [ChromeDino] and -/// [DinoWalls]. -/// {@endtemplate} -// TODO(allisonryan0002): use a controller to initiate dino bonus when dino is -// fully implemented. -class DinoDesert extends Blueprint { - /// {@macro dino_desert} - DinoDesert() - : super( - components: [ - ChromeDino()..initialPosition = Vector2(12.3, -6.9), - ], - blueprints: [ - DinoWalls(), - ], - ); -} 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/dino_desert.dart b/lib/game/components/dino_desert/dino_desert.dart new file mode 100644 index 00000000..4d8cd7b6 --- /dev/null +++ b/lib/game/components/dino_desert/dino_desert.dart @@ -0,0 +1,48 @@ +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], +/// [DinoWalls], and the [Slingshots]. +/// {@endtemplate} +class DinoDesert extends Component { + /// {@macro dino_desert} + DinoDesert() + : super( + children: [ + ChromeDino( + children: [ + ScoringBehavior(points: Points.twoHundredThousand) + ..applyTo(['inside_mouth']), + ], + )..initialPosition = Vector2(12.6, -6.9), + _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 { + @override + Body createBody() { + final shape = EdgeShape() + ..set( + Vector2(25, -14.2), + Vector2(25, -7.7), + ); + + return world.createBody(BodyDef())..createFixtureFromShape(shape); + } +} 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 949fead1..8f1b46e8 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.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(29.5, -24.5), + ); + animatronic.playing = true; + signpost.bloc.onProgressed(); + } } }); } diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index 7508d5c3..1fb8907b 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -5,41 +5,43 @@ import 'package:flutter/material.dart'; import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template flutter_forest} /// Area positioned at the top right of the board where the [Ball] can bounce /// off [DashNestBumper]s. /// {@endtemplate} -class FlutterForest extends Component { +class FlutterForest extends Component with ZIndex { /// {@macro flutter_forest} FlutterForest() : super( - priority: RenderPriority.flutterForest, children: [ Signpost( children: [ - ScoringBehavior(points: 20), + ScoringBehavior(points: Points.fiveThousand), ], )..initialPosition = Vector2(8.35, -58.3), DashNestBumper.main( children: [ - ScoringBehavior(points: 200000), + ScoringBehavior(points: Points.twoHundredThousand), ], )..initialPosition = Vector2(18.55, -59.35), DashNestBumper.a( children: [ - ScoringBehavior(points: 20000), + ScoringBehavior(points: Points.twentyThousand), ], )..initialPosition = Vector2(8.95, -51.95), DashNestBumper.b( children: [ - ScoringBehavior(points: 20000), + ScoringBehavior(points: Points.twentyThousand), ], - )..initialPosition = Vector2(23.3, -46.75), + )..initialPosition = Vector2(22.3, -46.75), DashAnimatronic()..position = Vector2(20, -66), FlutterForestBonusBehavior(), ], - ); + ) { + zIndex = ZIndexes.flutterForest; + } /// Creates a [FlutterForest] without any children. /// diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index 15aac66f..af4343fa 100644 --- a/lib/game/components/game_flow_controller.dart +++ b/lib/game/components/game_flow_controller.dart @@ -38,6 +38,7 @@ class GameFlowController extends ComponentController /// Puts the game in the playing state. void start() { + component.audio.backgroundMusic(); component.firstChild()?.focusOnGame(); component.overlays.remove(PinballGame.playButtonOverlay); } diff --git a/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart index 92664531..cb9ad308 100644 --- a/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart +++ b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart @@ -17,7 +17,7 @@ class GoogleWordBonusBehavior extends Component // https://github.com/flame-engine/flame/pull/1538 letter.bloc.stream.listen((_) { final achievedBonus = googleLetters - .every((letter) => letter.bloc.state == GoogleLetterState.active); + .every((letter) => letter.bloc.state == GoogleLetterState.lit); if (achievedBonus) { gameRef.audio.googleBonus(); diff --git a/lib/game/components/google_word/google_word.dart b/lib/game/components/google_word/google_word.dart index 63999fe1..af1faea9 100644 --- a/lib/game/components/google_word/google_word.dart +++ b/lib/game/components/google_word/google_word.dart @@ -3,11 +3,12 @@ import 'package:flutter/material.dart'; import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template google_word} /// Loads all [GoogleLetter]s to compose a [GoogleWord]. /// {@endtemplate} -class GoogleWord extends Component { +class GoogleWord extends Component with ZIndex { /// {@macro google_word} GoogleWord({ required Vector2 position, @@ -15,31 +16,33 @@ class GoogleWord extends Component { children: [ GoogleLetter( 0, - children: [ScoringBehavior(points: 5000)], - )..initialPosition = position + Vector2(-12.92, 1.82), + children: [ScoringBehavior(points: Points.fiveThousand)], + )..initialPosition = position + Vector2(-13.1, 1.72), GoogleLetter( 1, - children: [ScoringBehavior(points: 5000)], - )..initialPosition = position + Vector2(-8.33, -0.65), + children: [ScoringBehavior(points: Points.fiveThousand)], + )..initialPosition = position + Vector2(-8.33, -0.75), GoogleLetter( 2, - children: [ScoringBehavior(points: 5000)], - )..initialPosition = position + Vector2(-2.88, -1.75), + children: [ScoringBehavior(points: Points.fiveThousand)], + )..initialPosition = position + Vector2(-2.88, -1.85), GoogleLetter( 3, - children: [ScoringBehavior(points: 5000)], - )..initialPosition = position + Vector2(2.88, -1.75), + children: [ScoringBehavior(points: Points.fiveThousand)], + )..initialPosition = position + Vector2(2.88, -1.85), GoogleLetter( 4, - children: [ScoringBehavior(points: 5000)], - )..initialPosition = position + Vector2(8.33, -0.65), + children: [ScoringBehavior(points: Points.fiveThousand)], + )..initialPosition = position + Vector2(8.33, -0.75), GoogleLetter( 5, - children: [ScoringBehavior(points: 5000)], - )..initialPosition = position + Vector2(12.92, 1.82), + children: [ScoringBehavior(points: Points.fiveThousand)], + )..initialPosition = position + Vector2(13.1, 1.72), GoogleWordBonusBehavior(), ], - ); + ) { + zIndex = ZIndexes.decal; + } /// Creates a [GoogleWord] without any children. /// diff --git a/lib/game/components/launcher.dart b/lib/game/components/launcher.dart index 959e8da0..ffac6507 100644 --- a/lib/game/components/launcher.dart +++ b/lib/game/components/launcher.dart @@ -1,21 +1,20 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame/components.dart'; import 'package:pinball/game/components/components.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; -import 'package:pinball_flame/pinball_flame.dart'; /// {@template launcher} -/// A [Blueprint] which creates the [Plunger], [RocketSpriteComponent] and -/// [LaunchRamp]. +/// Channel on the right side of the board containing the [LaunchRamp], +/// [Plunger], and [RocketSpriteComponent]. /// {@endtemplate} -class Launcher extends Blueprint { +class Launcher extends Component { /// {@macro launcher} Launcher() : super( - components: [ - ControlledPlunger(compressionDistance: 10.5) - ..initialPosition = Vector2(41.1, 43), + children: [ + LaunchRamp(), + ControlledPlunger(compressionDistance: 9.2) + ..initialPosition = Vector2(41.2, 43.7), RocketSpriteComponent()..position = Vector2(43, 62.3), ], - blueprints: [LaunchRamp()], ); } 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/components/multipliers/multipliers.dart b/lib/game/components/multipliers/multipliers.dart index 6a6a1563..8e9df1ff 100644 --- a/lib/game/components/multipliers/multipliers.dart +++ b/lib/game/components/multipliers/multipliers.dart @@ -3,11 +3,12 @@ import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template multipliers} /// A group for the multipliers on the board. /// {@endtemplate} -class Multipliers extends Component { +class Multipliers extends Component with ZIndex { /// {@macro multipliers} Multipliers() : super( @@ -34,7 +35,9 @@ class Multipliers extends Component { ), MultipliersBehavior(), ], - ); + ) { + zIndex = ZIndexes.decal; + } /// Creates [Multipliers] without any children. /// diff --git a/lib/game/components/scoring_behavior.dart b/lib/game/components/scoring_behavior.dart index 3ef82bb5..e8f51e90 100644 --- a/lib/game/components/scoring_behavior.dart +++ b/lib/game/components/scoring_behavior.dart @@ -12,23 +12,23 @@ import 'package:pinball_flame/pinball_flame.dart'; class ScoringBehavior extends ContactBehavior with HasGameRef { /// {@macro scoring_behavior} ScoringBehavior({ - required int points, + required Points points, }) : _points = points; - final int _points; + final Points _points; @override void beginContact(Object other, Contact contact) { super.beginContact(other, contact); if (other is! Ball) return; - gameRef.read().add(Scored(points: _points)); + gameRef.read().add(Scored(points: _points.value)); gameRef.audio.score(); - gameRef.add( - ScoreText( - text: _points.toString(), - position: other.body.position, - ), - ); + gameRef.firstChild()!.add( + ScoreComponent( + points: _points, + position: other.body.position, + ), + ); } } diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_scorch.dart similarity index 71% rename from lib/game/components/sparky_fire_zone.dart rename to lib/game/components/sparky_scorch.dart index a37c2469..434e9479 100644 --- a/lib/game/components/sparky_fire_zone.dart +++ b/lib/game/components/sparky_scorch.dart @@ -1,40 +1,36 @@ // ignore_for_file: avoid_renaming_method_parameters +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; -/// {@template sparky_fire_zone} -/// Area positioned at the top left of the board where the [Ball] -/// can bounce off [SparkyBumper]s. -/// -/// When a [Ball] hits [SparkyBumper]s, the bumper animates. +/// {@template sparky_scorch} +/// Area positioned at the top left of the board containing the +/// [SparkyComputer], [SparkyAnimatronic], and [SparkyBumper]s. /// {@endtemplate} -class SparkyFireZone extends Blueprint { - /// {@macro sparky_fire_zone} - SparkyFireZone() +class SparkyScorch extends Component { + /// {@macro sparky_scorch} + SparkyScorch() : super( - components: [ + children: [ SparkyBumper.a( children: [ - ScoringBehavior(points: 20000), + ScoringBehavior(points: Points.twentyThousand), ], )..initialPosition = Vector2(-22.9, -41.65), SparkyBumper.b( children: [ - ScoringBehavior(points: 20000), + ScoringBehavior(points: Points.twentyThousand), ], )..initialPosition = Vector2(-21.25, -57.9), SparkyBumper.c( children: [ - ScoringBehavior(points: 20000), + ScoringBehavior(points: Points.twentyThousand), ], )..initialPosition = Vector2(-3.3, -52.55), - SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8), + SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9), SparkyAnimatronic()..position = Vector2(-13.8, -58.2), - ], - blueprints: [ SparkyComputer(), ], ); @@ -51,13 +47,19 @@ class SparkyComputerSensor extends BodyComponent : super( renderBody: false, children: [ - ScoringBehavior(points: 200000), + ScoringBehavior(points: Points.twentyThousand), ], ); @override Body createBody() { - final shape = CircleShape()..radius = 0.1; + final shape = PolygonShape() + ..setAsBox( + 1, + 0.1, + Vector2.zero(), + -0.18, + ); final fixtureDef = FixtureDef(shape, isSensor: true); final bodyDef = BodyDef( position: initialPosition, diff --git a/lib/game/game.dart b/lib/game/game.dart index 7de964eb..ad02533d 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -1,4 +1,3 @@ -export 'assets_manager/cubit/assets_manager_cubit.dart'; export 'bloc/game_bloc.dart'; export 'components/components.dart'; export 'game_assets.dart'; diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index df1b7d0b..cf6c5f59 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; @@ -24,8 +23,10 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.flipper.right.keyName), images.load(components.Assets.images.baseboard.left.keyName), images.load(components.Assets.images.baseboard.right.keyName), - images.load(components.Assets.images.kicker.left.keyName), - images.load(components.Assets.images.kicker.right.keyName), + images.load(components.Assets.images.kicker.left.lit.keyName), + images.load(components.Assets.images.kicker.left.dimmed.keyName), + images.load(components.Assets.images.kicker.right.lit.keyName), + images.load(components.Assets.images.kicker.right.dimmed.keyName), images.load(components.Assets.images.slingshot.upper.keyName), images.load(components.Assets.images.slingshot.lower.keyName), images.load(components.Assets.images.launchRamp.ramp.keyName), @@ -37,6 +38,7 @@ extension PinballGameAssetsX on PinballGame { ), images.load(components.Assets.images.dino.bottomWall.keyName), images.load(components.Assets.images.dino.topWall.keyName), + images.load(components.Assets.images.dino.topWallTunnel.keyName), images.load(components.Assets.images.dino.animatronic.head.keyName), images.load(components.Assets.images.dino.animatronic.mouth.keyName), images.load(components.Assets.images.dash.animatronic.keyName), @@ -89,21 +91,30 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.android.bumper.cow.dimmed.keyName), images.load(components.Assets.images.sparky.computer.top.keyName), images.load(components.Assets.images.sparky.computer.base.keyName), + images.load(components.Assets.images.sparky.computer.glow.keyName), images.load(components.Assets.images.sparky.animatronic.keyName), - images.load(components.Assets.images.sparky.bumper.a.inactive.keyName), - images.load(components.Assets.images.sparky.bumper.a.active.keyName), - images.load(components.Assets.images.sparky.bumper.b.active.keyName), - images.load(components.Assets.images.sparky.bumper.b.inactive.keyName), - images.load(components.Assets.images.sparky.bumper.c.active.keyName), - images.load(components.Assets.images.sparky.bumper.c.inactive.keyName), + images.load(components.Assets.images.sparky.bumper.a.lit.keyName), + images.load(components.Assets.images.sparky.bumper.a.dimmed.keyName), + images.load(components.Assets.images.sparky.bumper.b.lit.keyName), + images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName), + images.load(components.Assets.images.sparky.bumper.c.lit.keyName), + images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName), images.load(components.Assets.images.backbox.marquee.keyName), images.load(components.Assets.images.backbox.displayDivider.keyName), - images.load(components.Assets.images.googleWord.letter1.keyName), - images.load(components.Assets.images.googleWord.letter2.keyName), - images.load(components.Assets.images.googleWord.letter3.keyName), - images.load(components.Assets.images.googleWord.letter4.keyName), - images.load(components.Assets.images.googleWord.letter5.keyName), - images.load(components.Assets.images.googleWord.letter6.keyName), + images.load(components.Assets.images.googleWord.letter1.lit.keyName), + images.load(components.Assets.images.googleWord.letter1.dimmed.keyName), + images.load(components.Assets.images.googleWord.letter2.lit.keyName), + images.load(components.Assets.images.googleWord.letter2.dimmed.keyName), + images.load(components.Assets.images.googleWord.letter3.lit.keyName), + images.load(components.Assets.images.googleWord.letter3.dimmed.keyName), + images.load(components.Assets.images.googleWord.letter4.lit.keyName), + images.load(components.Assets.images.googleWord.letter4.dimmed.keyName), + images.load(components.Assets.images.googleWord.letter5.lit.keyName), + images.load(components.Assets.images.googleWord.letter5.dimmed.keyName), + images.load(components.Assets.images.googleWord.letter6.lit.keyName), + images.load(components.Assets.images.googleWord.letter6.dimmed.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), @@ -114,11 +125,14 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.multiplier.x5.dimmed.keyName), images.load(components.Assets.images.multiplier.x6.lit.keyName), images.load(components.Assets.images.multiplier.x6.dimmed.keyName), + images.load(components.Assets.images.score.fiveThousand.keyName), + images.load(components.Assets.images.score.twentyThousand.keyName), + images.load(components.Assets.images.score.twoHundredThousand.keyName), + images.load(components.Assets.images.score.oneMillion.keyName), images.load(dashTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName), images.load(dinoTheme.leaderboardIcon.keyName), - images.load(Assets.images.components.background.path), ]; } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index bad2573d..7713edcb 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -25,7 +25,7 @@ class PinballGame extends Forge2DGame required this.characterTheme, required this.audio, required this.l10n, - }) { + }) : super(gravity: Vector2(0, 30)) { images.prefix = ''; controller = _GameBallsController(this); } @@ -46,32 +46,39 @@ class PinballGame extends Forge2DGame @override Future onLoad() async { - unawaited(add(gameFlowController = GameFlowController(this))); - unawaited(add(CameraController(this))); - await add(Backbox()); - await add(BoardBackgroundSpriteComponent()); - await add(Drain()); - await add(BottomGroup()); - unawaited(addFromBlueprint(Boundaries())); - - final launcher = Launcher(); - unawaited(addFromBlueprint(launcher)); - await add(Multipliers()); - await add(FlutterForest()); - await addFromBlueprint(SparkyFireZone()); - await addFromBlueprint(AndroidAcres()); - await addFromBlueprint(DinoDesert()); - unawaited(addFromBlueprint(Slingshots())); + await add(gameFlowController = GameFlowController(this)); + await add(CameraController(this)); + + final machine = [ + BoardBackgroundSpriteComponent(), + Boundaries(), + Backbox(), + ]; + final decals = [ + GoogleWord(position: Vector2(-4.25, 1.8)), + Multipliers(), + Multiballs(), + ]; + final characterAreas = [ + AndroidAcres(), + DinoDesert(), + FlutterForest(), + SparkyScorch(), + ]; + await add( - GoogleWord( - position: Vector2( - BoardDimensions.bounds.center.dx - 4.1, - BoardDimensions.bounds.center.dy + 1.8, - ), + ZCanvasComponent( + children: [ + ...machine, + ...decals, + ...characterAreas, + Drain(), + BottomGroup(), + Launcher(), + ], ), ); - controller.attachTo(launcher.components.whereType().single); await super.onLoad(); } @@ -80,12 +87,12 @@ class PinballGame extends Forge2DGame @override void onTapDown(TapDownInfo info) { if (info.raw.kind == PointerDeviceKind.touch) { - final rocket = children.whereType().first; + final rocket = descendants().whereType().first; final bounds = rocket.topLeftPosition & rocket.size; // 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())) { - children.whereType().first.pull(); + descendants().whereType().single.pullFor(2); } else { final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; @@ -101,21 +108,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())) { - children.whereType().first.release(); - } else { - _moveFlippersDown(); - } + _moveFlippersDown(); super.onTapUp(info); } @override void onTapCancel() { - children.whereType().first.release(); - _moveFlippersDown(); super.onTapCancel(); } @@ -135,8 +133,6 @@ class _GameBallsController extends ComponentController with BlocComponent { _GameBallsController(PinballGame game) : super(game); - late final Plunger _plunger; - @override bool listenWhen(GameState? previousState, GameState newState) { final noBallsLeft = component.descendants().whereType().isEmpty; @@ -148,30 +144,27 @@ class _GameBallsController extends ComponentController @override void onNewState(GameState state) { super.onNewState(state); - _spawnBall(); + spawnBall(); } @override Future onLoad() async { await super.onLoad(); - _spawnBall(); + spawnBall(); } - void _spawnBall() { - final ball = ControlledBall.launch( - characterTheme: component.characterTheme, - )..initialPosition = Vector2( - _plunger.body.position.x, - _plunger.body.position.y - Ball.size.y, - ); - component.add(ball); - } - - /// Attaches the controller to the plunger. - // TODO(alestiago): Remove this method and use onLoad instead. - // ignore: use_setters_to_change_properties - void attachTo(Plunger plunger) { - _plunger = plunger; + void spawnBall() { + // TODO(alestiago): Refactor with behavioural pattern. + component.ready().whenComplete(() { + final plunger = parent!.descendants().whereType().single; + final ball = ControlledBall.launch( + characterTheme: component.characterTheme, + )..initialPosition = Vector2( + plunger.body.position.x, + plunger.body.position.y - Ball.size.y, + ); + component.firstChild()?.add(ball); + }); } } @@ -185,7 +178,7 @@ class DebugPinballGame extends PinballGame with FPSCounter { audio: audio, l10n: l10n, ) { - controller = _DebugGameBallsController(this); + controller = _GameBallsController(this); } @override @@ -194,42 +187,21 @@ class DebugPinballGame extends PinballGame with FPSCounter { await add(_DebugInformation()); } - // TODO(allisonryan0002): Remove after google letters have been correctly - // placed. - // Future _loadBackground() async { - // final sprite = await loadSprite( - // Assets.images.components.background.path, - // ); - // final spriteComponent = SpriteComponent( - // sprite: sprite, - // size: Vector2(120, 160), - // anchor: Anchor.center, - // ) - // ..position = Vector2(0, -7.8) - // ..priority = RenderPriority.boardBackground; - - // await add(spriteComponent); - // } - @override void onTapUp(TapUpInfo info) { super.onTapUp(info); if (info.raw.kind == PointerDeviceKind.mouse) { - add(ControlledBall.debug()..initialPosition = info.eventPosition.game); + final ball = ControlledBall.debug() + ..initialPosition = info.eventPosition.game; + firstChild()?.add(ball); } } } -class _DebugGameBallsController extends _GameBallsController { - _DebugGameBallsController(PinballGame game) : super(game); -} - // TODO(wolfenrain): investigate this CI failure. // coverage:ignore-start class _DebugInformation extends Component with HasGameRef { - _DebugInformation() : super(priority: RenderPriority.debugInfo); - @override PositionType get positionType => PositionType.widget; diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 1b186790..3a626ba4 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -4,11 +4,13 @@ import 'package:flame/game.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_ui/pinball_ui.dart'; class PinballGamePage extends StatelessWidget { const PinballGamePage({ @@ -53,15 +55,14 @@ class PinballGamePage extends StatelessWidget { ...game.preLoadAssets(), pinballAudio.load(), ...BonusAnimation.loadAssets(), + ...SelectedCharacter.loadAssets(), ]; return MultiBlocProvider( providers: [ BlocProvider(create: (_) => StartGameBloc(game: game)), BlocProvider(create: (_) => GameBloc()), - BlocProvider( - create: (_) => AssetsManagerCubit(loadables)..load(), - ), + BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()), ], child: PinballGameView(game: game), ); @@ -81,32 +82,13 @@ class PinballGameView extends StatelessWidget { final isLoading = context.select( (AssetsManagerCubit bloc) => bloc.state.progress != 1, ); - - return Scaffold( - backgroundColor: Colors.blue, - body: isLoading - ? const _PinballGameLoadingView() - : PinballGameLoadedView(game: game), - ); - } -} - -class _PinballGameLoadingView extends StatelessWidget { - const _PinballGameLoadingView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final loadingProgress = context.select( - (AssetsManagerCubit bloc) => bloc.state.progress, - ); - - return Padding( - padding: const EdgeInsets.all(24), - child: Center( - child: LinearProgressIndicator( - color: Colors.white, - value: loadingProgress, - ), + return Container( + decoration: const CrtBackground(), + child: Scaffold( + backgroundColor: PinballColors.transparent, + body: isLoading + ? const AssetsLoadingPage() + : PinballGameLoadedView(game: game), ), ); } diff --git a/lib/game/view/widgets/bonus_animation.dart b/lib/game/view/widgets/bonus_animation.dart index da67e1aa..35e600f2 100644 --- a/lib/game/view/widgets/bonus_animation.dart +++ b/lib/game/view/widgets/bonus_animation.dart @@ -126,7 +126,7 @@ class _BonusAnimationState extends State ); animation = spriteSheet.createAnimation( row: 0, - stepTime: 1 / 24, + stepTime: 1 / 12, to: spriteSheet.rows * spriteSheet.columns, loop: false, ); diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index 3db62a50..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,29 +21,12 @@ class PlayButtonOverlay extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; - return Center( - child: ElevatedButton( - onPressed: () { - _game.gameFlowController.start(); - showDialog( - context: context, - barrierDismissible: false, - builder: (_) { - // TODO(arturplaczek): remove after merge StarBlocListener - final height = MediaQuery.of(context).size.height * 0.5; - - return Center( - child: SizedBox( - height: height, - width: height * 1.4, - child: const CharacterSelectionDialog(), - ), - ); - }, - ); - }, - 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/how_to_play.dart b/lib/how_to_play/how_to_play.dart new file mode 100644 index 00000000..e691bba3 --- /dev/null +++ b/lib/how_to_play/how_to_play.dart @@ -0,0 +1 @@ +export 'widgets/widgets.dart'; diff --git a/lib/start_game/widgets/how_to_play_dialog.dart b/lib/how_to_play/widgets/how_to_play_dialog.dart similarity index 68% rename from lib/start_game/widgets/how_to_play_dialog.dart rename to lib/how_to_play/widgets/how_to_play_dialog.dart index 500a4288..e91698f5 100644 --- a/lib/start_game/widgets/how_to_play_dialog.dart +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -3,12 +3,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/gen/gen.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_ui/pinball_ui.dart'; import 'package:platform_helper/platform_helper.dart'; -@visibleForTesting enum Control { left, right, @@ -50,6 +51,16 @@ extension on Control { } } +Future showHowToPlayDialog(BuildContext context) { + final audio = context.read(); + return showDialog( + context: context, + builder: (_) => HowToPlayDialog(), + ).then((_) { + audio.ioPinballVoiceOver(); + }); +} + class HowToPlayDialog extends StatefulWidget { HowToPlayDialog({ Key? key, @@ -70,7 +81,7 @@ class _HowToPlayDialogState extends State { super.initState(); closeTimer = Timer(const Duration(seconds: 3), () { if (mounted) { - Navigator.of(context).maybePop(); + Navigator.of(context).pop(); } }); } @@ -84,9 +95,11 @@ class _HowToPlayDialogState extends State { @override Widget build(BuildContext context) { final isMobile = widget.platformHelper.isMobile; - return PixelatedDecoration( - header: const _HowToPlayHeader(), - body: isMobile ? const _MobileBody() : const _DesktopBody(), + final l10n = context.l10n; + return PinballDialog( + title: l10n.howToPlay, + subtitle: l10n.tipsForFlips, + child: isMobile ? const _MobileBody() : const _DesktopBody(), ); } } @@ -121,23 +134,20 @@ class _MobileLaunchControls extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final textStyle = Theme.of(context).textTheme.headline3; + final headline3 = Theme.of(context) + .textTheme + .headline3! + .copyWith(color: PinballColors.white); return Column( children: [ - Text( - l10n.tapAndHoldRocket, - style: textStyle, - ), + Text(l10n.tapAndHoldRocket, style: headline3), Text.rich( TextSpan( children: [ - TextSpan( - text: '${l10n.to} ', - style: textStyle, - ), + TextSpan(text: '${l10n.to} ', style: headline3), TextSpan( text: l10n.launch, - style: textStyle?.copyWith(color: PinballColors.blue), + style: headline3.copyWith(color: PinballColors.blue), ), ], ), @@ -153,23 +163,20 @@ class _MobileFlipperControls extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final textStyle = Theme.of(context).textTheme.headline3; + final headline3 = Theme.of(context) + .textTheme + .headline3! + .copyWith(color: PinballColors.white); return Column( children: [ - Text( - l10n.tapLeftRightScreen, - style: textStyle, - ), + Text(l10n.tapLeftRightScreen, style: headline3), Text.rich( TextSpan( children: [ - TextSpan( - text: '${l10n.to} ', - style: textStyle, - ), + TextSpan(text: '${l10n.to} ', style: headline3), TextSpan( text: l10n.flip, - style: textStyle?.copyWith(color: PinballColors.orange), + style: headline3.copyWith(color: PinballColors.orange), ), ], ), @@ -184,55 +191,23 @@ class _DesktopBody extends StatelessWidget { @override Widget build(BuildContext context) { - const spacing = SizedBox(height: 16); return ListView( children: const [ - spacing, + SizedBox(height: 16), _DesktopLaunchControls(), - spacing, + SizedBox(height: 16), _DesktopFlipperControls(), ], ); } } -class _HowToPlayHeader extends StatelessWidget { - const _HowToPlayHeader({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - final textStyle = Theme.of(context).textTheme.headline3?.copyWith( - color: PinballColors.darkBlue, - ); - return FittedBox( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - l10n.howToPlay, - style: textStyle?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - Text( - l10n.tipsForFlips, - style: textStyle, - ), - ], - ), - ); - } -} - class _DesktopLaunchControls extends StatelessWidget { const _DesktopLaunchControls({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final l10n = context.l10n; - const spacing = SizedBox(width: 10); - return Column( children: [ Text( @@ -242,11 +217,11 @@ class _DesktopLaunchControls extends StatelessWidget { const SizedBox(height: 10), Wrap( children: const [ - KeyButton(control: Control.down), - spacing, - KeyButton(control: Control.space), - spacing, - KeyButton(control: Control.s), + _KeyButton(control: Control.down), + SizedBox(width: 10), + _KeyButton(control: Control.space), + SizedBox(width: 10), + _KeyButton(control: Control.s), ], ) ], @@ -260,8 +235,6 @@ class _DesktopFlipperControls extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - const rowSpacing = SizedBox(width: 20); - return Column( children: [ Text( @@ -275,17 +248,17 @@ class _DesktopFlipperControls extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: const [ - KeyButton(control: Control.left), - rowSpacing, - KeyButton(control: Control.right), + _KeyButton(control: Control.left), + SizedBox(width: 20), + _KeyButton(control: Control.right), ], ), const SizedBox(height: 8), Wrap( children: const [ - KeyButton(control: Control.a), - rowSpacing, - KeyButton(control: Control.d), + _KeyButton(control: Control.a), + SizedBox(width: 20), + _KeyButton(control: Control.d), ], ) ], @@ -295,29 +268,24 @@ class _DesktopFlipperControls extends StatelessWidget { } } -@visibleForTesting -class KeyButton extends StatelessWidget { - const KeyButton({ - Key? key, - required Control control, - }) : _control = control, - super(key: key); +class _KeyButton extends StatelessWidget { + const _KeyButton({Key? key, required this.control}) : super(key: key); - final Control _control; + final Control control; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final textStyle = - _control.isArrow ? textTheme.headline1 : textTheme.headline3; + control.isArrow ? textTheme.headline1 : textTheme.headline3; const height = 60.0; - final width = _control.isSpace ? height * 2.83 : height; + final width = control.isSpace ? height * 2.83 : height; return DecoratedBox( decoration: BoxDecoration( image: DecorationImage( fit: BoxFit.fill, image: AssetImage( - _control.isSpace + control.isSpace ? Assets.images.components.space.keyName : Assets.images.components.key.keyName, ), @@ -328,9 +296,9 @@ class KeyButton extends StatelessWidget { height: height, child: Center( child: RotatedBox( - quarterTurns: _control.isDown ? 1 : 0, + quarterTurns: control.isDown ? 1 : 0, child: Text( - _control.getCharacter(context), + control.getCharacter(context), style: textStyle?.copyWith(color: PinballColors.white), ), ), diff --git a/lib/start_game/widgets/widgets.dart b/lib/how_to_play/widgets/widgets.dart similarity index 100% rename from lib/start_game/widgets/widgets.dart rename to lib/how_to_play/widgets/widgets.dart diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e1d47b1c..03fde0bd 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -20,7 +20,7 @@ "@flipperControls": { "description": "Text displayed on the how to play dialog with the flipper controls" }, - "tapAndHoldRocket": "Tap & Hold Rocket", + "tapAndHoldRocket": "Tap Rocket", "@tapAndHoldRocket": { "description": "Text displayed on the how to launch on mobile" }, @@ -46,19 +46,19 @@ }, "select": "Select", "@select": { - "description": "Text displayed on the character selection page select button" + "description": "Text displayed on the character selection dialog - select button" }, "space": "Space", "@space": { "description": "Text displayed on space control button" }, - "characterSelectionTitle": "Choose your character!", + "characterSelectionTitle": "Select a Character", "@characterSelectionTitle": { - "description": "Title text displayed on the character selection page" + "description": "Title text displayed on the character selection dialog" }, - "characterSelectionSubtitle": "There’s no wrong answer", + "characterSelectionSubtitle": "There’s no wrong choice!", "@characterSelectionSubtitle": { - "description": "Text displayed on the selecting character dialog at game beginning" + "description": "Subtitle text displayed on the character selection dialog" }, "gameOver": "Game Over", "@gameOver": { @@ -119,5 +119,13 @@ "footerGoogleIOText": "Google I/O", "@footerGoogleIOText": { "description": "Text shown on the footer which mentions Google I/O" + }, + "loading": "Loading", + "@loading": { + "description": "Text shown to indicate loading times" + }, + "ioPinball": "I/O Pinball", + "@ioPinball": { + "description": "I/O Pinball - Name of the game" } -} \ No newline at end of file +} diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 548a81a6..0945f30f 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - // ignore_for_file: public_member_api_docs import 'package:flutter/widgets.dart'; diff --git a/lib/main_development.dart b/lib/main_development.dart index 529c66e2..21166057 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; diff --git a/lib/main_production.dart b/lib/main_production.dart index 529c66e2..21166057 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 529c66e2..21166057 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; diff --git a/lib/select_character/cubit/character_theme_state.dart b/lib/select_character/cubit/character_theme_state.dart index ffe5667c..a1669f69 100644 --- a/lib/select_character/cubit/character_theme_state.dart +++ b/lib/select_character/cubit/character_theme_state.dart @@ -10,6 +10,14 @@ class CharacterThemeState extends Equatable { final CharacterTheme characterTheme; + bool get isSparkySelected => characterTheme == const SparkyTheme(); + + bool get isDashSelected => characterTheme == const DashTheme(); + + bool get isAndroidSelected => characterTheme == const AndroidTheme(); + + bool get isDinoSelected => characterTheme == const DinoTheme(); + @override List get props => [characterTheme]; } diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 863722e6..3b00829b 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -1,139 +1,147 @@ -// ignore_for_file: public_member_api_docs - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; -import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; +/// Inflates [CharacterSelectionDialog] using [showDialog]. +Future showCharacterSelectionDialog(BuildContext context) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const CharacterSelectionDialog(), + ); +} + +/// {@template character_selection_dialog} +/// Dialog used to select the playing character of the game. +/// {@endtemplate character_selection_dialog} class CharacterSelectionDialog extends StatelessWidget { + /// {@macro character_selection_dialog} const CharacterSelectionDialog({Key? key}) : super(key: key); - static Route route() { - return MaterialPageRoute( - builder: (_) => const CharacterSelectionDialog(), - ); - } - @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CharacterThemeCubit(), - child: const CharacterSelectionView(), + final l10n = context.l10n; + return PinballDialog( + title: l10n.characterSelectionTitle, + subtitle: l10n.characterSelectionSubtitle, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + children: [ + Expanded(child: _CharacterPreview()), + Expanded(child: _CharacterGrid()), + ], + ), + ), + const SizedBox(height: 8), + const _SelectCharacterButton(), + ], + ), + ), ); } } -class CharacterSelectionView extends StatelessWidget { - const CharacterSelectionView({Key? key}) : super(key: key); +class _SelectCharacterButton extends StatelessWidget { + const _SelectCharacterButton({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final l10n = context.l10n; + return PinballButton( + onTap: () async { + Navigator.of(context).pop(); + await showHowToPlayDialog(context); + }, + text: l10n.select, + ); + } +} - return PixelatedDecoration( - header: Text( - l10n.characterSelectionTitle, - style: Theme.of(context).textTheme.headline3, - ), - body: SingleChildScrollView( - child: Column( +class _CharacterGrid extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const _CharacterSelectionGridView(), - const SizedBox(height: 20), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - // TODO(arturplaczek): remove after merge StarBlocListener - final height = MediaQuery.of(context).size.height * 0.5; - showDialog( - context: context, - builder: (_) => Center( - child: SizedBox( - height: height, - width: height * 1.4, - child: HowToPlayDialog(), - ), - ), - ); - }, - child: Text(l10n.start), + Column( + children: [ + _Character( + key: const Key('sparky_character_selection'), + character: const SparkyTheme(), + isSelected: state.isSparkySelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('android_character_selection'), + character: const AndroidTheme(), + isSelected: state.isAndroidSelected, + ), + ], + ), + const SizedBox(width: 6), + Column( + children: [ + _Character( + key: const Key('dash_character_selection'), + character: const DashTheme(), + isSelected: state.isDashSelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('dino_character_selection'), + character: const DinoTheme(), + isSelected: state.isDinoSelected, + ), + ], ), ], - ), - ), + ); + }, ); } } -class _CharacterSelectionGridView extends StatelessWidget { - const _CharacterSelectionGridView({Key? key}) : super(key: key); - +class _CharacterPreview extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), - child: GridView.count( - shrinkWrap: true, - crossAxisCount: 2, - mainAxisSpacing: 20, - crossAxisSpacing: 20, - children: const [ - CharacterImageButton( - DashTheme(), - key: Key('characterSelectionPage_dashButton'), - ), - CharacterImageButton( - SparkyTheme(), - key: Key('characterSelectionPage_sparkyButton'), - ), - CharacterImageButton( - AndroidTheme(), - key: Key('characterSelectionPage_androidButton'), - ), - CharacterImageButton( - DinoTheme(), - key: Key('characterSelectionPage_dinoButton'), - ), - ], - ), + return BlocBuilder( + builder: (context, state) { + return SelectedCharacter(currentCharacter: state.characterTheme); + }, ); } } -// TODO(allisonryan0002): remove visibility when adding final UI. -@visibleForTesting -class CharacterImageButton extends StatelessWidget { - const CharacterImageButton( - this.characterTheme, { +class _Character extends StatelessWidget { + const _Character({ Key? key, + required this.character, + required this.isSelected, }) : super(key: key); - final CharacterTheme characterTheme; + final CharacterTheme character; + final bool isSelected; @override Widget build(BuildContext context) { - final currentCharacterTheme = - context.select( - (cubit) => cubit.state.characterTheme, - ); - - return GestureDetector( - onTap: () => - context.read().characterSelected(characterTheme), - child: DecoratedBox( - decoration: BoxDecoration( - color: (currentCharacterTheme == characterTheme) - ? Colors.blue.withOpacity(0.5) - : null, - borderRadius: BorderRadius.circular(6), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: characterTheme.icon.image(), + return Expanded( + child: Opacity( + opacity: isSelected ? 1 : 0.3, + child: TextButton( + onPressed: () => + context.read().characterSelected(character), + child: character.icon.image(fit: BoxFit.contain), ), ), ); diff --git a/lib/select_character/view/selected_character.dart b/lib/select_character/view/selected_character.dart new file mode 100644 index 00000000..68b5ad8a --- /dev/null +++ b/lib/select_character/view/selected_character.dart @@ -0,0 +1,102 @@ +import 'package:flame/components.dart'; +import 'package:flame/flame.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// {@template selected_character} +/// Shows an animated version of the character currently selected. +/// {@endtemplate} +class SelectedCharacter extends StatefulWidget { + /// {@macro selected_character} + const SelectedCharacter({ + Key? key, + required this.currentCharacter, + }) : super(key: key); + + /// The character that is selected at the moment. + final CharacterTheme currentCharacter; + + @override + State createState() => _SelectedCharacterState(); + + /// Returns a list of assets to be loaded. + static List loadAssets() { + return [ + Flame.images.load(const DashTheme().animation.keyName), + Flame.images.load(const AndroidTheme().animation.keyName), + Flame.images.load(const DinoTheme().animation.keyName), + Flame.images.load(const SparkyTheme().animation.keyName), + ]; + } +} + +class _SelectedCharacterState extends State + with TickerProviderStateMixin { + SpriteAnimationController? _controller; + + @override + void initState() { + super.initState(); + _setupCharacterAnimation(); + } + + @override + void didUpdateWidget(covariant SelectedCharacter oldWidget) { + super.didUpdateWidget(oldWidget); + _setupCharacterAnimation(); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + widget.currentCharacter.name, + style: Theme.of(context).textTheme.headline2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: SpriteAnimationWidget( + controller: _controller!, + anchor: Anchor.center, + ), + ); + }, + ), + ), + ], + ); + } + + void _setupCharacterAnimation() { + final spriteSheet = SpriteSheet.fromColumnsAndRows( + image: Flame.images.fromCache(widget.currentCharacter.animation.keyName), + columns: 12, + rows: 6, + ); + final animation = spriteSheet.createAnimation( + row: 0, + stepTime: 1 / 24, + to: spriteSheet.rows * spriteSheet.columns, + ); + if (_controller != null) _controller?.dispose(); + _controller = SpriteAnimationController(vsync: this, animation: animation) + ..forward() + ..repeat(); + } +} diff --git a/lib/select_character/view/view.dart b/lib/select_character/view/view.dart index 1af489b5..41f82053 100644 --- a/lib/select_character/view/view.dart +++ b/lib/select_character/view/view.dart @@ -1 +1,2 @@ export 'character_selection_page.dart'; +export 'selected_character.dart'; diff --git a/lib/start_game/start_game.dart b/lib/start_game/start_game.dart index 1556b533..7171c66d 100644 --- a/lib/start_game/start_game.dart +++ b/lib/start_game/start_game.dart @@ -1,2 +1 @@ export 'bloc/start_game_bloc.dart'; -export 'widgets/widgets.dart'; 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/io_pinball_voice_over.mp3 b/packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3 new file mode 100644 index 00000000..7829086c Binary files /dev/null and b/packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3 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..0f68e170 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -5,16 +5,24 @@ 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 ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.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..f87b05d1 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,18 @@ 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.sfx.ioPinballVoiceOver)), + _preCacheSingleAudio(_prefixFile(Assets.music.background)), + ]); } /// Plays the basic score sound @@ -65,6 +89,16 @@ class PinballAudio { _playSingleAudio(_prefixFile(Assets.sfx.google)); } + /// Plays the I/O Pinball voice over audio. + void ioPinballVoiceOver() { + _playSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)); + } + + /// Plays the background music + void backgroundMusic() { + _loopSingleAudio(_prefixFile(Assets.music.background)); + } + String _prefixFile(String file) { return 'packages/pinball_audio/$file'; } 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..9d6dff98 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,35 @@ 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/sfx/io_pinball_voice_over.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 +167,30 @@ void main() { ).called(1); }); }); + + group('ioPinballVoiceOver', () { + test('plays the correct file', () async { + await audio.load(); + audio.ioPinballVoiceOver(); + + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.ioPinballVoiceOver}', + ), + ).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/dino/top-wall-tunnel.png b/packages/pinball_components/assets/images/dino/top-wall-tunnel.png new file mode 100644 index 00000000..07cb9c46 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/top-wall-tunnel.png differ diff --git a/packages/pinball_components/assets/images/dino/top-wall.png b/packages/pinball_components/assets/images/dino/top-wall.png index cb4c82f2..7ee69411 100644 Binary files a/packages/pinball_components/assets/images/dino/top-wall.png and b/packages/pinball_components/assets/images/dino/top-wall.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter1.png b/packages/pinball_components/assets/images/google_word/letter1.png deleted file mode 100644 index f79ea687..00000000 Binary files a/packages/pinball_components/assets/images/google_word/letter1.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/google_word/letter1/dimmed.png b/packages/pinball_components/assets/images/google_word/letter1/dimmed.png new file mode 100644 index 00000000..dd728a17 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter1/dimmed.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter1/lit.png b/packages/pinball_components/assets/images/google_word/letter1/lit.png new file mode 100644 index 00000000..fa0d0509 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter1/lit.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter2.png b/packages/pinball_components/assets/images/google_word/letter2.png deleted file mode 100644 index e9d205e3..00000000 Binary files a/packages/pinball_components/assets/images/google_word/letter2.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/google_word/letter2/dimmed.png b/packages/pinball_components/assets/images/google_word/letter2/dimmed.png new file mode 100644 index 00000000..4542e517 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter2/dimmed.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter2/lit.png b/packages/pinball_components/assets/images/google_word/letter2/lit.png new file mode 100644 index 00000000..04bf4ca0 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter2/lit.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter3.png b/packages/pinball_components/assets/images/google_word/letter3.png deleted file mode 100644 index e9d205e3..00000000 Binary files a/packages/pinball_components/assets/images/google_word/letter3.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/google_word/letter3/dimmed.png b/packages/pinball_components/assets/images/google_word/letter3/dimmed.png new file mode 100644 index 00000000..9043cfa6 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter3/dimmed.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter3/lit.png b/packages/pinball_components/assets/images/google_word/letter3/lit.png new file mode 100644 index 00000000..f50511ff Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter3/lit.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter4.png b/packages/pinball_components/assets/images/google_word/letter4.png deleted file mode 100644 index f79ea687..00000000 Binary files a/packages/pinball_components/assets/images/google_word/letter4.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/google_word/letter4/dimmed.png b/packages/pinball_components/assets/images/google_word/letter4/dimmed.png new file mode 100644 index 00000000..c30d4eed Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter4/dimmed.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter4/lit.png b/packages/pinball_components/assets/images/google_word/letter4/lit.png new file mode 100644 index 00000000..87fee646 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter4/lit.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter5.png b/packages/pinball_components/assets/images/google_word/letter5.png deleted file mode 100644 index 13f30fb7..00000000 Binary files a/packages/pinball_components/assets/images/google_word/letter5.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/google_word/letter5/dimmed.png b/packages/pinball_components/assets/images/google_word/letter5/dimmed.png new file mode 100644 index 00000000..4bce185b Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter5/dimmed.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter5/lit.png b/packages/pinball_components/assets/images/google_word/letter5/lit.png new file mode 100644 index 00000000..6f88a2bc Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter5/lit.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter6.png b/packages/pinball_components/assets/images/google_word/letter6.png deleted file mode 100644 index 7d87654b..00000000 Binary files a/packages/pinball_components/assets/images/google_word/letter6.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/google_word/letter6/dimmed.png b/packages/pinball_components/assets/images/google_word/letter6/dimmed.png new file mode 100644 index 00000000..48befa49 Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter6/dimmed.png differ diff --git a/packages/pinball_components/assets/images/google_word/letter6/lit.png b/packages/pinball_components/assets/images/google_word/letter6/lit.png new file mode 100644 index 00000000..157cd77d Binary files /dev/null and b/packages/pinball_components/assets/images/google_word/letter6/lit.png differ diff --git a/packages/pinball_components/assets/images/kicker/left.png b/packages/pinball_components/assets/images/kicker/left.png deleted file mode 100644 index 42bd5030..00000000 Binary files a/packages/pinball_components/assets/images/kicker/left.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/kicker/left/dimmed.png b/packages/pinball_components/assets/images/kicker/left/dimmed.png new file mode 100644 index 00000000..70196876 Binary files /dev/null and b/packages/pinball_components/assets/images/kicker/left/dimmed.png differ diff --git a/packages/pinball_components/assets/images/kicker/left/lit.png b/packages/pinball_components/assets/images/kicker/left/lit.png new file mode 100644 index 00000000..d2f57661 Binary files /dev/null and b/packages/pinball_components/assets/images/kicker/left/lit.png differ diff --git a/packages/pinball_components/assets/images/kicker/right.png b/packages/pinball_components/assets/images/kicker/right.png deleted file mode 100644 index 0a746f3c..00000000 Binary files a/packages/pinball_components/assets/images/kicker/right.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/kicker/right/dimmed.png b/packages/pinball_components/assets/images/kicker/right/dimmed.png new file mode 100644 index 00000000..3e4a3b4f Binary files /dev/null and b/packages/pinball_components/assets/images/kicker/right/dimmed.png differ diff --git a/packages/pinball_components/assets/images/kicker/right/lit.png b/packages/pinball_components/assets/images/kicker/right/lit.png new file mode 100644 index 00000000..cfe992b4 Binary files /dev/null and b/packages/pinball_components/assets/images/kicker/right/lit.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/score/five-thousand.png b/packages/pinball_components/assets/images/score/five-thousand.png new file mode 100644 index 00000000..d373e2e1 Binary files /dev/null and b/packages/pinball_components/assets/images/score/five-thousand.png differ diff --git a/packages/pinball_components/assets/images/score/one-million.png b/packages/pinball_components/assets/images/score/one-million.png new file mode 100644 index 00000000..5c7ec15b Binary files /dev/null and b/packages/pinball_components/assets/images/score/one-million.png differ diff --git a/packages/pinball_components/assets/images/score/twenty-thousand.png b/packages/pinball_components/assets/images/score/twenty-thousand.png new file mode 100644 index 00000000..2f9bfd57 Binary files /dev/null and b/packages/pinball_components/assets/images/score/twenty-thousand.png differ diff --git a/packages/pinball_components/assets/images/score/two-hundred-thousand.png b/packages/pinball_components/assets/images/score/two-hundred-thousand.png new file mode 100644 index 00000000..a6f19db4 Binary files /dev/null and b/packages/pinball_components/assets/images/score/two-hundred-thousand.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/assets/images/sparky/bumper/a/inactive.png b/packages/pinball_components/assets/images/sparky/bumper/a/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/a/inactive.png rename to packages/pinball_components/assets/images/sparky/bumper/a/dimmed.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/a/active.png b/packages/pinball_components/assets/images/sparky/bumper/a/lit.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/a/active.png rename to packages/pinball_components/assets/images/sparky/bumper/a/lit.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/b/inactive.png b/packages/pinball_components/assets/images/sparky/bumper/b/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/b/inactive.png rename to packages/pinball_components/assets/images/sparky/bumper/b/dimmed.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/b/active.png b/packages/pinball_components/assets/images/sparky/bumper/b/lit.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/b/active.png rename to packages/pinball_components/assets/images/sparky/bumper/b/lit.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/c/inactive.png b/packages/pinball_components/assets/images/sparky/bumper/c/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/c/inactive.png rename to packages/pinball_components/assets/images/sparky/bumper/c/dimmed.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/c/active.png b/packages/pinball_components/assets/images/sparky/bumper/c/lit.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/c/active.png rename to packages/pinball_components/assets/images/sparky/bumper/c/lit.png diff --git a/packages/pinball_components/assets/images/sparky/computer/glow.png b/packages/pinball_components/assets/images/sparky/computer/glow.png new file mode 100644 index 00000000..07ffdb0c Binary files /dev/null and b/packages/pinball_components/assets/images/sparky/computer/glow.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 251a6e6b..08a8fbbe 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -18,6 +18,7 @@ class $AssetsImagesGen { /// File path: assets/images/board-background.png AssetGenImage get boardBackground => const AssetGenImage('assets/images/board-background.png'); + $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); @@ -27,9 +28,11 @@ 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(); + $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); @@ -117,6 +120,10 @@ class $AssetsImagesDinoGen { AssetGenImage get bottomWall => const AssetGenImage('assets/images/dino/bottom-wall.png'); + /// File path: assets/images/dino/top-wall-tunnel.png + AssetGenImage get topWallTunnel => + const AssetGenImage('assets/images/dino/top-wall-tunnel.png'); + /// File path: assets/images/dino/top-wall.png AssetGenImage get topWall => const AssetGenImage('assets/images/dino/top-wall.png'); @@ -137,41 +144,25 @@ class $AssetsImagesFlipperGen { class $AssetsImagesGoogleWordGen { const $AssetsImagesGoogleWordGen(); - /// File path: assets/images/google_word/letter1.png - AssetGenImage get letter1 => - const AssetGenImage('assets/images/google_word/letter1.png'); - - /// File path: assets/images/google_word/letter2.png - AssetGenImage get letter2 => - const AssetGenImage('assets/images/google_word/letter2.png'); - - /// File path: assets/images/google_word/letter3.png - AssetGenImage get letter3 => - const AssetGenImage('assets/images/google_word/letter3.png'); - - /// File path: assets/images/google_word/letter4.png - AssetGenImage get letter4 => - const AssetGenImage('assets/images/google_word/letter4.png'); - - /// File path: assets/images/google_word/letter5.png - AssetGenImage get letter5 => - const AssetGenImage('assets/images/google_word/letter5.png'); - - /// File path: assets/images/google_word/letter6.png - AssetGenImage get letter6 => - const AssetGenImage('assets/images/google_word/letter6.png'); + $AssetsImagesGoogleWordLetter1Gen get letter1 => + const $AssetsImagesGoogleWordLetter1Gen(); + $AssetsImagesGoogleWordLetter2Gen get letter2 => + const $AssetsImagesGoogleWordLetter2Gen(); + $AssetsImagesGoogleWordLetter3Gen get letter3 => + const $AssetsImagesGoogleWordLetter3Gen(); + $AssetsImagesGoogleWordLetter4Gen get letter4 => + const $AssetsImagesGoogleWordLetter4Gen(); + $AssetsImagesGoogleWordLetter5Gen get letter5 => + const $AssetsImagesGoogleWordLetter5Gen(); + $AssetsImagesGoogleWordLetter6Gen get letter6 => + const $AssetsImagesGoogleWordLetter6Gen(); } class $AssetsImagesKickerGen { const $AssetsImagesKickerGen(); - /// File path: assets/images/kicker/left.png - AssetGenImage get left => - const AssetGenImage('assets/images/kicker/left.png'); - - /// File path: assets/images/kicker/right.png - AssetGenImage get right => - const AssetGenImage('assets/images/kicker/right.png'); + $AssetsImagesKickerLeftGen get left => const $AssetsImagesKickerLeftGen(); + $AssetsImagesKickerRightGen get right => const $AssetsImagesKickerRightGen(); } class $AssetsImagesLaunchRampGen { @@ -190,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(); @@ -212,6 +215,26 @@ class $AssetsImagesPlungerGen { const AssetGenImage('assets/images/plunger/rocket.png'); } +class $AssetsImagesScoreGen { + const $AssetsImagesScoreGen(); + + /// File path: assets/images/score/five-thousand.png + AssetGenImage get fiveThousand => + const AssetGenImage('assets/images/score/five-thousand.png'); + + /// File path: assets/images/score/one-million.png + AssetGenImage get oneMillion => + const AssetGenImage('assets/images/score/one-million.png'); + + /// File path: assets/images/score/twenty-thousand.png + AssetGenImage get twentyThousand => + const AssetGenImage('assets/images/score/twenty-thousand.png'); + + /// File path: assets/images/score/two-hundred-thousand.png + AssetGenImage get twoHundredThousand => + const AssetGenImage('assets/images/score/two-hundred-thousand.png'); +} + class $AssetsImagesSignpostGen { const $AssetsImagesSignpostGen(); @@ -340,6 +363,102 @@ class $AssetsImagesDinoAnimatronicGen { const AssetGenImage('assets/images/dino/animatronic/mouth.png'); } +class $AssetsImagesGoogleWordLetter1Gen { + const $AssetsImagesGoogleWordLetter1Gen(); + + /// File path: assets/images/google_word/letter1/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/google_word/letter1/dimmed.png'); + + /// File path: assets/images/google_word/letter1/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/google_word/letter1/lit.png'); +} + +class $AssetsImagesGoogleWordLetter2Gen { + const $AssetsImagesGoogleWordLetter2Gen(); + + /// File path: assets/images/google_word/letter2/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/google_word/letter2/dimmed.png'); + + /// File path: assets/images/google_word/letter2/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/google_word/letter2/lit.png'); +} + +class $AssetsImagesGoogleWordLetter3Gen { + const $AssetsImagesGoogleWordLetter3Gen(); + + /// File path: assets/images/google_word/letter3/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/google_word/letter3/dimmed.png'); + + /// File path: assets/images/google_word/letter3/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/google_word/letter3/lit.png'); +} + +class $AssetsImagesGoogleWordLetter4Gen { + const $AssetsImagesGoogleWordLetter4Gen(); + + /// File path: assets/images/google_word/letter4/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/google_word/letter4/dimmed.png'); + + /// File path: assets/images/google_word/letter4/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/google_word/letter4/lit.png'); +} + +class $AssetsImagesGoogleWordLetter5Gen { + const $AssetsImagesGoogleWordLetter5Gen(); + + /// File path: assets/images/google_word/letter5/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/google_word/letter5/dimmed.png'); + + /// File path: assets/images/google_word/letter5/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/google_word/letter5/lit.png'); +} + +class $AssetsImagesGoogleWordLetter6Gen { + const $AssetsImagesGoogleWordLetter6Gen(); + + /// File path: assets/images/google_word/letter6/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/google_word/letter6/dimmed.png'); + + /// File path: assets/images/google_word/letter6/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/google_word/letter6/lit.png'); +} + +class $AssetsImagesKickerLeftGen { + const $AssetsImagesKickerLeftGen(); + + /// File path: assets/images/kicker/left/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/kicker/left/dimmed.png'); + + /// File path: assets/images/kicker/left/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/kicker/left/lit.png'); +} + +class $AssetsImagesKickerRightGen { + const $AssetsImagesKickerRightGen(); + + /// File path: assets/images/kicker/right/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/kicker/right/dimmed.png'); + + /// File path: assets/images/kicker/right/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/kicker/right/lit.png'); +} + class $AssetsImagesMultiplierX2Gen { const $AssetsImagesMultiplierX2Gen(); @@ -415,6 +534,10 @@ class $AssetsImagesSparkyComputerGen { AssetGenImage get base => const AssetGenImage('assets/images/sparky/computer/base.png'); + /// File path: assets/images/sparky/computer/glow.png + AssetGenImage get glow => + const AssetGenImage('assets/images/sparky/computer/glow.png'); + /// File path: assets/images/sparky/computer/top.png AssetGenImage get top => const AssetGenImage('assets/images/sparky/computer/top.png'); @@ -523,37 +646,37 @@ class $AssetsImagesDashBumperMainGen { class $AssetsImagesSparkyBumperAGen { const $AssetsImagesSparkyBumperAGen(); - /// File path: assets/images/sparky/bumper/a/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/sparky/bumper/a/active.png'); + /// File path: assets/images/sparky/bumper/a/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/sparky/bumper/a/dimmed.png'); - /// File path: assets/images/sparky/bumper/a/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/sparky/bumper/a/inactive.png'); + /// File path: assets/images/sparky/bumper/a/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/sparky/bumper/a/lit.png'); } class $AssetsImagesSparkyBumperBGen { const $AssetsImagesSparkyBumperBGen(); - /// File path: assets/images/sparky/bumper/b/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/sparky/bumper/b/active.png'); + /// File path: assets/images/sparky/bumper/b/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/sparky/bumper/b/dimmed.png'); - /// File path: assets/images/sparky/bumper/b/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/sparky/bumper/b/inactive.png'); + /// File path: assets/images/sparky/bumper/b/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/sparky/bumper/b/lit.png'); } class $AssetsImagesSparkyBumperCGen { const $AssetsImagesSparkyBumperCGen(); - /// File path: assets/images/sparky/bumper/c/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/sparky/bumper/c/active.png'); + /// File path: assets/images/sparky/bumper/c/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/sparky/bumper/c/dimmed.png'); - /// File path: assets/images/sparky/bumper/c/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/sparky/bumper/c/inactive.png'); + /// File path: assets/images/sparky/bumper/c/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/sparky/bumper/c/lit.png'); } class Assets { 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_bumper/android_bumper.dart b/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart index e1a3857e..7ddabee8 100644 --- a/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart +++ b/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart @@ -5,6 +5,7 @@ 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_bumper/behaviors/behaviors.dart'; +import 'package:pinball_components/src/components/bumping_behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; export 'cubit/android_bumper_cubit.dart'; @@ -12,7 +13,7 @@ export 'cubit/android_bumper_cubit.dart'; /// {@template android_bumper} /// Bumper for area under the [AndroidSpaceship]. /// {@endtemplate} -class AndroidBumper extends BodyComponent with InitialPosition { +class AndroidBumper extends BodyComponent with InitialPosition, ZIndex { /// {@macro android_bumper} AndroidBumper._({ required double majorRadius, @@ -25,7 +26,6 @@ class AndroidBumper extends BodyComponent with InitialPosition { }) : _majorRadius = majorRadius, _minorRadius = minorRadius, super( - priority: RenderPriority.androidBumper, renderBody: false, children: [ AndroidBumperBallContactBehavior(), @@ -38,7 +38,9 @@ class AndroidBumper extends BodyComponent with InitialPosition { ), ...?children, ], - ); + ) { + zIndex = ZIndexes.androidBumper; + } /// {@macro android_bumper} AndroidBumper.a({ @@ -50,7 +52,10 @@ class AndroidBumper extends BodyComponent with InitialPosition { dimmedAssetPath: Assets.images.android.bumper.a.dimmed.keyName, spritePosition: Vector2(0, -0.1), bloc: AndroidBumperCubit(), - children: children, + children: [ + ...?children, + BumpingBehavior(strength: 20), + ], ); /// {@macro android_bumper} @@ -63,7 +68,10 @@ class AndroidBumper extends BodyComponent with InitialPosition { dimmedAssetPath: Assets.images.android.bumper.b.dimmed.keyName, spritePosition: Vector2(0, -0.1), bloc: AndroidBumperCubit(), - children: children, + children: [ + ...?children, + BumpingBehavior(strength: 20), + ], ); /// {@macro android_bumper} @@ -76,7 +84,10 @@ class AndroidBumper extends BodyComponent with InitialPosition { dimmedAssetPath: Assets.images.android.bumper.cow.dimmed.keyName, spritePosition: Vector2(0, -0.68), bloc: AndroidBumperCubit(), - children: children, + children: [ + ...?children, + BumpingBehavior(strength: 20), + ], ); /// Creates an [AndroidBumper] without any children. @@ -112,15 +123,11 @@ class AndroidBumper extends BodyComponent with InitialPosition { majorRadius: _majorRadius, minorRadius: _minorRadius, )..rotate(1.29); - final fixtureDef = FixtureDef( - shape, - restitution: 4, - ); final bodyDef = BodyDef( position: initialPosition, ); - return world.createBody(bodyDef)..createFixture(fixtureDef); + return world.createBody(bodyDef)..createFixtureFromShape(shape); } } 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 62% 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 1dcf6780..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,28 +5,56 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/gen/assets.gen.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; +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'; -class AndroidSpaceship extends Blueprint { - AndroidSpaceship({required Vector2 position}) - : super( - components: [ +export 'cubit/android_spaceship_cubit.dart'; + +class AndroidSpaceship extends Component { + 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: RenderPriority.ballOnSpaceshipRail, + outsidePriority: ZIndexes.ballOnSpaceshipRail, )..initialPosition = position - Vector2(5.3, -5.4), _SpaceshipHole( outsideLayer: Layer.board, - outsidePriority: RenderPriority.ballOnBoard, + outsidePriority: ZIndexes.ballOnBoard, )..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 { @@ -65,12 +93,13 @@ class _SpaceshipSaucerShape extends ChainShape { } class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent - with HasGameRef { + with HasGameRef, ZIndex { _SpaceshipSaucerSpriteAnimationComponent() : super( anchor: Anchor.center, - priority: RenderPriority.spaceshipSaucer, - ); + ) { + zIndex = ZIndexes.spaceshipSaucer; + } @override Future onLoad() async { @@ -101,12 +130,14 @@ class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent } // TODO(allisonryan0002): add pulsing behavior. -class _LightBeamSpriteComponent extends SpriteComponent with HasGameRef { +class _LightBeamSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { _LightBeamSpriteComponent() : super( anchor: Anchor.center, - priority: RenderPriority.spaceshipLightBeam, - ); + ) { + zIndex = ZIndexes.spaceshipLightBeam; + } @override Future onLoad() async { @@ -121,11 +152,11 @@ class _LightBeamSpriteComponent extends SpriteComponent with HasGameRef { } } -class _AndroidHead extends BodyComponent with InitialPosition, Layered { - _AndroidHead() +class AndroidSpaceshipEntrance extends BodyComponent + with ParentIsA, Layered { + AndroidSpaceshipEntrance({Iterable? children}) : super( - priority: RenderPriority.androidHead, - children: [_AndroidHeadSpriteAnimationComponent()], + children: children, renderBody: false, ) { layer = Layer.spaceship; @@ -133,66 +164,31 @@ class _AndroidHead extends BodyComponent with InitialPosition, Layered { @override Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: 3.1, - minorRadius: 2, - )..rotate(1.4); - // TODO(allisonryan0002): use bumping behavior. + final shape = PolygonShape() + ..setAsBox( + 2, + 0.1, + Vector2(-27.4, -37.2), + -0.12, + ); final fixtureDef = FixtureDef( shape, - restitution: 0.1, + isSensor: true, ); - final bodyDef = BodyDef(position: initialPosition); + final bodyDef = BodyDef(); return world.createBody(bodyDef)..createFixture(fixtureDef); } } -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, - ); - - 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, - ), - ); - } -} - class _SpaceshipHole extends LayerSensor { _SpaceshipHole({required Layer outsideLayer, required int outsidePriority}) : super( insideLayer: Layer.spaceship, outsideLayer: outsideLayer, orientation: LayerEntranceOrientation.down, - insidePriority: RenderPriority.ballOnSpaceship, - outsidePriority: outsidePriority, + insideZIndex: ZIndexes.ballOnSpaceship, + outsideZIndex: outsidePriority, ) { layer = Layer.spaceship; } 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/ball.dart b/packages/pinball_components/lib/src/components/ball/ball.dart similarity index 85% rename from packages/pinball_components/lib/src/components/ball.dart rename to packages/pinball_components/lib/src/components/ball/ball.dart index 1c9c1270..4f913c2c 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball/ball.dart @@ -1,17 +1,18 @@ import 'dart:async'; import 'dart:math' as math; -import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/widgets.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/ball/behaviors/ball_scaling_behavior.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template ball} /// A solid, [BodyType.dynamic] sphere that rolls and bounces around. /// {@endtemplate} class Ball extends BodyComponent - with Layered, InitialPosition { + with Layered, InitialPosition, ZIndex { /// {@macro ball} Ball({ required this.baseColor, @@ -19,6 +20,7 @@ class Ball extends BodyComponent renderBody: false, children: [ _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), + BallScalingBehavior(), ], ) { // TODO(ruimiguel): while developing Ball can be launched by clicking mouse, @@ -29,6 +31,15 @@ class Ball extends BodyComponent layer = Layer.board; } + /// Creates a [Ball] without any behaviors. + /// + /// This can be used for testing [Ball]'s behaviors in isolation. + @visibleForTesting + Ball.test({required this.baseColor}) + : super( + children: [_BallSpriteComponent()], + ); + /// The size of the [Ball]. static final Vector2 size = Vector2.all(4.13); @@ -67,7 +78,7 @@ class Ball extends BodyComponent /// /// If previously [stop]ped, the previous ball's velocity is not kept. void resume() { - body.gravityScale = Vector2(0, 1); + body.gravityScale = Vector2(1, 1); } /// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball]. @@ -80,26 +91,9 @@ class Ball extends BodyComponent void update(double dt) { super.update(dt); - _rescaleSize(); _setPositionalGravity(); } - void _rescaleSize() { - final boardHeight = BoardDimensions.bounds.height; - const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; - - final standardizedYPosition = body.position.y + (boardHeight / 2); - - final scaleFactor = maxShrinkValue + - ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue)); - - body.fixtures.first.shape.radius = (size.x / 2) * scaleFactor; - - // TODO(alestiago): Revisit and see if there's a better way to do this. - final spriteComponent = firstChild<_BallSpriteComponent>(); - spriteComponent?.scale = Vector2.all(scaleFactor); - } - void _setPositionalGravity() { final defaultGravity = gameRef.world.gravity.y; final maxXDeviationFromCenter = BoardDimensions.bounds.width / 2; @@ -133,13 +127,14 @@ class _BallSpriteComponent extends SpriteComponent with HasGameRef { } class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent - with HasGameRef { + with HasGameRef, ZIndex { _TurboChargeSpriteAnimationComponent() : super( anchor: const Anchor(0.53, 0.72), - priority: RenderPriority.turboChargeFlame, removeOnFinish: true, - ); + ) { + zIndex = ZIndexes.turboChargeFlame; + } late final Vector2 _textureSize; diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart new file mode 100644 index 00000000..7fc06fb1 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_scaling_behavior.dart @@ -0,0 +1,24 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Scales the ball's body and sprite according to its position on the board. +class BallScalingBehavior extends Component with ParentIsA { + @override + void update(double dt) { + super.update(dt); + final boardHeight = BoardDimensions.bounds.height; + const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; + + final standardizedYPosition = parent.body.position.y + (boardHeight / 2); + final scaleFactor = maxShrinkValue + + ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue)); + + parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor; + + parent.firstChild()!.scale.setValues( + scaleFactor, + scaleFactor, + ); + } +} diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart new file mode 100644 index 00000000..22928734 --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'ball_scaling_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/board_background_sprite_component.dart b/packages/pinball_components/lib/src/components/board_background_sprite_component.dart index 1a4e34c6..ba5b430e 100644 --- a/packages/pinball_components/lib/src/components/board_background_sprite_component.dart +++ b/packages/pinball_components/lib/src/components/board_background_sprite_component.dart @@ -2,14 +2,17 @@ import 'package:flame/components.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; -class BoardBackgroundSpriteComponent extends SpriteComponent with HasGameRef { +class BoardBackgroundSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { BoardBackgroundSpriteComponent() : super( anchor: Anchor.center, - priority: RenderPriority.boardBackground, position: Vector2(0, -1), - ); + ) { + zIndex = ZIndexes.boardBackground; + } @override Future onLoad() async { diff --git a/packages/pinball_components/lib/src/components/boundaries.dart b/packages/pinball_components/lib/src/components/boundaries.dart index 3d0f9445..84d3aaeb 100644 --- a/packages/pinball_components/lib/src/components/boundaries.dart +++ b/packages/pinball_components/lib/src/components/boundaries.dart @@ -4,13 +4,13 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template boundaries} -/// A [Blueprint] which creates the [_BottomBoundary] and [_OuterBoundary]. -///{@endtemplate boundaries} -class Boundaries extends Blueprint { +/// Pinball machine walls. +/// {@endtemplate} +class Boundaries extends Component { /// {@macro boundaries} Boundaries() : super( - components: [ + children: [ _BottomBoundary(), _OuterBoundary(), _OuterBottomBoundarySpriteComponent(), @@ -22,14 +22,15 @@ class Boundaries extends Blueprint { /// Curved boundary at the bottom of the board where the [Ball] exits the field /// of play. /// {@endtemplate bottom_boundary} -class _BottomBoundary extends BodyComponent with InitialPosition { +class _BottomBoundary extends BodyComponent with InitialPosition, ZIndex { /// {@macro bottom_boundary} _BottomBoundary() : super( renderBody: false, - priority: RenderPriority.bottomBoundary, children: [_BottomBoundarySpriteComponent()], - ); + ) { + zIndex = ZIndexes.bottomBoundary; + } List _createFixtureDefs() { final bottomLeftCurve = BezierCurveShape( @@ -84,17 +85,20 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { } /// {@template outer_boundary} -/// Boundary enclosing the top and left side of the board. The right side of the -/// board is closed by the barrier the [LaunchRamp] creates. +/// Boundary enclosing the top and left side of the board. +/// +/// The right side of the board is closed by the barrier the [LaunchRamp] +/// creates. /// {@endtemplate outer_boundary} -class _OuterBoundary extends BodyComponent with InitialPosition { +class _OuterBoundary extends BodyComponent with InitialPosition, ZIndex { /// {@macro outer_boundary} _OuterBoundary() : super( renderBody: false, - priority: RenderPriority.outerBoundary, children: [_OuterBoundarySpriteComponent()], - ); + ) { + zIndex = ZIndexes.outerBoundary; + } List _createFixtureDefs() { final topWall = EdgeShape() @@ -189,13 +193,14 @@ class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef { } class _OuterBottomBoundarySpriteComponent extends SpriteComponent - with HasGameRef { + with HasGameRef, ZIndex { _OuterBottomBoundarySpriteComponent() : super( - priority: RenderPriority.outerBottomBoundary, anchor: Anchor.center, position: Vector2(0, 71), - ); + ) { + zIndex = ZIndexes.outerBottomBoundary; + } @override Future onLoad() async { diff --git a/packages/pinball_components/lib/src/components/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino.dart deleted file mode 100644 index e1a1a1fc..00000000 --- a/packages/pinball_components/lib/src/components/chrome_dino.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'dart:async'; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart' hide Timer; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template chrome_dino} -/// Dino that swivels back and forth, opening its mouth to eat a [Ball]. -/// -/// Upon eating a [Ball], the dino rotates and spits the [Ball] out in a -/// different direction. -/// {@endtemplate} -class ChromeDino extends BodyComponent with InitialPosition { - /// {@macro chrome_dino} - ChromeDino() - : super( - priority: RenderPriority.dino, - renderBody: false, - ); - - /// The size of the dinosaur mouth. - static final size = Vector2(5.5, 5); - - /// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc - /// motion. - Future<_ChromeDinoJoint> _anchorToJoint() async { - // TODO(allisonryan0002): try moving to anchor after new body is defined. - final anchor = _ChromeDinoAnchor() - ..initialPosition = initialPosition + Vector2(9, -4); - - await add(anchor); - - final jointDef = _ChromeDinoAnchorRevoluteJointDef( - chromeDino: this, - anchor: anchor, - ); - final joint = _ChromeDinoJoint(jointDef); - world.createJoint(joint); - - return joint; - } - - @override - Future onLoad() async { - await super.onLoad(); - final joint = await _anchorToJoint(); - const framesInAnimation = 98; - const animationFPS = 1 / 24; - await add( - TimerComponent( - period: (framesInAnimation / 2) * animationFPS, - onTick: joint._swivel, - repeat: true, - ), - ); - } - - List _createFixtureDefs() { - final fixtureDefs = []; - - // TODO(allisonryan0002): Update this shape to better match sprite. - final box = PolygonShape() - ..setAsBox( - size.x / 2, - size.y / 2, - initialPosition + Vector2(-4, 2), - -_ChromeDinoJoint._halfSweepingAngle, - ); - final fixtureDef = FixtureDef(box, density: 1); - fixtureDefs.add(fixtureDef); - - return fixtureDefs; - } - - @override - Body createBody() { - final bodyDef = BodyDef( - position: initialPosition, - type: BodyType.dynamic, - gravityScale: Vector2.zero(), - ); - - final body = world.createBody(bodyDef); - _createFixtureDefs().forEach(body.createFixture); - - return body; - } -} - -class _ChromeDinoAnchor extends JointAnchor { - _ChromeDinoAnchor(); - - // TODO(allisonryan0002): if these aren't moved when fixing the rendering, see - // if the joint can be created in onMount to resolve render syncing. - @override - Future onLoad() async { - await super.onLoad(); - await addAll([ - _ChromeDinoMouthSprite(), - _ChromeDinoHeadSprite(), - ]); - } -} - -/// {@template chrome_dino_anchor_revolute_joint_def} -/// Hinges a [ChromeDino] to a [_ChromeDinoAnchor]. -/// {@endtemplate} -class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef { - /// {@macro chrome_dino_anchor_revolute_joint_def} - _ChromeDinoAnchorRevoluteJointDef({ - required ChromeDino chromeDino, - required _ChromeDinoAnchor anchor, - }) { - initialize( - chromeDino.body, - anchor.body, - chromeDino.body.position + anchor.body.position, - ); - enableLimit = true; - lowerAngle = -_ChromeDinoJoint._halfSweepingAngle; - upperAngle = _ChromeDinoJoint._halfSweepingAngle; - - enableMotor = true; - maxMotorTorque = chromeDino.body.mass * 255; - motorSpeed = 2; - } -} - -class _ChromeDinoJoint extends RevoluteJoint { - _ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def); - - static const _halfSweepingAngle = 0.1143; - - /// Sweeps the [ChromeDino] up and down repeatedly. - void _swivel() { - setMotorSpeed(-motorSpeed); - } -} - -class _ChromeDinoMouthSprite extends SpriteAnimationComponent with HasGameRef { - _ChromeDinoMouthSprite() - : super( - anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), - angle: _ChromeDinoJoint._halfSweepingAngle, - ); - - @override - Future onLoad() async { - await super.onLoad(); - final image = gameRef.images.fromCache( - Assets.images.dino.animatronic.mouth.keyName, - ); - - const amountPerRow = 11; - const amountPerColumn = 9; - final textureSize = Vector2( - image.width / amountPerRow, - image.height / amountPerColumn, - ); - size = textureSize / 10; - - final data = SpriteAnimationData.sequenced( - amount: (amountPerColumn * amountPerRow) - 1, - amountPerRow: amountPerRow, - stepTime: 1 / 24, - textureSize: textureSize, - ); - animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; - } -} - -class _ChromeDinoHeadSprite extends SpriteAnimationComponent with HasGameRef { - _ChromeDinoHeadSprite() - : super( - anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), - angle: _ChromeDinoJoint._halfSweepingAngle, - ); - - @override - Future onLoad() async { - await super.onLoad(); - final image = gameRef.images.fromCache( - Assets.images.dino.animatronic.head.keyName, - ); - - const amountPerRow = 11; - const amountPerColumn = 9; - final textureSize = Vector2( - image.width / amountPerRow, - image.height / amountPerColumn, - ); - size = textureSize / 10; - - final data = SpriteAnimationData.sequenced( - amount: (amountPerColumn * amountPerRow) - 1, - amountPerRow: amountPerRow, - stepTime: 1 / 24, - textureSize: textureSize, - ); - animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; - } -} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/behaviors.dart new file mode 100644 index 00000000..3d4e5bad --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/behaviors.dart @@ -0,0 +1,4 @@ +export 'chrome_dino_chomping_behavior.dart'; +export 'chrome_dino_mouth_opening_behavior.dart'; +export 'chrome_dino_spitting_behavior.dart'; +export 'chrome_dino_swiveling_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart new file mode 100644 index 00000000..eff84ff4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior.dart @@ -0,0 +1,20 @@ +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 chrome_dino_chomping_behavior} +/// Chomps a [Ball] after it has entered the [ChromeDino]'s mouth. +/// +/// The chomped [Ball] is hidden in the mouth until it is spit out. +/// {@endtemplate} +class ChromeDinoChompingBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + other.firstChild()!.setOpacity(0); + parent.bloc.onChomp(other); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior.dart new file mode 100644 index 00000000..6779a5d8 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior.dart @@ -0,0 +1,18 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template chrome_dino_mouth_opening_behavior} +/// Allows a [Ball] to enter the [ChromeDino] mouth when it is open. +/// {@endtemplate} +class ChromeDinoMouthOpeningBehavior extends ContactBehavior { + @override + void preSolve(Object other, Contact contact, Manifold oldManifold) { + super.preSolve(other, contact, oldManifold); + if (other is! Ball) return; + + if (parent.bloc.state.isMouthOpen && parent.firstChild() == null) { + contact.setEnabled(false); + } + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart new file mode 100644 index 00000000..78a8b9d5 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior.dart @@ -0,0 +1,44 @@ +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 chrome_dino_spitting_behavior} +/// Spits the [Ball] from the [ChromeDino] the next time the mouth opens. +/// {@endtemplate} +class ChromeDinoSpittingBehavior extends Component + with ContactCallbacks, ParentIsA { + bool _waitingForSwivel = true; + + void _onNewState(ChromeDinoState state) { + if (state.status == ChromeDinoStatus.chomping) { + if (state.isMouthOpen && !_waitingForSwivel) { + add( + TimerComponent( + period: 0.4, + onTick: _spit, + removeOnFinish: true, + ), + ); + _waitingForSwivel = true; + } + if (_waitingForSwivel && !state.isMouthOpen) { + _waitingForSwivel = false; + } + } + } + + void _spit() { + parent.bloc.state.ball! + ..firstChild()!.setOpacity(1) + ..body.linearVelocity = Vector2(-50, 0); + parent.bloc.onSpit(); + } + + @override + Future onLoad() async { + await super.onLoad(); + + parent.bloc.stream.listen(_onNewState); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior.dart b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior.dart new file mode 100644 index 00000000..ab98c6a8 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior.dart @@ -0,0 +1,90 @@ +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 chrome_dino_swivel_behavior} +/// Swivels the [ChromeDino] up and down periodically to match its animation +/// sequence. +/// {@endtemplate} +class ChromeDinoSwivelingBehavior extends TimerComponent + with ParentIsA { + /// {@macro chrome_dino_swivel_behavior} + ChromeDinoSwivelingBehavior() + : super( + period: 98 / 48, + repeat: true, + ); + + late final RevoluteJoint _joint; + + @override + Future onLoad() async { + final anchor = _ChromeDinoAnchor() + ..initialPosition = parent.initialPosition + Vector2(9, -4); + await add(anchor); + + final jointDef = _ChromeDinoAnchorRevoluteJointDef( + chromeDino: parent, + anchor: anchor, + ); + _joint = RevoluteJoint(jointDef); + parent.world.createJoint(_joint); + } + + @override + void update(double dt) { + super.update(dt); + + final angle = _joint.jointAngle(); + + if (angle < _joint.upperLimit && + angle > _joint.lowerLimit && + parent.bloc.state.isMouthOpen) { + parent.bloc.onCloseMouth(); + } else if ((angle >= _joint.upperLimit || angle <= _joint.lowerLimit) && + !parent.bloc.state.isMouthOpen) { + parent.bloc.onOpenMouth(); + } + } + + @override + void onTick() { + super.onTick(); + _joint.setMotorSpeed(-_joint.motorSpeed); + } +} + +class _ChromeDinoAnchor extends JointAnchor + with ParentIsA { + @override + void onMount() { + super.onMount(); + parent.parent.children + .whereType() + .forEach((sprite) { + sprite.animation!.currentIndex = 45; + sprite.changeParent(this); + }); + } +} + +class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef { + _ChromeDinoAnchorRevoluteJointDef({ + required ChromeDino chromeDino, + required _ChromeDinoAnchor anchor, + }) { + initialize( + chromeDino.body, + anchor.body, + chromeDino.body.position + anchor.body.position, + ); + enableLimit = true; + lowerAngle = -ChromeDino.halfSweepingAngle; + upperAngle = ChromeDino.halfSweepingAngle; + + enableMotor = true; + maxMotorTorque = chromeDino.body.mass * 255; + motorSpeed = 2; + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino/chrome_dino.dart new file mode 100644 index 00000000..38a335b9 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/chrome_dino.dart @@ -0,0 +1,207 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Timer; +import 'package:flutter/material.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/chrome_dino_cubit.dart'; + +/// {@template chrome_dino} +/// Dino that swivels back and forth, opening its mouth to eat a [Ball]. +/// +/// Upon eating a [Ball], the dino rotates and spits the [Ball] out in the +/// opposite direction. +/// {@endtemplate} +class ChromeDino extends BodyComponent + with InitialPosition, ContactCallbacks, ZIndex { + /// {@macro chrome_dino} + ChromeDino({Iterable? children}) + : bloc = ChromeDinoCubit(), + super( + children: [ + _ChromeDinoMouthSprite(), + _ChromeDinoHeadSprite(), + ChromeDinoMouthOpeningBehavior()..applyTo(['mouth_opening']), + ChromeDinoSwivelingBehavior(), + ChromeDinoChompingBehavior()..applyTo(['inside_mouth']), + ChromeDinoSpittingBehavior(), + ...?children, + ], + renderBody: false, + ) { + zIndex = ZIndexes.dino; + } + + /// Creates a [ChromeDino] without any children. + /// + /// This can be used for testing [ChromeDino]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + ChromeDino.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 ChromeDinoCubit bloc; + + /// Angle to rotate the dino up or down from the starting horizontal position. + static const halfSweepingAngle = 0.1143; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + List _createFixtureDefs() { + const mouthAngle = -(halfSweepingAngle + 0.28); + final size = Vector2(5.5, 6); + + final topEdge = PolygonShape() + ..setAsBox( + size.x / 2, + 0.1, + initialPosition + Vector2(-4.2, -1.4), + mouthAngle, + ); + final topEdgeFixtureDef = FixtureDef(topEdge, density: 100); + + final backEdge = PolygonShape() + ..setAsBox( + 0.1, + size.y / 2, + initialPosition + Vector2(-1.3, 0.5), + -halfSweepingAngle, + ); + final backEdgeFixtureDef = FixtureDef(backEdge, density: 100); + + final bottomEdge = PolygonShape() + ..setAsBox( + size.x / 2, + 0.1, + initialPosition + Vector2(-3.5, 4.7), + mouthAngle, + ); + final bottomEdgeFixtureDef = FixtureDef( + bottomEdge, + density: 100, + ); + + final mouthOpeningEdge = PolygonShape() + ..setAsBox( + 0.1, + size.y / 2.5, + initialPosition + Vector2(-6.4, 2.7), + -halfSweepingAngle, + ); + final mouthOpeningEdgeFixtureDef = FixtureDef( + mouthOpeningEdge, + density: 0.1, + userData: 'mouth_opening', + ); + + final insideSensor = PolygonShape() + ..setAsBox( + 0.2, + 0.2, + initialPosition + Vector2(-3.5, 1.5), + 0, + ); + final insideSensorFixtureDef = FixtureDef( + insideSensor, + isSensor: true, + userData: 'inside_mouth', + ); + + return [ + topEdgeFixtureDef, + backEdgeFixtureDef, + bottomEdgeFixtureDef, + mouthOpeningEdgeFixtureDef, + insideSensorFixtureDef, + ]; + } + + @override + Body createBody() { + final bodyDef = BodyDef( + position: initialPosition, + type: BodyType.dynamic, + gravityScale: Vector2.zero(), + ); + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} + +class _ChromeDinoMouthSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoMouthSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: ChromeDino.halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.mouth.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data); + } +} + +class _ChromeDinoHeadSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoHeadSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: ChromeDino.halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.head.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data); + } +} 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 new file mode 100644 index 00000000..649e804b --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_cubit.dart @@ -0,0 +1,34 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball_components/pinball_components.dart'; + +part 'chrome_dino_state.dart'; + +class ChromeDinoCubit extends Cubit { + ChromeDinoCubit() : super(const ChromeDinoState.inital()); + + void onOpenMouth() { + emit(state.copyWith(isMouthOpen: true)); + } + + void onCloseMouth() { + emit(state.copyWith(isMouthOpen: false)); + } + + void onChomp(Ball ball) { + if (ball != state.ball) { + emit(state.copyWith(status: ChromeDinoStatus.chomping, ball: ball)); + } + } + + void onSpit() { + emit( + ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: state.isMouthOpen, + ), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart new file mode 100644 index 00000000..a5d3b183 --- /dev/null +++ b/packages/pinball_components/lib/src/components/chrome_dino/cubit/chrome_dino_state.dart @@ -0,0 +1,46 @@ +// ignore_for_file: public_member_api_docs + +part of 'chrome_dino_cubit.dart'; + +enum ChromeDinoStatus { + idle, + chomping, +} + +class ChromeDinoState extends Equatable { + const ChromeDinoState({ + required this.status, + required this.isMouthOpen, + this.ball, + }); + + const ChromeDinoState.inital() + : this( + status: ChromeDinoStatus.idle, + isMouthOpen: false, + ); + + final ChromeDinoStatus status; + final bool isMouthOpen; + final Ball? ball; + + ChromeDinoState copyWith({ + ChromeDinoStatus? status, + bool? isMouthOpen, + Ball? ball, + }) { + final state = ChromeDinoState( + status: status ?? this.status, + isMouthOpen: isMouthOpen ?? this.isMouthOpen, + ball: ball ?? this.ball, + ); + return state; + } + + @override + List get props => [ + status, + isMouthOpen, + ball, + ]; +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index f107e53e..8f915f88 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,13 +1,14 @@ +export 'android_animatronic.dart'; export 'android_bumper/android_bumper.dart'; -export 'android_spaceship.dart'; -export 'ball.dart'; +export 'android_spaceship/android_spaceship.dart'; +export 'ball/ball.dart'; export 'baseboard.dart'; export 'board_background_sprite_component.dart'; export 'board_dimensions.dart'; export 'board_side.dart'; export 'boundaries.dart'; export 'camera_zoom.dart'; -export 'chrome_dino.dart'; +export 'chrome_dino/chrome_dino.dart'; export 'dash_animatronic.dart'; export 'dash_nest_bumper/dash_nest_bumper.dart'; export 'dino_walls.dart'; @@ -16,20 +17,21 @@ export 'flipper.dart'; export 'google_letter/google_letter.dart'; export 'initial_position.dart'; export 'joint_anchor.dart'; -export 'kicker.dart'; +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 'render_priority.dart'; export 'rocket.dart'; -export 'score_text.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'; export 'sparky_animatronic.dart'; export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_computer.dart'; +export 'z_indexes.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart index 82ec0036..208936c8 100644 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart @@ -4,6 +4,7 @@ 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/bumping_behavior.dart'; import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -47,8 +48,11 @@ class DashNestBumper extends BodyComponent with InitialPosition { activeAssetPath: Assets.images.dash.bumper.main.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName, spritePosition: Vector2(0, -0.3), - children: children, bloc: DashNestBumperCubit(), + children: [ + ...?children, + BumpingBehavior(strength: 20), + ], ); /// {@macro dash_nest_bumper} @@ -60,8 +64,11 @@ class DashNestBumper extends BodyComponent with InitialPosition { activeAssetPath: Assets.images.dash.bumper.a.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, spritePosition: Vector2(0.35, -1.2), - children: children, bloc: DashNestBumperCubit(), + children: [ + ...?children, + BumpingBehavior(strength: 20), + ], ); /// {@macro dash_nest_bumper} @@ -73,8 +80,11 @@ class DashNestBumper extends BodyComponent with InitialPosition { activeAssetPath: Assets.images.dash.bumper.b.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, spritePosition: Vector2(0.35, -1.2), - children: children, bloc: DashNestBumperCubit(), + children: [ + ...?children, + BumpingBehavior(strength: 20), + ], ); /// Creates an [DashNestBumper] without any children. @@ -108,13 +118,11 @@ class DashNestBumper extends BodyComponent with InitialPosition { majorRadius: _majorRadius, minorRadius: _minorRadius, )..rotate(math.pi / 1.9); - final fixtureDef = FixtureDef(shape, restitution: 4); final bodyDef = BodyDef( position: initialPosition, - userData: this, ); - return world.createBody(bodyDef)..createFixture(fixtureDef); + return world.createBody(bodyDef)..createFixtureFromShape(shape); } } diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index 39824490..5125d4bc 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -6,14 +6,14 @@ import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_flame/pinball_flame.dart'; -/// {@template dinowalls} -/// A [Blueprint] which creates walls for the [ChromeDino]. +/// {@template dino_walls} +/// Walls near the [ChromeDino]. /// {@endtemplate} -class DinoWalls extends Blueprint { - /// {@macro dinowalls} +class DinoWalls extends Component { + /// {@macro dino_walls} DinoWalls() : super( - components: [ + children: [ _DinoTopWall(), _DinoBottomWall(), ], @@ -27,54 +27,66 @@ class _DinoTopWall extends BodyComponent with InitialPosition { ///{@macro dino_top_wall} _DinoTopWall() : super( - priority: RenderPriority.dinoTopWall, - children: [_DinoTopWallSpriteComponent()], + children: [ + _DinoTopWallSpriteComponent(), + _DinoTopWallTunnelSpriteComponent(), + ], renderBody: false, ); List _createFixtureDefs() { - final topStraightShape = EdgeShape() + final topEdgeShape = EdgeShape() ..set( - Vector2(28.65, -34.3), - Vector2(29.5, -34.3), + Vector2(29.25, -35.27), + Vector2(28.4, -34.77), ); final topCurveShape = BezierCurveShape( controlPoints: [ - topStraightShape.vertex1, - Vector2(18.8, -26.2), - Vector2(26.6, -20.2), + topEdgeShape.vertex2, + Vector2(21.35, -28.72), + Vector2(23.45, -24.62), ], ); - final middleCurveShape = BezierCurveShape( - controlPoints: [ + final tunnelTopEdgeShape = EdgeShape() + ..set( topCurveShape.vertices.last, - Vector2(27.8, -19.3), - Vector2(26.8, -18.7), - ], - ); + Vector2(30.35, -27.32), + ); - final bottomCurveShape = BezierCurveShape( - controlPoints: [ - middleCurveShape.vertices.last, - Vector2(23, -14.2), - Vector2(27, -14.2), - ], - ); + final tunnelBottomEdgeShape = EdgeShape() + ..set( + Vector2(30.75, -23.17), + Vector2(25.45, -21.22), + ); + + final middleEdgeShape = EdgeShape() + ..set( + tunnelBottomEdgeShape.vertex2, + Vector2(27.45, -19.32), + ); + + final bottomEdgeShape = EdgeShape() + ..set( + middleEdgeShape.vertex2, + Vector2(24.65, -15.02), + ); - final bottomStraightShape = EdgeShape() + final undersideEdgeShape = EdgeShape() ..set( - bottomCurveShape.vertices.last, - Vector2(31, -13.7), + bottomEdgeShape.vertex2, + Vector2(31.75, -13.77), ); return [ - FixtureDef(topStraightShape), + FixtureDef(topEdgeShape), FixtureDef(topCurveShape), - FixtureDef(middleCurveShape), - FixtureDef(bottomCurveShape), - FixtureDef(bottomStraightShape), + FixtureDef(tunnelTopEdgeShape), + FixtureDef(tunnelBottomEdgeShape), + FixtureDef(middleEdgeShape), + FixtureDef(bottomEdgeShape), + FixtureDef(undersideEdgeShape), ]; } @@ -86,19 +98,21 @@ class _DinoTopWall extends BodyComponent with InitialPosition { ); final body = world.createBody(bodyDef); - _createFixtureDefs().forEach( - (fixture) => body.createFixture( - fixture - ..restitution = 0.1 - ..friction = 0, - ), - ); + _createFixtureDefs().forEach(body.createFixture); return body; } } -class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { +class _DinoTopWallSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _DinoTopWallSpriteComponent() + : super( + position: Vector2(22.75, -38.07), + ) { + zIndex = ZIndexes.dinoTopWall; + } + @override Future onLoad() async { await super.onLoad(); @@ -109,24 +123,44 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(22.8, -38.1); + } +} + +class _DinoTopWallTunnelSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _DinoTopWallTunnelSpriteComponent() + : super(position: Vector2(23.31, -26.01)) { + zIndex = ZIndexes.dinoTopWallTunnel; + } + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.topWallTunnel.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; } } /// {@template dino_bottom_wall} /// Wall segment located below [ChromeDino]. /// {@endtemplate} -class _DinoBottomWall extends BodyComponent with InitialPosition { +class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex { ///{@macro dino_top_wall} _DinoBottomWall() : super( - priority: RenderPriority.dinoBottomWall, children: [_DinoBottomWallSpriteComponent()], renderBody: false, - ); + ) { + zIndex = ZIndexes.dinoBottomWall; + } List _createFixtureDefs() { - final topStraightShape = EdgeShape() + final topEdgeShape = EdgeShape() ..set( Vector2(32.4, -8.8), Vector2(25, -7.7), @@ -134,29 +168,29 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { final topLeftCurveShape = BezierCurveShape( controlPoints: [ - topStraightShape.vertex2, + topEdgeShape.vertex2, Vector2(21.8, -7), Vector2(29.8, 13.8), ], ); - final bottomLeftStraightShape = EdgeShape() + final bottomLeftEdgeShape = EdgeShape() ..set( topLeftCurveShape.vertices.last, Vector2(31.9, 44.1), ); - final bottomStraightShape = EdgeShape() + final bottomEdgeShape = EdgeShape() ..set( - bottomLeftStraightShape.vertex2, + bottomLeftEdgeShape.vertex2, Vector2(37.8, 44.1), ); return [ - FixtureDef(topStraightShape), + FixtureDef(topEdgeShape), FixtureDef(topLeftCurveShape), - FixtureDef(bottomLeftStraightShape), - FixtureDef(bottomStraightShape), + FixtureDef(bottomLeftEdgeShape), + FixtureDef(bottomEdgeShape), ]; } diff --git a/packages/pinball_components/lib/src/components/fire_effect.dart b/packages/pinball_components/lib/src/components/fire_effect.dart index 14639527..e793b3e6 100644 --- a/packages/pinball_components/lib/src/components/fire_effect.dart +++ b/packages/pinball_components/lib/src/components/fire_effect.dart @@ -26,10 +26,8 @@ class FireEffect extends ParticleSystemComponent { required this.burstPower, required this.direction, Vector2? position, - int? priority, }) : super( position: position, - priority: priority, ); /// A [double] value that will define how "strong" the burst of particles diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index bb982e96..b62d2390 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -24,7 +24,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// The speed required to move the [Flipper] to its highest position. /// /// The higher the value, the faster the [Flipper] will move. - static const double _speed = 60; + static const double _speed = 90; /// Whether the [Flipper] is on the left or right side of the board. /// diff --git a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart index a352e98d..9ef219ff 100644 --- a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart +++ b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart @@ -5,13 +5,13 @@ import 'package:bloc/bloc.dart'; part 'google_letter_state.dart'; class GoogleLetterCubit extends Cubit { - GoogleLetterCubit() : super(GoogleLetterState.inactive); + GoogleLetterCubit() : super(GoogleLetterState.dimmed); void onBallContacted() { - emit(GoogleLetterState.active); + emit(GoogleLetterState.lit); } void onReset() { - emit(GoogleLetterState.inactive); + emit(GoogleLetterState.dimmed); } } diff --git a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart index e1339320..1e5a29e8 100644 --- a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart +++ b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart @@ -1,10 +1,8 @@ +// ignore_for_file: public_member_api_docs + part of 'google_letter_cubit.dart'; -/// Indicates the [GoogleLetterCubit]'s current state. enum GoogleLetterState { - /// A lit up letter. - active, - - /// A dimmed letter. - inactive, + lit, + dimmed, } diff --git a/packages/pinball_components/lib/src/components/google_letter/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart index a865acf8..16218fa3 100644 --- a/packages/pinball_components/lib/src/components/google_letter/google_letter.dart +++ b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart @@ -7,6 +7,33 @@ import 'package:pinball_flame/pinball_flame.dart'; export 'cubit/google_letter_cubit.dart'; +final _spritePaths = >[ + { + GoogleLetterState.lit: Assets.images.googleWord.letter1.lit.keyName, + GoogleLetterState.dimmed: Assets.images.googleWord.letter1.dimmed.keyName, + }, + { + GoogleLetterState.lit: Assets.images.googleWord.letter2.lit.keyName, + GoogleLetterState.dimmed: Assets.images.googleWord.letter2.dimmed.keyName, + }, + { + GoogleLetterState.lit: Assets.images.googleWord.letter3.lit.keyName, + GoogleLetterState.dimmed: Assets.images.googleWord.letter3.dimmed.keyName, + }, + { + GoogleLetterState.lit: Assets.images.googleWord.letter4.lit.keyName, + GoogleLetterState.dimmed: Assets.images.googleWord.letter4.dimmed.keyName, + }, + { + GoogleLetterState.lit: Assets.images.googleWord.letter5.lit.keyName, + GoogleLetterState.dimmed: Assets.images.googleWord.letter5.dimmed.keyName, + }, + { + GoogleLetterState.lit: Assets.images.googleWord.letter6.lit.keyName, + GoogleLetterState.dimmed: Assets.images.googleWord.letter6.dimmed.keyName, + }, +]; + /// {@template google_letter} /// Circular sensor that represents a letter in "GOOGLE" for a given index. /// {@endtemplate} @@ -15,13 +42,27 @@ class GoogleLetter extends BodyComponent with InitialPosition { GoogleLetter( int index, { Iterable? children, - }) : bloc = GoogleLetterCubit(), - super( + }) : this._( + index, + bloc: GoogleLetterCubit(), + children: children, + ); + + GoogleLetter._( + int index, { + required this.bloc, + Iterable? children, + }) : super( children: [ + _GoogleLetterSpriteGroupComponent( + litAssetPath: _spritePaths[index][GoogleLetterState.lit]!, + dimmedAssetPath: _spritePaths[index][GoogleLetterState.dimmed]!, + current: bloc.state, + ), GoogleLetterBallContactBehavior(), - _GoogleLetterSprite(_GoogleLetterSprite.spritePaths[index]), ...?children, ], + renderBody: false, ); /// Creates a [GoogleLetter] without any children. @@ -61,33 +102,37 @@ class GoogleLetter extends BodyComponent with InitialPosition { } } -class _GoogleLetterSprite extends SpriteComponent +class _GoogleLetterSpriteGroupComponent + extends SpriteGroupComponent with HasGameRef, ParentIsA { - _GoogleLetterSprite(String path) - : _path = path, - super(anchor: Anchor.center); - - static final spritePaths = [ - Assets.images.googleWord.letter1.keyName, - Assets.images.googleWord.letter2.keyName, - Assets.images.googleWord.letter3.keyName, - Assets.images.googleWord.letter4.keyName, - Assets.images.googleWord.letter5.keyName, - Assets.images.googleWord.letter6.keyName, - ]; + _GoogleLetterSpriteGroupComponent({ + required String litAssetPath, + required String dimmedAssetPath, + required GoogleLetterState current, + }) : _litAssetPath = litAssetPath, + _dimmedAssetPath = dimmedAssetPath, + super( + anchor: Anchor.center, + current: current, + ); - final String _path; + final String _litAssetPath; + final String _dimmedAssetPath; @override Future onLoad() async { await super.onLoad(); - // TODO(alisonryan2002): Make SpriteGroupComponent. - // parent.bloc.stream.listen(); + parent.bloc.stream.listen((state) => current = state); - // TODO(alestiago): Used cached assets. - final sprite = await gameRef.loadSprite(_path); - this.sprite = sprite; - // TODO(alestiago): Size correctly once the assets are provided. - size = sprite.originalSize / 5; + final sprites = { + GoogleLetterState.lit: Sprite( + gameRef.images.fromCache(_litAssetPath), + ), + GoogleLetterState.dimmed: Sprite( + gameRef.images.fromCache(_dimmedAssetPath), + ), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; } } diff --git a/packages/pinball_components/lib/src/components/kicker/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/kicker/behaviors/behaviors.dart new file mode 100644 index 00000000..e1098a34 --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'kicker_ball_contact_behavior.dart'; +export 'kicker_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_ball_contact_behavior.dart new file mode 100644 index 00000000..d5d2eb6c --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_ball_contact_behavior.dart @@ -0,0 +1,14 @@ +// 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 KickerBallContactBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} diff --git a/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_blinking_behavior.dart b/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_blinking_behavior.dart new file mode 100644 index 00000000..569d461f --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_blinking_behavior.dart @@ -0,0 +1,37 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template kicker_blinking_behavior} +/// Makes a [Kicker] blink back to [KickerState.lit] when [KickerState.dimmed]. +/// {@endtemplate} +class KickerBlinkingBehavior extends TimerComponent with ParentIsA { + /// {@macro kicker_blinking_behavior} + KickerBlinkingBehavior() : super(period: 0.05); + + void _onNewState(KickerState state) { + switch (state) { + case KickerState.lit: + break; + case KickerState.dimmed: + timer + ..reset() + ..start(); + break; + } + } + + @override + Future onLoad() async { + await super.onLoad(); + timer.stop(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + timer.stop(); + parent.bloc.onBlinked(); + } +} diff --git a/packages/pinball_components/lib/src/components/kicker/cubit/kicker_cubit.dart b/packages/pinball_components/lib/src/components/kicker/cubit/kicker_cubit.dart new file mode 100644 index 00000000..488f4683 --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/cubit/kicker_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'kicker_state.dart'; + +class KickerCubit extends Cubit { + KickerCubit() : super(KickerState.lit); + + void onBallContacted() { + emit(KickerState.dimmed); + } + + void onBlinked() { + emit(KickerState.lit); + } +} diff --git a/packages/pinball_components/lib/src/components/kicker/cubit/kicker_state.dart b/packages/pinball_components/lib/src/components/kicker/cubit/kicker_state.dart new file mode 100644 index 00000000..08d52709 --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/cubit/kicker_state.dart @@ -0,0 +1,8 @@ +// ignore_for_file: public_member_api_docs + +part of 'kicker_cubit.dart'; + +enum KickerState { + lit, + dimmed, +} diff --git a/packages/pinball_components/lib/src/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker/kicker.dart similarity index 55% rename from packages/pinball_components/lib/src/components/kicker.dart rename to packages/pinball_components/lib/src/components/kicker/kicker.dart index 527ffde4..1a45ad60 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker/kicker.dart @@ -2,9 +2,15 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; import 'package:geometry/geometry.dart' as geometry show centroid; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/src/components/bumping_behavior.dart'; +import 'package:pinball_components/src/components/kicker/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/kicker_cubit.dart'; /// {@template kicker} /// Triangular [BodyType.static] body that propels the [Ball] towards the @@ -17,42 +23,69 @@ class Kicker extends BodyComponent with InitialPosition { Kicker({ required BoardSide side, Iterable? children, + }) : this._( + side: side, + bloc: KickerCubit(), + children: children, + ); + + Kicker._({ + required BoardSide side, + required this.bloc, + Iterable? children, }) : _side = side, super( children: [ - _KickerSpriteComponent(side: side), + BumpingBehavior(strength: 25)..applyTo(['bouncy_edge']), + KickerBallContactBehavior()..applyTo(['bouncy_edge']), + KickerBlinkingBehavior(), + _KickerSpriteGroupComponent( + side: side, + state: bloc.state, + ), ...?children, ], renderBody: false, ); - /// The size of the [Kicker] body. - static final Vector2 size = Vector2(4.4, 15); + /// Creates a [Kicker] without any children. + /// + /// This can be used for testing [Kicker]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + Kicker.test({ + required this.bloc, + required BoardSide side, + }) : _side = side; + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final KickerCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } /// Whether the [Kicker] is on the left or right side of the board. - /// - /// A [Kicker] with [BoardSide.left] propels the [Ball] to the right, - /// whereas a [Kicker] with [BoardSide.right] propels the [Ball] to the - /// left. final BoardSide _side; List _createFixtureDefs() { - final fixturesDefs = []; final direction = _side.direction; const quarterPi = math.pi / 4; + final size = Vector2(4.4, 15); final upperCircle = CircleShape()..radius = 1.6; upperCircle.position.setValues(0, upperCircle.radius / 2); - final upperCircleFixtureDef = FixtureDef(upperCircle); - fixturesDefs.add(upperCircleFixtureDef); final lowerCircle = CircleShape()..radius = 1.6; lowerCircle.position.setValues( size.x * -direction, size.y + 0.8, ); - final lowerCircleFixtureDef = FixtureDef(lowerCircle); - fixturesDefs.add(lowerCircleFixtureDef); final wallFacingEdge = EdgeShape() ..set( @@ -63,8 +96,6 @@ class Kicker extends BodyComponent with InitialPosition { ), Vector2(2.5 * direction, size.y - 2), ); - final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge); - fixturesDefs.add(wallFacingLineFixtureDef); final bottomEdge = EdgeShape() ..set( @@ -75,8 +106,6 @@ class Kicker extends BodyComponent with InitialPosition { lowerCircle.radius * math.sin(quarterPi), ), ); - final bottomLineFixtureDef = FixtureDef(bottomEdge); - fixturesDefs.add(bottomLineFixtureDef); final bouncyEdge = EdgeShape() ..set( @@ -92,12 +121,13 @@ class Kicker extends BodyComponent with InitialPosition { ), ); - final bouncyFixtureDef = FixtureDef( - bouncyEdge, - // TODO(alestiago): Play with restitution value once game is bundled. - restitution: 10, - ); - fixturesDefs.add(bouncyFixtureDef); + final fixturesDefs = [ + FixtureDef(upperCircle), + FixtureDef(lowerCircle), + FixtureDef(wallFacingEdge), + FixtureDef(bottomEdge), + FixtureDef(bouncyEdge, userData: 'bouncy_edge'), + ]; // TODO(alestiago): Evaluate if there is value on centering the fixtures. final centroid = geometry.centroid( @@ -130,25 +160,46 @@ class Kicker extends BodyComponent with InitialPosition { } } -class _KickerSpriteComponent extends SpriteComponent with HasGameRef { - _KickerSpriteComponent({required BoardSide side}) : _side = side; +class _KickerSpriteGroupComponent extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _KickerSpriteGroupComponent({ + required BoardSide side, + required KickerState state, + }) : _side = side, + super( + anchor: Anchor.center, + position: Vector2(0.7 * -side.direction, -2.2), + current: state, + ); final BoardSide _side; @override Future onLoad() async { await super.onLoad(); - - // TODO(alestiago): Used cached asset. - final sprite = await gameRef.loadSprite( - (_side.isLeft) - ? Assets.images.kicker.left.keyName - : Assets.images.kicker.right.keyName, - ); - this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(0.7 * -_side.direction, -2.2); + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + parent.bloc.stream.listen((state) => current = state); + + final sprites = { + KickerState.lit: Sprite( + gameRef.images.fromCache( + (_side.isLeft) + ? Assets.images.kicker.left.lit.keyName + : Assets.images.kicker.right.lit.keyName, + ), + ), + KickerState.dimmed: Sprite( + gameRef.images.fromCache( + (_side.isLeft) + ? Assets.images.kicker.left.dimmed.keyName + : Assets.images.kicker.right.dimmed.keyName, + ), + ), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; } } diff --git a/packages/pinball_components/lib/src/components/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index 13f063b6..4713c3a2 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -8,14 +8,13 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template launch_ramp} -/// A [Blueprint] which creates the [_LaunchRampBase] and -/// [_LaunchRampForegroundRailing]. +/// Ramp where the ball is launched from. /// {@endtemplate} -class LaunchRamp extends Blueprint { +class LaunchRamp extends Component { /// {@macro launch_ramp} LaunchRamp() : super( - components: [ + children: [ _LaunchRampBase(), _LaunchRampForegroundRailing(), _LaunchRampExit()..initialPosition = Vector2(0.6, -34), @@ -24,20 +23,16 @@ class LaunchRamp extends Blueprint { ); } -/// {@template launch_ramp_base} -/// Ramp the [Ball] is launched from at the beginning of each ball life. -/// {@endtemplate} -class _LaunchRampBase extends BodyComponent with Layered { - /// {@macro launch_ramp_base} +class _LaunchRampBase extends BodyComponent with Layered, ZIndex { _LaunchRampBase() : super( - priority: RenderPriority.launchRamp, renderBody: false, children: [ _LaunchRampBackgroundRailingSpriteComponent(), _LaunchRampBaseSpriteComponent(), ], ) { + zIndex = ZIndexes.launchRamp; layer = Layer.launcher; } @@ -140,18 +135,14 @@ class _LaunchRampBackgroundRailingSpriteComponent extends SpriteComponent } } -/// {@template launch_ramp_foreground_railing} -/// Foreground railing for the [_LaunchRampBase] to render in front of the -/// [Ball]. -/// {@endtemplate} -class _LaunchRampForegroundRailing extends BodyComponent { - /// {@macro launch_ramp_foreground_railing} +class _LaunchRampForegroundRailing extends BodyComponent with ZIndex { _LaunchRampForegroundRailing() : super( - priority: RenderPriority.launchRampForegroundRailing, children: [_LaunchRampForegroundRailingSpriteComponent()], renderBody: false, - ); + ) { + zIndex = ZIndexes.launchRampForegroundRailing; + } List _createFixtureDefs() { final fixturesDef = []; @@ -239,8 +230,8 @@ class _LaunchRampExit extends LayerSensor { insideLayer: Layer.launcher, outsideLayer: Layer.board, orientation: LayerEntranceOrientation.down, - insidePriority: RenderPriority.ballOnLaunchRamp, - outsidePriority: RenderPriority.ballOnBoard, + insideZIndex: ZIndexes.ballOnLaunchRamp, + outsideZIndex: ZIndexes.ballOnBoard, ) { layer = Layer.launcher; } diff --git a/packages/pinball_components/lib/src/components/layer.dart b/packages/pinball_components/lib/src/components/layer.dart index 9b20ecf2..a39ad837 100644 --- a/packages/pinball_components/lib/src/components/layer.dart +++ b/packages/pinball_components/lib/src/components/layer.dart @@ -8,9 +8,6 @@ import 'package:flutter/material.dart'; /// [BodyComponent]s with compatible [Layer]s can collide with each other, /// ignoring others. This compatibility depends on bit masking operation /// between layers. For more information read: https://en.wikipedia.org/wiki/Mask_(computing). -/// -/// A parent [Layered] have priority against its children's layer. Them won't be -/// changed but will be ignored. /// {@endtemplate} mixin Layered on BodyComponent { Layer _layer = Layer.all; diff --git a/packages/pinball_components/lib/src/components/layer_sensor.dart b/packages/pinball_components/lib/src/components/layer_sensor.dart index 7a749357..6b5f3832 100644 --- a/packages/pinball_components/lib/src/components/layer_sensor.dart +++ b/packages/pinball_components/lib/src/components/layer_sensor.dart @@ -18,7 +18,7 @@ enum LayerEntranceOrientation { /// [BodyComponent] located at the entrance and exit of a [Layer]. /// /// By default the base [layer] is set to [Layer.board] and the -/// [outsidePriority] is set to the lowest possible [Layer]. +/// [_outsideZIndex] is set to [ZIndexes.ballOnBoard]. /// {@endtemplate} abstract class LayerSensor extends BodyComponent with InitialPosition, Layered, ContactCallbacks { @@ -26,32 +26,21 @@ abstract class LayerSensor extends BodyComponent LayerSensor({ required Layer insideLayer, Layer? outsideLayer, - required int insidePriority, - int? outsidePriority, + required int insideZIndex, + int? outsideZIndex, required this.orientation, }) : _insideLayer = insideLayer, _outsideLayer = outsideLayer ?? Layer.board, - _insidePriority = insidePriority, - _outsidePriority = outsidePriority ?? RenderPriority.ballOnBoard, + _insideZIndex = insideZIndex, + _outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard, super(renderBody: false) { layer = Layer.opening; } + final Layer _insideLayer; final Layer _outsideLayer; - final int _insidePriority; - final int _outsidePriority; - - /// Mask bits value for collisions on [Layer]. - Layer get insideLayer => _insideLayer; - - /// Mask bits value for collisions outside of [Layer]. - Layer get outsideLayer => _outsideLayer; - - /// Render priority for the [Ball] on [Layer]. - int get insidePriority => _insidePriority; - - /// Render priority for the [Ball] outside of [Layer]. - int get outsidePriority => _outsidePriority; + final int _insideZIndex; + final int _outsideZIndex; /// The [Shape] of the [LayerSensor]. Shape get shape; @@ -80,7 +69,7 @@ abstract class LayerSensor extends BodyComponent super.beginContact(other, contact); if (other is! Ball) return; - if (other.layer != insideLayer) { + if (other.layer != _insideLayer) { final isBallEnteringOpening = (orientation == LayerEntranceOrientation.down && other.body.linearVelocity.y < 0) || @@ -89,15 +78,13 @@ abstract class LayerSensor extends BodyComponent if (isBallEnteringOpening) { other - ..layer = insideLayer - ..priority = insidePriority - ..reorderChildren(); + ..layer = _insideLayer + ..zIndex = _insideZIndex; } } else { other - ..layer = outsideLayer - ..priority = outsidePriority - ..reorderChildren(); + ..layer = _outsideLayer + ..zIndex = _outsideZIndex; } } } 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 81de9dd5..79b370a0 100644 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -1,6 +1,7 @@ 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 plunger} /// [Plunger] serves as a spring, that shoots the ball on the right side of the @@ -8,16 +9,12 @@ import 'package:pinball_components/pinball_components.dart'; /// /// [Plunger] ignores gravity so the player controls its downward [pull]. /// {@endtemplate} -class Plunger extends BodyComponent with InitialPosition, Layered { +class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { /// {@macro plunger} Plunger({ required this.compressionDistance, - // TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities - // are fixed. - }) : super( - priority: RenderPriority.plunger, - renderBody: false, - ) { + }) : super(renderBody: false) { + zIndex = ZIndexes.plunger; layer = Layer.launcher; } @@ -71,6 +68,14 @@ class Plunger extends BodyComponent with InitialPosition, Layered { 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); @@ -82,11 +87,26 @@ class Plunger extends BodyComponent with InitialPosition, Layered { /// The velocity's magnitude depends on how far the [Plunger] has been pulled /// from its original [initialPosition]. void release() { - final velocity = (initialPosition.y - body.position.y) * 7; + _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/render_priority.dart b/packages/pinball_components/lib/src/components/render_priority.dart deleted file mode 100644 index d2d08628..00000000 --- a/packages/pinball_components/lib/src/components/render_priority.dart +++ /dev/null @@ -1,122 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:pinball_components/pinball_components.dart'; - -/// {@template render_priority} -/// Priorities for the component rendering order in the pinball game. -/// {@endtemplate} -// TODO(allisonryan0002): find alternative to section comments. -abstract class RenderPriority { - static const _base = 0; - static const _above = 1; - static const _below = -1; - - // Ball - - /// Render priority for the [Ball] while it's on the board. - static const int ballOnBoard = _base; - - /// Render priority for the [Ball] while it's on the [SpaceshipRamp]. - static const int ballOnSpaceshipRamp = - _above + spaceshipRampBackgroundRailing; - - /// Render priority for the [Ball] while it's on the [AndroidSpaceship]. - static const int ballOnSpaceship = _above + spaceshipSaucer; - - /// Render priority for the [Ball] while it's on the [SpaceshipRail]. - static const int ballOnSpaceshipRail = _above + spaceshipRail; - - /// Render priority for the [Ball] while it's on the [LaunchRamp]. - static const int ballOnLaunchRamp = launchRamp; - - // Background - - // TODO(allisonryan0002): fix this magic priority. Could bump all priorities - // so there are no negatives. - static const int boardBackground = 3 * _below + _base; - - // Boundaries - - static const int bottomBoundary = _above + dinoBottomWall; - - static const int outerBoundary = _above + boardBackground; - - static const int outerBottomBoundary = _above + rocket; - - // Bottom Group - - static const int bottomGroup = _above + ballOnBoard; - - // Launcher - - static const int launchRamp = _above + outerBoundary; - - static const int launchRampForegroundRailing = ballOnBoard; - - static const int plunger = _above + launchRamp; - - static const int rocket = _below + bottomBoundary; - - // Dino Land - - static const int dinoTopWall = _above + ballOnBoard; - - static const int dino = _above + dinoTopWall; - - static const int dinoBottomWall = _above + dino; - - static const int slingshot = _above + dinoBottomWall; - - // Flutter Forest - - static const int flutterForest = _above + launchRampForegroundRailing; - - // Sparky Fire Zone - - static const int computerBase = _below + ballOnBoard; - - static const int computerTop = _above + ballOnBoard; - - static const int sparkyAnimatronic = _above + spaceshipRampForegroundRailing; - - static const int sparkyBumper = _above + ballOnBoard; - - static const int turboChargeFlame = _above + ballOnBoard; - - // Android Acres - - static const int spaceshipRail = _above + bottomGroup; - - static const int spaceshipRailExit = _above + ballOnSpaceshipRail; - - static const int spaceshipSaucer = _above + ballOnSpaceshipRail; - - static const int spaceshipLightBeam = _below + spaceshipSaucer; - - static const int androidHead = _above + spaceshipSaucer; - - static const int spaceshipRamp = _above + ballOnBoard; - - static const int spaceshipRampBackgroundRailing = _above + spaceshipRamp; - - static const int spaceshipRampArrow = _above + spaceshipRamp; - - static const int spaceshipRampForegroundRailing = - _above + ballOnSpaceshipRamp; - - static const int spaceshipRampBoardOpening = _below + ballOnBoard; - - static const int androidBumper = _above + ballOnBoard; - - // Score Text - - static const int scoreText = _above + spaceshipRampForegroundRailing; - - // Debug information - - static const int debugInfo = _above + scoreText; - - // Backboard - - static const int backboardMarquee = _below + outerBoundary; -} diff --git a/packages/pinball_components/lib/src/components/rocket.dart b/packages/pinball_components/lib/src/components/rocket.dart index 6ba0b10c..07133fc5 100644 --- a/packages/pinball_components/lib/src/components/rocket.dart +++ b/packages/pinball_components/lib/src/components/rocket.dart @@ -1,17 +1,16 @@ import 'package:flame/components.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template rocket_sprite_component} /// A [SpriteComponent] for the rocket over [Plunger]. /// {@endtemplate} -class RocketSpriteComponent extends SpriteComponent with HasGameRef { +class RocketSpriteComponent extends SpriteComponent with HasGameRef, ZIndex { /// {@macro rocket_sprite_component} - RocketSpriteComponent() - : super( - priority: RenderPriority.rocket, - anchor: Anchor.center, - ); + RocketSpriteComponent() : super(anchor: Anchor.center) { + zIndex = ZIndexes.rocket; + } @override Future onLoad() async { diff --git a/packages/pinball_components/lib/src/components/score_component.dart b/packages/pinball_components/lib/src/components/score_component.dart new file mode 100644 index 00000000..12d198cb --- /dev/null +++ b/packages/pinball_components/lib/src/components/score_component.dart @@ -0,0 +1,92 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +enum Points { + fiveThousand, + twentyThousand, + twoHundredThousand, + oneMillion, +} + +/// {@template score_component} +/// A [ScoreComponent] that spawns at a given [position] with a moving +/// animation. +/// {@endtemplate} +class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex { + /// {@macro score_component} + ScoreComponent({ + required this.points, + required Vector2 position, + }) : super( + position: position, + anchor: Anchor.center, + ) { + zIndex = ZIndexes.score; + } + + late final Effect _effect; + + late Points points; + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite( + gameRef.images.fromCache(points.asset), + ); + this.sprite = sprite; + size = sprite.originalSize / 55; + + await add( + _effect = MoveEffect.by( + Vector2(0, -5), + EffectController(duration: 1), + ), + ); + } + + @override + void update(double dt) { + super.update(dt); + + if (_effect.controller.completed) { + removeFromParent(); + } + } +} + +extension PointsX on Points { + int get value { + switch (this) { + case Points.fiveThousand: + return 5000; + case Points.twentyThousand: + return 20000; + case Points.twoHundredThousand: + return 200000; + case Points.oneMillion: + return 1000000; + } + } +} + +extension on Points { + String get asset { + switch (this) { + case Points.fiveThousand: + return Assets.images.score.fiveThousand.keyName; + case Points.twentyThousand: + return Assets.images.score.twentyThousand.keyName; + case Points.twoHundredThousand: + return Assets.images.score.twoHundredThousand.keyName; + case Points.oneMillion: + return Assets.images.score.oneMillion.keyName; + } + } +} diff --git a/packages/pinball_components/lib/src/components/score_text.dart b/packages/pinball_components/lib/src/components/score_text.dart deleted file mode 100644 index a81b4a6f..00000000 --- a/packages/pinball_components/lib/src/components/score_text.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:async'; - -import 'package:flame/components.dart'; -import 'package:flame/effects.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template score_text} -/// A [TextComponent] that spawns at a given [position] with a moving animation. -/// {@endtemplate} -class ScoreText extends TextComponent { - /// {@macro score_text} - ScoreText({ - required String text, - required Vector2 position, - this.color = Colors.black, - }) : super( - text: text, - position: position, - anchor: Anchor.center, - priority: RenderPriority.scoreText, - ); - - late final Effect _effect; - - /// The [text]'s [Color]. - final Color color; - - @override - Future onLoad() async { - textRenderer = TextPaint( - style: TextStyle( - fontFamily: PinballFonts.pixeloidMono, - color: color, - fontSize: 4, - ), - ); - - await add( - _effect = MoveEffect.by( - Vector2(0, -5), - EffectController(duration: 1), - ), - ); - } - - @override - void update(double dt) { - super.update(dt); - - if (_effect.controller.completed) { - removeFromParent(); - } - } -} 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/lib/src/components/slingshot.dart b/packages/pinball_components/lib/src/components/slingshot.dart index b48bf2f9..e203c082 100644 --- a/packages/pinball_components/lib/src/components/slingshot.dart +++ b/packages/pinball_components/lib/src/components/slingshot.dart @@ -1,17 +1,17 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/bumping_behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template slingshots} -/// A [Blueprint] which creates the pair of [Slingshot]s on the right side of -/// the board. +/// A collection of [Slingshot]s. /// {@endtemplate} -class Slingshots extends Blueprint { +class Slingshots extends Component with ZIndex { /// {@macro slingshots} Slingshots() : super( - components: [ + children: [ Slingshot( length: 5.64, angle: -0.017, @@ -23,11 +23,13 @@ class Slingshots extends Blueprint { spritePath: Assets.images.slingshot.lower.keyName, )..initialPosition = Vector2(24.7, 6.2), ], - ); + ) { + zIndex = ZIndexes.slingshots; + } } /// {@template slingshot} -/// Elastic bumper that bounces the [Ball] off of its straight sides. +/// Elastic bumper that bounces the [Ball] off of its sides. /// {@endtemplate} class Slingshot extends BodyComponent with InitialPosition { /// {@macro slingshot} @@ -38,8 +40,10 @@ class Slingshot extends BodyComponent with InitialPosition { }) : _length = length, _angle = angle, super( - priority: RenderPriority.slingshot, - children: [_SlinghsotSpriteComponent(spritePath, angle: angle)], + children: [ + _SlinghsotSpriteComponent(spritePath, angle: angle), + BumpingBehavior(strength: 20), + ], renderBody: false, ); @@ -52,37 +56,27 @@ class Slingshot extends BodyComponent with InitialPosition { final topCircleShape = CircleShape()..radius = circleRadius; topCircleShape.position.setValues(0, -_length / 2); - final topCircleFixtureDef = FixtureDef(topCircleShape); final bottomCircleShape = CircleShape()..radius = circleRadius; bottomCircleShape.position.setValues(0, _length / 2); - final bottomCircleFixtureDef = FixtureDef(bottomCircleShape); final leftEdgeShape = EdgeShape() ..set( Vector2(circleRadius, _length / 2), Vector2(circleRadius, -_length / 2), ); - final leftEdgeShapeFixtureDef = FixtureDef( - leftEdgeShape, - restitution: 5, - ); final rightEdgeShape = EdgeShape() ..set( Vector2(-circleRadius, _length / 2), Vector2(-circleRadius, -_length / 2), ); - final rightEdgeShapeFixtureDef = FixtureDef( - rightEdgeShape, - restitution: 5, - ); return [ - topCircleFixtureDef, - bottomCircleFixtureDef, - leftEdgeShapeFixtureDef, - rightEdgeShapeFixtureDef, + FixtureDef(topCircleShape), + FixtureDef(bottomCircleShape), + FixtureDef(leftEdgeShape), + FixtureDef(rightEdgeShape), ]; } diff --git a/packages/pinball_components/lib/src/components/spaceship_rail.dart b/packages/pinball_components/lib/src/components/spaceship_rail.dart index df9fc16c..7dbabc93 100644 --- a/packages/pinball_components/lib/src/components/spaceship_rail.dart +++ b/packages/pinball_components/lib/src/components/spaceship_rail.dart @@ -6,13 +6,13 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship_rail} -/// A [Blueprint] for the rail exiting the [AndroidSpaceship]. +/// Rail exiting the [AndroidSpaceship]. /// {@endtemplate} -class SpaceshipRail extends Blueprint { +class SpaceshipRail extends Component { /// {@macro spaceship_rail} SpaceshipRail() : super( - components: [ + children: [ _SpaceshipRail(), _SpaceshipRailExit(), _SpaceshipRailExitSpriteComponent() @@ -20,14 +20,14 @@ class SpaceshipRail extends Blueprint { ); } -class _SpaceshipRail extends BodyComponent with Layered { +class _SpaceshipRail extends BodyComponent with Layered, ZIndex { _SpaceshipRail() : super( - priority: RenderPriority.spaceshipRail, children: [_SpaceshipRailSpriteComponent()], renderBody: false, ) { layer = Layer.spaceshipExitRail; + zIndex = ZIndexes.spaceshipRail; } List _createFixtureDefs() { @@ -125,13 +125,14 @@ class _SpaceshipRailSpriteComponent extends SpriteComponent with HasGameRef { } class _SpaceshipRailExitSpriteComponent extends SpriteComponent - with HasGameRef { + with HasGameRef, ZIndex { _SpaceshipRailExitSpriteComponent() : super( anchor: Anchor.center, position: Vector2(-28, 19.4), - priority: RenderPriority.spaceshipRailExit, - ); + ) { + zIndex = ZIndexes.spaceshipRailExit; + } @override Future onLoad() async { @@ -152,7 +153,7 @@ class _SpaceshipRailExit extends LayerSensor { : super( orientation: LayerEntranceOrientation.down, insideLayer: Layer.spaceshipExitRail, - insidePriority: RenderPriority.ballOnSpaceshipRail, + insideZIndex: ZIndexes.ballOnSpaceshipRail, ) { layer = Layer.spaceshipExitRail; } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp.dart index 6a034daa..c1be0943 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp.dart @@ -8,22 +8,22 @@ import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship_ramp} -/// A [Blueprint] which creates the ramp leading into the [AndroidSpaceship]. +/// Ramp leading into the [AndroidSpaceship]. /// {@endtemplate} -class SpaceshipRamp extends Blueprint { +class SpaceshipRamp extends Component { /// {@macro spaceship_ramp} SpaceshipRamp() : super( - components: [ + children: [ _SpaceshipRampOpening( - outsidePriority: RenderPriority.ballOnBoard, + outsidePriority: ZIndexes.ballOnBoard, rotation: math.pi, ) ..initialPosition = Vector2(1.7, -19.8) ..layer = Layer.opening, _SpaceshipRampOpening( outsideLayer: Layer.spaceship, - outsidePriority: RenderPriority.ballOnSpaceship, + outsidePriority: ZIndexes.ballOnSpaceship, rotation: math.pi, ) ..initialPosition = Vector2(-13.7, -18.6) @@ -41,10 +41,8 @@ class SpaceshipRamp extends Blueprint { /// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. /// /// If the current state is the last one it cycles back to the initial state. - void progress() => components - .whereType<_SpaceshipRampArrowSpriteComponent>() - .first - .progress(); + void progress() => + firstChild<_SpaceshipRampArrowSpriteComponent>()?.progress(); } /// Indicates the state of the arrow on the [SpaceshipRamp]. @@ -94,16 +92,16 @@ extension on SpaceshipRampArrowSpriteState { } class _SpaceshipRampBackground extends BodyComponent - with InitialPosition, Layered { + with InitialPosition, Layered, ZIndex { _SpaceshipRampBackground() : super( - priority: RenderPriority.spaceshipRamp, renderBody: false, children: [ _SpaceshipRampBackgroundRampSpriteComponent(), ], ) { layer = Layer.spaceshipEntranceRamp; + zIndex = ZIndexes.spaceshipRamp; } /// Width between walls of the ramp. @@ -148,13 +146,14 @@ class _SpaceshipRampBackground extends BodyComponent } class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent - with HasGameRef { + with HasGameRef, ZIndex { _SpaceshipRampBackgroundRailingSpriteComponent() : super( anchor: Anchor.center, position: Vector2(-11.7, -54.3), - priority: RenderPriority.spaceshipRampBackgroundRailing, - ); + ) { + zIndex = ZIndexes.spaceshipRampBackgroundRailing; + } @override Future onLoad() async { @@ -197,14 +196,15 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent /// {@endtemplate} class _SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent - with HasGameRef { + with HasGameRef, ZIndex { /// {@macro spaceship_ramp_arrow_sprite_component} _SpaceshipRampArrowSpriteComponent() : super( anchor: Anchor.center, position: Vector2(-3.9, -56.5), - priority: RenderPriority.spaceshipRampArrow, - ); + ) { + zIndex = ZIndexes.spaceshipRampArrow; + } /// Changes arrow image to the next [Sprite]. void progress() => current = current?.next; @@ -226,8 +226,10 @@ class _SpaceshipRampArrowSpriteComponent } class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent - with HasGameRef { - _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center); + with HasGameRef, ZIndex { + _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) { + zIndex = ZIndexes.spaceshipRampBoardOpening; + } @override Future onLoad() async { @@ -243,14 +245,14 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent } class _SpaceshipRampForegroundRailing extends BodyComponent - with InitialPosition, Layered { + with InitialPosition, Layered, ZIndex { _SpaceshipRampForegroundRailing() : super( - priority: RenderPriority.spaceshipRampForegroundRailing, renderBody: false, children: [_SpaceshipRampForegroundRailingSpriteComponent()], ) { layer = Layer.spaceshipEntranceRamp; + zIndex = ZIndexes.spaceshipRampForegroundRailing; } List _createFixtureDefs() { @@ -352,8 +354,8 @@ class _SpaceshipRampOpening extends LayerSensor { insideLayer: Layer.spaceshipEntranceRamp, outsideLayer: outsideLayer, orientation: LayerEntranceOrientation.down, - insidePriority: RenderPriority.ballOnSpaceshipRamp, - outsidePriority: outsidePriority, + insideZIndex: ZIndexes.ballOnSpaceshipRamp, + outsideZIndex: outsidePriority, ); final double _rotation; diff --git a/packages/pinball_components/lib/src/components/sparky_animatronic.dart b/packages/pinball_components/lib/src/components/sparky_animatronic.dart index 714a5700..2ee2803c 100644 --- a/packages/pinball_components/lib/src/components/sparky_animatronic.dart +++ b/packages/pinball_components/lib/src/components/sparky_animatronic.dart @@ -1,17 +1,20 @@ import 'package:flame/components.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_animatronic} /// Animated Sparky that sits on top of the [SparkyComputer]. /// {@endtemplate} -class SparkyAnimatronic extends SpriteAnimationComponent with HasGameRef { +class SparkyAnimatronic extends SpriteAnimationComponent + with HasGameRef, ZIndex { /// {@macro sparky_animatronic} SparkyAnimatronic() : super( anchor: Anchor.center, playing: false, - priority: RenderPriority.sparkyAnimatronic, - ); + ) { + zIndex = ZIndexes.sparkyAnimatronic; + } @override Future onLoad() async { diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart index 81cfa5e1..2c2c50fe 100644 --- a/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart +++ b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart @@ -3,19 +3,19 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_bumper_blinking_behavior} -/// Makes a [SparkyBumper] blink back to [SparkyBumperState.active] when -/// [SparkyBumperState.inactive]. +/// Makes a [SparkyBumper] blink back to [SparkyBumperState.lit] when +/// [SparkyBumperState.dimmed]. /// {@endtemplate} class SparkyBumperBlinkingBehavior extends TimerComponent with ParentIsA { - /// {@macro sparky_bumper_sprite_behavior} + /// {@macro sparky_bumper_blinking_behavior} SparkyBumperBlinkingBehavior() : super(period: 0.05); void _onNewState(SparkyBumperState state) { switch (state) { - case SparkyBumperState.active: + case SparkyBumperState.lit: break; - case SparkyBumperState.inactive: + case SparkyBumperState.dimmed: timer ..reset() ..start(); diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart index bbb9b63b..2f7ba7c4 100644 --- a/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart +++ b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart @@ -5,13 +5,13 @@ import 'package:bloc/bloc.dart'; part 'sparky_bumper_state.dart'; class SparkyBumperCubit extends Cubit { - SparkyBumperCubit() : super(SparkyBumperState.active); + SparkyBumperCubit() : super(SparkyBumperState.lit); void onBallContacted() { - emit(SparkyBumperState.inactive); + emit(SparkyBumperState.dimmed); } void onBlinked() { - emit(SparkyBumperState.active); + emit(SparkyBumperState.lit); } } diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart index 35cc5ffa..096af299 100644 --- a/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart +++ b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart @@ -1,10 +1,8 @@ +// ignore_for_file: public_member_api_docs + part of 'sparky_bumper_cubit.dart'; -/// Indicates the [SparkyBumperCubit]'s current state. enum SparkyBumperState { - /// A lit up bumper. - active, - - /// A dimmed bumper. - inactive, + lit, + dimmed, } diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart b/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart index d6434375..b909f0ba 100644 --- a/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart +++ b/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart @@ -4,6 +4,7 @@ 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/bumping_behavior.dart'; import 'package:pinball_components/src/components/sparky_bumper/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -12,33 +13,34 @@ export 'cubit/sparky_bumper_cubit.dart'; /// {@template sparky_bumper} /// Bumper for Sparky area. /// {@endtemplate} -class SparkyBumper extends BodyComponent with InitialPosition { +class SparkyBumper extends BodyComponent with InitialPosition, ZIndex { /// {@macro sparky_bumper} SparkyBumper._({ required double majorRadius, required double minorRadius, - required String onAssetPath, - required String offAssetPath, + required String litAssetPath, + required String dimmedAssetPath, required Vector2 spritePosition, required this.bloc, Iterable? children, }) : _majorRadius = majorRadius, _minorRadius = minorRadius, super( - priority: RenderPriority.sparkyBumper, renderBody: false, children: [ SparkyBumperBallContactBehavior(), SparkyBumperBlinkingBehavior(), _SparkyBumperSpriteGroupComponent( - onAssetPath: onAssetPath, - offAssetPath: offAssetPath, + litAssetPath: litAssetPath, + dimmedAssetPath: dimmedAssetPath, position: spritePosition, state: bloc.state, ), ...?children, ], - ); + ) { + zIndex = ZIndexes.sparkyBumper; + } /// {@macro sparky_bumper} SparkyBumper.a({ @@ -46,11 +48,14 @@ class SparkyBumper extends BodyComponent with InitialPosition { }) : this._( majorRadius: 2.9, minorRadius: 2.1, - onAssetPath: Assets.images.sparky.bumper.a.active.keyName, - offAssetPath: Assets.images.sparky.bumper.a.inactive.keyName, + litAssetPath: Assets.images.sparky.bumper.a.lit.keyName, + dimmedAssetPath: Assets.images.sparky.bumper.a.dimmed.keyName, spritePosition: Vector2(0, -0.25), bloc: SparkyBumperCubit(), - children: children, + children: [ + ...?children, + BumpingBehavior(strength: 20), + ], ); /// {@macro sparky_bumper} @@ -59,11 +64,14 @@ class SparkyBumper extends BodyComponent with InitialPosition { }) : this._( majorRadius: 2.85, minorRadius: 2, - onAssetPath: Assets.images.sparky.bumper.b.active.keyName, - offAssetPath: Assets.images.sparky.bumper.b.inactive.keyName, + litAssetPath: Assets.images.sparky.bumper.b.lit.keyName, + dimmedAssetPath: Assets.images.sparky.bumper.b.dimmed.keyName, spritePosition: Vector2(0, -0.35), bloc: SparkyBumperCubit(), - children: children, + children: [ + ...?children, + BumpingBehavior(strength: 20), + ], ); /// {@macro sparky_bumper} @@ -72,11 +80,14 @@ class SparkyBumper extends BodyComponent with InitialPosition { }) : this._( majorRadius: 3, minorRadius: 2.2, - onAssetPath: Assets.images.sparky.bumper.c.active.keyName, - offAssetPath: Assets.images.sparky.bumper.c.inactive.keyName, + litAssetPath: Assets.images.sparky.bumper.c.lit.keyName, + dimmedAssetPath: Assets.images.sparky.bumper.c.dimmed.keyName, spritePosition: Vector2(0, -0.4), bloc: SparkyBumperCubit(), - children: children, + children: [ + ...?children, + BumpingBehavior(strength: 20), + ], ); /// Creates an [SparkyBumper] without any children. @@ -111,15 +122,11 @@ class SparkyBumper extends BodyComponent with InitialPosition { majorRadius: _majorRadius, minorRadius: _minorRadius, )..rotate(math.pi / 2.1); - final fixtureDef = FixtureDef( - shape, - restitution: 4, + final bodyDef = BodyDef( + position: initialPosition, ); - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - return world.createBody(bodyDef)..createFixture(fixtureDef); + return world.createBody(bodyDef)..createFixtureFromShape(shape); } } @@ -127,20 +134,20 @@ class _SparkyBumperSpriteGroupComponent extends SpriteGroupComponent with HasGameRef, ParentIsA { _SparkyBumperSpriteGroupComponent({ - required String onAssetPath, - required String offAssetPath, + required String litAssetPath, + required String dimmedAssetPath, required Vector2 position, required SparkyBumperState state, - }) : _onAssetPath = onAssetPath, - _offAssetPath = offAssetPath, + }) : _litAssetPath = litAssetPath, + _dimmedAssetPath = dimmedAssetPath, super( anchor: Anchor.center, position: position, current: state, ); - final String _onAssetPath; - final String _offAssetPath; + final String _litAssetPath; + final String _dimmedAssetPath; @override Future onLoad() async { @@ -151,11 +158,11 @@ class _SparkyBumperSpriteGroupComponent parent.bloc.stream.listen((state) => current = state); final sprites = { - SparkyBumperState.active: Sprite( - gameRef.images.fromCache(_onAssetPath), + SparkyBumperState.lit: Sprite( + gameRef.images.fromCache(_litAssetPath), ), - SparkyBumperState.inactive: Sprite( - gameRef.images.fromCache(_offAssetPath), + SparkyBumperState.dimmed: Sprite( + gameRef.images.fromCache(_dimmedAssetPath), ), }; this.sprites = sprites; diff --git a/packages/pinball_components/lib/src/components/sparky_computer.dart b/packages/pinball_components/lib/src/components/sparky_computer.dart index 7f0cef8c..512c9d48 100644 --- a/packages/pinball_components/lib/src/components/sparky_computer.dart +++ b/packages/pinball_components/lib/src/components/sparky_computer.dart @@ -8,24 +8,26 @@ import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_computer} /// A computer owned by Sparky. /// {@endtemplate} -class SparkyComputer extends Blueprint { +class SparkyComputer extends Component { /// {@macro sparky_computer} SparkyComputer() : super( - components: [ + children: [ _ComputerBase(), _ComputerTopSpriteComponent(), + _ComputerGlowSpriteComponent(), ], ); } -class _ComputerBase extends BodyComponent with InitialPosition { +class _ComputerBase extends BodyComponent with InitialPosition, ZIndex { _ComputerBase() : super( - priority: RenderPriority.computerBase, renderBody: false, children: [_ComputerBaseSpriteComponent()], - ); + ) { + zIndex = ZIndexes.computerBase; + } List _createFixtureDefs() { final leftEdge = EdgeShape() @@ -65,35 +67,65 @@ class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef { _ComputerBaseSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(-11.95, -48.35), + position: Vector2(-12.1, -48.15), ); @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.sparky.computer.base.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.sparky.computer.base.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; } } -class _ComputerTopSpriteComponent extends SpriteComponent with HasGameRef { +class _ComputerTopSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { _ComputerTopSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(-12.45, -49.75), - priority: RenderPriority.computerTop, - ); + position: Vector2(-12.52, -49.37), + ) { + zIndex = ZIndexes.computerTop; + } + + @override + Future onLoad() async { + await super.onLoad(); + + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.sparky.computer.top.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} + +class _ComputerGlowSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _ComputerGlowSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(7.4, 10), + ) { + zIndex = ZIndexes.computerGlow; + } @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.sparky.computer.top.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.sparky.computer.glow.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; diff --git a/packages/pinball_components/lib/src/components/z_indexes.dart b/packages/pinball_components/lib/src/components/z_indexes.dart new file mode 100644 index 00000000..d820fb5b --- /dev/null +++ b/packages/pinball_components/lib/src/components/z_indexes.dart @@ -0,0 +1,117 @@ +// ignore_for_file: public_member_api_docs + +/// Z-Indexes for the component rendering order in the pinball game. +// TODO(allisonryan0002): find alternative to section comments. +abstract class ZIndexes { + static const _base = 0; + static const _above = 1; + static const _below = -1; + + // Ball + + static const ballOnBoard = _base; + + static const ballOnSpaceshipRamp = _above + spaceshipRampBackgroundRailing; + + static const ballOnSpaceship = _above + spaceshipSaucer; + + static const ballOnSpaceshipRail = _above + spaceshipRail; + + static const ballOnLaunchRamp = _above + launchRamp; + + // Background + + // TODO(allisonryan0002): fix this magic zindex. Could bump all priorities so + // there are no negatives. + static const boardBackground = 5 * _below + _base; + + static const decal = _above + boardBackground; + + // Boundaries + + static const bottomBoundary = _above + dinoBottomWall; + + static const outerBoundary = _above + boardBackground; + + static const outerBottomBoundary = _above + rocket; + + // Bottom Group + + static const bottomGroup = _above + ballOnBoard; + + // Launcher + + static const launchRamp = _above + outerBoundary; + + static const launchRampForegroundRailing = _above + ballOnLaunchRamp; + + static const plunger = _above + launchRamp; + + static const rocket = _below + bottomBoundary; + + // Dino Desert + + static const dinoTopWall = _above + ballOnBoard; + + static const dinoTopWallTunnel = _below + ballOnBoard; + + static const dino = _above + dinoTopWall; + + static const dinoBottomWall = _above + dino; + + static const slingshots = _above + dinoBottomWall; + + // Flutter Forest + + static const flutterForest = _above + ballOnBoard; + + // Sparky Scorch + + static const computerBase = _below + ballOnBoard; + + static const computerTop = _above + ballOnBoard; + + static const computerGlow = _above + ballOnBoard; + + static const sparkyAnimatronic = _above + spaceshipRampForegroundRailing; + + static const sparkyBumper = _above + ballOnBoard; + + static const turboChargeFlame = _above + ballOnBoard; + + // Android Acres + + static const spaceshipRail = _above + bottomGroup; + + static const spaceshipRailExit = _above + ballOnSpaceshipRail; + + static const spaceshipSaucer = _above + ballOnSpaceshipRail; + + static const spaceshipLightBeam = _below + spaceshipSaucer; + + static const androidHead = _above + ballOnSpaceship; + + static const spaceshipRamp = _above + sparkyBumper; + + static const spaceshipRampBackgroundRailing = _above + spaceshipRamp; + + static const spaceshipRampArrow = _above + spaceshipRamp; + + static const spaceshipRampForegroundRailing = _above + ballOnSpaceshipRamp; + + static const spaceshipRampBoardOpening = _below + ballOnBoard; + + static const androidBumper = _above + ballOnBoard; + + // Score + + static const score = _above + spaceshipRampForegroundRailing; + + // Debug information + + static const debugInfo = _above + score; + + // Backboard + + static const backboardMarquee = _below + outerBoundary; +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 313c1ee2..596408bb 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -64,7 +64,8 @@ flutter: - assets/images/android/bumper/a/ - assets/images/android/bumper/b/ - assets/images/android/bumper/cow/ - - assets/images/kicker/ + - assets/images/kicker/left/ + - assets/images/kicker/right/ - assets/images/plunger/ - assets/images/slingshot/ - assets/images/sparky/ @@ -72,14 +73,21 @@ flutter: - assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/b/ - assets/images/sparky/bumper/c/ - - assets/images/backbox/ - - assets/images/google_word/ + - assets/images/google_word/letter1/ + - assets/images/google_word/letter2/ + - assets/images/google_word/letter3/ + - assets/images/google_word/letter4/ + - 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/ - assets/images/multiplier/x5/ - assets/images/multiplier/x6/ + - assets/images/score/ + - assets/images/backbox/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index d6b4c8aa..ccb1b0bc 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -1,9 +1,3 @@ -// Copyright (c) 2022, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. import 'package:dashbook/dashbook.dart'; import 'package:flutter/material.dart'; import 'package:sandbox/stories/stories.dart'; @@ -14,18 +8,17 @@ void main() { addBallStories(dashbook); addLayerStories(dashbook); addEffectsStories(dashbook); - addChromeDinoStories(dashbook); addFlutterForestStories(dashbook); + addSparkyScorchStories(dashbook); + addAndroidAcresStories(dashbook); + addDinoDesertStories(dashbook); addBottomGroupStories(dashbook); addPlungerStories(dashbook); - addSlingshotStories(dashbook); - addSparkyBumperStories(dashbook); - addAndroidAcresStories(dashbook); addBoundariesStories(dashbook); addGoogleWordStories(dashbook); addLaunchRampStories(dashbook); - addScoreTextStories(dashbook); - addDinoWallStories(dashbook); + addScoreStories(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 076b2d2b..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 @@ -2,13 +2,12 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class AndroidSpaceshipGame extends BallGame { AndroidSpaceshipGame() : super( - ballPriority: RenderPriority.ballOnSpaceship, + ballPriority: ZIndexes.ballOnSpaceship, ballLayer: Layer.spaceship, imagesFileNames: [ Assets.images.android.spaceship.saucer.keyName, @@ -18,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. @@ -29,8 +28,11 @@ class AndroidSpaceshipGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2.zero()); - await addFromBlueprint( - AndroidSpaceship(position: Vector2.zero()), + await addAll( + [ + AndroidSpaceship(position: Vector2.zero()), + AndroidAnimatronic(), + ], ); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart index 87bac14d..dee83e26 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart @@ -3,14 +3,13 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRailGame extends BallGame { SpaceshipRailGame() : super( color: Colors.blue, - ballPriority: RenderPriority.ballOnSpaceshipRail, + ballPriority: ZIndexes.ballOnSpaceshipRail, ballLayer: Layer.spaceshipExitRail, imagesFileNames: [ Assets.images.android.rail.main.keyName, @@ -30,7 +29,7 @@ class SpaceshipRailGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2(-30, -10)); - await addFromBlueprint(SpaceshipRail()); + await add(SpaceshipRail()); await ready(); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart index 43944a37..cabe4d54 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart @@ -4,14 +4,13 @@ import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRampGame extends BallGame with KeyboardEvents { SpaceshipRampGame() : super( color: Colors.blue, - ballPriority: RenderPriority.ballOnSpaceshipRamp, + ballPriority: ZIndexes.ballOnSpaceshipRamp, ballLayer: Layer.spaceshipEntranceRamp, imagesFileNames: [ Assets.images.android.ramp.railingBackground.keyName, @@ -42,7 +41,7 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { await super.onLoad(); camera.followVector2(Vector2(-12, -50)); - await addFromBlueprint( + await add( _spaceshipRamp = SpaceshipRamp(), ); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart index 9b7d96cc..590638e0 100644 --- a/packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart @@ -3,6 +3,16 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class KickerGame extends BallGame { + KickerGame() + : super( + imagesFileNames: [ + Assets.images.kicker.left.lit.keyName, + Assets.images.kicker.left.dimmed.keyName, + Assets.images.kicker.right.lit.keyName, + Assets.images.kicker.right.dimmed.keyName, + ], + ); + static const description = ''' Shows how Kickers are rendered. @@ -18,9 +28,9 @@ class KickerGame extends BallGame { await addAll( [ Kicker(side: BoardSide.left) - ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y), + ..initialPosition = Vector2(center.x - 8.8, center.y), Kicker(side: BoardSide.right) - ..initialPosition = Vector2(center.x + (Kicker.size.x * 2), center.y), + ..initialPosition = Vector2(center.x + 8.8, center.y), ], ); diff --git a/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart b/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart index cf78750d..12e8ec26 100644 --- a/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart @@ -1,6 +1,5 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class BoundariesGame extends BallGame { @@ -27,7 +26,7 @@ class BoundariesGame extends BallGame { camera ..followVector2(Vector2.zero()) ..zoom = 6; - await addFromBlueprint(Boundaries()); + await add(Boundaries()); await ready(); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart deleted file mode 100644 index a4c70c03..00000000 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/chrome_dino/chrome_dino_game.dart'; - -void addChromeDinoStories(Dashbook dashbook) { - dashbook.storiesOf('Chrome Dino').addGame( - title: 'Traced', - description: ChromeDinoGame.description, - gameBuilder: (_) => ChromeDinoGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_desert/chrome_dino_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart rename to packages/pinball_components/sandbox/lib/stories/dino_desert/chrome_dino_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_desert/dino_walls_game.dart similarity index 80% rename from packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart rename to packages/pinball_components/sandbox/lib/stories/dino_desert/dino_walls_game.dart index a6987fcc..849e17a8 100644 --- a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/dino_desert/dino_walls_game.dart @@ -2,11 +2,10 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class DinoWallGame extends BallGame { - DinoWallGame() : super(); +class DinoWallsGame extends BallGame { + DinoWallsGame() : super(); static const description = ''' Shows how DinoWalls are rendered. @@ -21,10 +20,11 @@ class DinoWallGame extends BallGame { await images.loadAll([ Assets.images.dino.topWall.keyName, + Assets.images.dino.topWallTunnel.keyName, Assets.images.dino.bottomWall.keyName, ]); - await addFromBlueprint(DinoWalls()); + await add(DinoWalls()); camera.followVector2(Vector2.zero()); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_desert/slingshots_game.dart similarity index 82% rename from packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart rename to packages/pinball_components/sandbox/lib/stories/dino_desert/slingshots_game.dart index 28858088..1a9eb2e5 100644 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/dino_desert/slingshots_game.dart @@ -1,10 +1,9 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SlingshotGame extends BallGame { - SlingshotGame() +class SlingshotsGame extends BallGame { + SlingshotsGame() : super( imagesFileNames: [ Assets.images.slingshot.upper.keyName, @@ -24,7 +23,7 @@ class SlingshotGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2.zero()); - await addFromBlueprint(Slingshots()); + await add(Slingshots()); await ready(); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/dino_desert/stories.dart b/packages/pinball_components/sandbox/lib/stories/dino_desert/stories.dart new file mode 100644 index 00000000..bd8fc329 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_desert/stories.dart @@ -0,0 +1,24 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/dino_desert/chrome_dino_game.dart'; +import 'package:sandbox/stories/dino_desert/dino_walls_game.dart'; +import 'package:sandbox/stories/dino_desert/slingshots_game.dart'; + +void addDinoDesertStories(Dashbook dashbook) { + dashbook.storiesOf('Dino Desert') + ..addGame( + title: 'Chrome Dino', + description: ChromeDinoGame.description, + gameBuilder: (_) => ChromeDinoGame(), + ) + ..addGame( + title: 'Dino Walls', + description: DinoWallsGame.description, + gameBuilder: (_) => DinoWallsGame(), + ) + ..addGame( + title: 'Slingshots', + description: SlingshotsGame.description, + gameBuilder: (_) => SlingshotsGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart deleted file mode 100644 index e24d26cc..00000000 --- a/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/dino_wall/dino_wall_game.dart'; - -void addDinoWallStories(Dashbook dashbook) { - dashbook.storiesOf('DinoWall').addGame( - title: 'Traced', - description: DinoWallGame.description, - gameBuilder: (_) => DinoWallGame(), - ); -} 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/google_word/google_letter_game.dart b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart index be90fdb9..bc537de2 100644 --- a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart @@ -6,7 +6,14 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class GoogleLetterGame extends BallGame { - GoogleLetterGame() : super(color: const Color(0xFF009900)); + GoogleLetterGame() + : super( + color: const Color(0xFF009900), + imagesFileNames: [ + Assets.images.googleWord.letter1.lit.keyName, + Assets.images.googleWord.letter1.dimmed.keyName, + ], + ); static const description = ''' Shows how a GoogleLetter is rendered. diff --git a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart index 1be94133..ea3bd4db 100644 --- a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart @@ -3,19 +3,18 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class LaunchRampGame extends BallGame { LaunchRampGame() : super( color: Colors.blue, - ballPriority: RenderPriority.ballOnLaunchRamp, + ballPriority: ZIndexes.ballOnLaunchRamp, ballLayer: Layer.launcher, ); static const description = ''' - Shows how LaunchRamp are rendered. + Shows how the LaunchRamp is rendered. - Activate the "trace" parameter to overlay the body. - Tap anywhere on the screen to spawn a ball into the game. @@ -26,9 +25,9 @@ class LaunchRampGame extends BallGame { await super.onLoad(); camera - ..followVector2(Vector2(0, 0)) + ..followVector2(Vector2.zero()) ..zoom = 7.5; - await addFromBlueprint(LaunchRamp()); + await add(LaunchRamp()); await ready(); await traceAllBodies(); } 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/plunger/plunger_game.dart b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart index 29eded8c..0f1ec2e4 100644 --- a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -29,7 +29,7 @@ class PlungerGame extends BallGame with KeyboardEvents, Traceable { final center = screenToWorld(camera.viewport.canvasSize! / 2); await add( plunger = Plunger(compressionDistance: 29) - ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y), + ..initialPosition = Vector2(center.x - 8.8, center.y), ); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/score/score_game.dart b/packages/pinball_components/sandbox/lib/stories/score/score_game.dart new file mode 100644 index 00000000..4bde5018 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/score/score_game.dart @@ -0,0 +1,44 @@ +import 'dart:math'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; + +class ScoreGame extends AssetsGame with TapDetector { + ScoreGame() + : super( + imagesFileNames: [ + Assets.images.score.fiveThousand.keyName, + Assets.images.score.twentyThousand.keyName, + Assets.images.score.twoHundredThousand.keyName, + Assets.images.score.oneMillion.keyName, + ], + ); + + static const description = ''' + Simple game to show how score component works, + + - Tap anywhere on the screen to spawn an image on the given location. +'''; + + final random = Random(); + + @override + Future onLoad() async { + await super.onLoad(); + camera.followVector2(Vector2.zero()); + } + + @override + void onTapUp(TapUpInfo info) { + final index = random.nextInt(Points.values.length); + final score = Points.values[index]; + + add( + ScoreComponent( + points: score, + position: info.eventPosition.game..multiply(Vector2(1, -1)), + ), + ); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/score/stories.dart b/packages/pinball_components/sandbox/lib/stories/score/stories.dart new file mode 100644 index 00000000..9c1d3c62 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/score/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/score/score_game.dart'; + +void addScoreStories(Dashbook dashbook) { + dashbook.storiesOf('Score').addGame( + title: 'Basic', + description: ScoreGame.description, + gameBuilder: (_) => ScoreGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/score_text/score_text_game.dart b/packages/pinball_components/sandbox/lib/stories/score_text/score_text_game.dart deleted file mode 100644 index aa776405..00000000 --- a/packages/pinball_components/sandbox/lib/stories/score_text/score_text_game.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:math'; - -import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:sandbox/common/common.dart'; - -class ScoreTextGame extends AssetsGame with TapDetector { - static const description = ''' - Simple game to show how score text works, - - - Tap anywhere on the screen to spawn an text on the given location. -'''; - - final random = Random(); - - @override - Future onLoad() async { - camera.followVector2(Vector2.zero()); - } - - @override - void onTapUp(TapUpInfo info) { - add( - ScoreText( - text: random.nextInt(100000).toString(), - color: Colors.white, - position: info.eventPosition.game..multiply(Vector2(1, -1)), - ), - ); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart b/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart deleted file mode 100644 index c4899a27..00000000 --- a/packages/pinball_components/sandbox/lib/stories/score_text/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/score_text/score_text_game.dart'; - -void addScoreTextStories(Dashbook dashbook) { - dashbook.storiesOf('ScoreText').addGame( - title: 'Basic', - description: ScoreTextGame.description, - gameBuilder: (_) => ScoreTextGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart deleted file mode 100644 index e4c04a0f..00000000 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/slingshot/slingshot_game.dart'; - -void addSlingshotStories(Dashbook dashbook) { - dashbook.storiesOf('Slingshots').addGame( - title: 'Traced', - description: SlingshotGame.description, - gameBuilder: (_) => SlingshotGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart deleted file mode 100644 index 418636db..00000000 --- a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/sparky_bumper/sparky_bumper_game.dart'; - -void addSparkyBumperStories(Dashbook dashbook) { - dashbook.storiesOf('Sparky Bumpers').addGame( - title: 'Traced', - description: SparkyBumperGame.description, - gameBuilder: (_) => SparkyBumperGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_bumper_game.dart similarity index 74% rename from packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart rename to packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_bumper_game.dart index 9bebde9e..c45c4895 100644 --- a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_bumper_game.dart @@ -9,6 +9,7 @@ class SparkyBumperGame extends BallGame { Shows how a SparkyBumper is rendered. - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. '''; @override @@ -16,12 +17,12 @@ class SparkyBumperGame extends BallGame { await super.onLoad(); await images.loadAll([ - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, ]); final center = screenToWorld(camera.viewport.canvasSize! / 2); diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_computer_game.dart b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_computer_game.dart new file mode 100644 index 00000000..b4002479 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_computer_game.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SparkyComputerGame extends BallGame { + static const description = ''' + Shows how the SparkyComputer is rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + await images.loadAll([ + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.glow.keyName, + ]); + + camera.followVector2(Vector2(-10, -40)); + await add(SparkyComputer()); + await ready(); + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_scorch/stories.dart b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/stories.dart new file mode 100644 index 00000000..9fb47f57 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/stories.dart @@ -0,0 +1,18 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/sparky_scorch/sparky_bumper_game.dart'; +import 'package:sandbox/stories/sparky_scorch/sparky_computer_game.dart'; + +void addSparkyScorchStories(Dashbook dashbook) { + dashbook.storiesOf('Sparky Scorch') + ..addGame( + title: 'Sparky Computer', + description: SparkyComputerGame.description, + gameBuilder: (_) => SparkyComputerGame(), + ) + ..addGame( + title: 'Sparky Bumper', + description: SparkyBumperGame.description, + gameBuilder: (_) => SparkyBumperGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index e86a6f2a..b48770ba 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -2,15 +2,14 @@ export 'android_acres/stories.dart'; export 'ball/stories.dart'; export 'bottom_group/stories.dart'; export 'boundaries/stories.dart'; -export 'chrome_dino/stories.dart'; -export 'dino_wall/stories.dart'; +export 'dino_desert/stories.dart'; export 'effects/stories.dart'; 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_text/stories.dart'; -export 'slingshot/stories.dart'; -export 'sparky_bumper/stories.dart'; +export 'score/stories.dart'; +export 'sparky_scorch/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 33c5670d..00000000 --- a/packages/pinball_components/test/helpers/mocks.dart +++ /dev/null @@ -1,28 +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 {} 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 a5256b79..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 @@ -7,9 +7,12 @@ 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_bumper/behaviors/behaviors.dart'; +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 = [ @@ -45,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(), @@ -62,6 +65,30 @@ void main() { }); group('adds', () { + flameTester.test('an AndroidBumperBallContactBehavior', (game) async { + final androidBumper = AndroidBumper.a(); + await game.ensureAdd(androidBumper); + expect( + androidBumper.children + .whereType() + .single, + isNotNull, + ); + }); + + flameTester.test('an AndroidBumperBlinkingBehavior', (game) async { + final androidBumper = AndroidBumper.a(); + await game.ensureAdd(androidBumper); + expect( + androidBumper.children + .whereType() + .single, + isNotNull, + ); + }); + }); + + group("'a' adds", () { flameTester.test('new children', (game) async { final component = Component(); final androidBumper = AndroidBumper.a( @@ -71,13 +98,51 @@ void main() { expect(androidBumper.children, contains(component)); }); - flameTester.test('an AndroidBumperBallContactBehavior', (game) async { + flameTester.test('a BumpingBehavior', (game) async { final androidBumper = AndroidBumper.a(); await game.ensureAdd(androidBumper); expect( - androidBumper.children - .whereType() - .single, + androidBumper.children.whereType().single, + isNotNull, + ); + }); + }); + + group("'b' adds", () { + flameTester.test('new children', (game) async { + final component = Component(); + final androidBumper = AndroidBumper.b( + children: [component], + ); + await game.ensureAdd(androidBumper); + expect(androidBumper.children, contains(component)); + }); + + flameTester.test('a BumpingBehavior', (game) async { + final androidBumper = AndroidBumper.b(); + await game.ensureAdd(androidBumper); + expect( + androidBumper.children.whereType().single, + isNotNull, + ); + }); + }); + + group("'cow' adds", () { + flameTester.test('new children', (game) async { + final component = Component(); + final androidBumper = AndroidBumper.cow( + children: [component], + ); + await game.ensureAdd(androidBumper); + expect(androidBumper.children, contains(component)); + }); + + flameTester.test('a BumpingBehavior', (game) async { + final androidBumper = AndroidBumper.cow(); + await game.ensureAdd(androidBumper); + expect( + androidBumper.children.whereType().single, isNotNull, ); }); 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 92219a64..00000000 --- a/packages/pinball_components/test/src/components/android_spaceship_test.dart +++ /dev/null @@ -1,66 +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', () { - group('Spaceship', () { - 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 { - await game.addFromBlueprint(AndroidSpaceship(position: Vector2.zero())); - await game.ready(); - }); - - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - await game.images.loadAll(assets); - await game - .addFromBlueprint(AndroidSpaceship(position: Vector2.zero())); - 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/ball_test.dart b/packages/pinball_components/test/src/components/ball/ball_test.dart similarity index 78% rename from packages/pinball_components/test/src/components/ball_test.dart rename to packages/pinball_components/test/src/components/ball/ball_test.dart index 26a03886..321e137b 100644 --- a/packages/pinball_components/test/src/components/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball/ball_test.dart @@ -6,18 +6,29 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/ball/behaviors/behaviors.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); group('Ball', () { + const baseColor = Color(0xFFFFFFFF); + + test( + 'can be instantiated', + () { + expect(Ball(baseColor: baseColor), isA()); + expect(Ball.test(baseColor: baseColor), isA()); + }, + ); + flameTester.test( 'loads correctly', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ready(); await game.ensureAdd(ball); @@ -25,11 +36,20 @@ void main() { }, ); + flameTester.test('add a BallScalingBehavior', (game) async { + final ball = Ball(baseColor: baseColor); + await game.ensureAdd(ball); + expect( + ball.descendants().whereType().length, + equals(1), + ); + }); + group('body', () { flameTester.test( 'is dynamic', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); expect(ball.body.bodyType, equals(BodyType.dynamic)); @@ -38,7 +58,7 @@ void main() { group('can be moved', () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); game.update(1); @@ -46,7 +66,7 @@ void main() { }); flameTester.test('by applying velocity', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.body.gravityScale = Vector2.zero(); @@ -61,7 +81,7 @@ void main() { flameTester.test( 'exists', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); expect(ball.body.fixtures[0], isA()); @@ -71,7 +91,7 @@ void main() { flameTester.test( 'is dense', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -82,7 +102,7 @@ void main() { flameTester.test( 'shape is circular', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); final fixture = ball.body.fixtures[0]; @@ -94,7 +114,7 @@ void main() { flameTester.test( 'has Layer.all as default filter maskBits', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ready(); await game.ensureAdd(ball); await game.ready(); @@ -108,7 +128,7 @@ void main() { group('stop', () { group("can't be moved", () { flameTester.test('by its weight', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.stop(); @@ -116,19 +136,6 @@ void main() { expect(ball.body.position, equals(ball.initialPosition)); }); }); - - // TODO(allisonryan0002): delete or retest this if/when solution is added - // to prevent forces on a ball while stopped. - - // flameTester.test('by applying velocity', (game) async { - // final ball = Ball(baseColor: Colors.blue); - // await game.ensureAdd(ball); - // ball.stop(); - - // ball.body.linearVelocity.setValues(10, 10); - // game.update(1); - // expect(ball.body.position, equals(ball.initialPosition)); - // }); }); group('resume', () { @@ -136,7 +143,7 @@ void main() { flameTester.test( 'by its weight when previously stopped', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.stop(); ball.resume(); @@ -149,7 +156,7 @@ void main() { flameTester.test( 'by applying velocity when previously stopped', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); ball.stop(); ball.resume(); @@ -165,7 +172,7 @@ void main() { group('boost', () { flameTester.test('applies an impulse to the ball', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); expect(ball.body.linearVelocity, equals(Vector2.zero())); @@ -176,7 +183,7 @@ void main() { }); flameTester.test('adds TurboChargeSpriteAnimation', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); await ball.boost(Vector2.all(10)); @@ -190,7 +197,7 @@ void main() { flameTester.test('removes TurboChargeSpriteAnimation after it finishes', (game) async { - final ball = Ball(baseColor: Colors.blue); + final ball = Ball(baseColor: baseColor); await game.ensureAdd(ball); await ball.boost(Vector2.all(10)); diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart new file mode 100644 index 00000000..42cd9073 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -0,0 +1,99 @@ +// ignore_for_file: cascade_invocations + +import 'dart:ui'; + +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_components/src/components/ball/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final asset = Assets.images.ball.ball.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + group('BallScalingBehavior', () { + const baseColor = Color(0xFFFFFFFF); + test('can be instantiated', () { + expect( + BallScalingBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final ball = Ball.test(baseColor: baseColor); + final behavior = BallScalingBehavior(); + await ball.add(behavior); + await game.ensureAdd(ball); + expect( + ball.firstChild(), + equals(behavior), + ); + }); + + flameTester.test('can be loaded', (game) async { + final ball = Ball.test(baseColor: baseColor); + final behavior = BallScalingBehavior(); + await ball.add(behavior); + await game.ensureAdd(ball); + expect( + ball.firstChild(), + equals(behavior), + ); + }); + + flameTester.test('scales the shape radius', (game) async { + final ball1 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, 10); + await ball1.add(BallScalingBehavior()); + + final ball2 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, -10); + await ball2.add(BallScalingBehavior()); + + await game.ensureAddAll([ball1, ball2]); + game.update(1); + + final shape1 = ball1.body.fixtures.first.shape; + final shape2 = ball2.body.fixtures.first.shape; + expect( + shape1.radius, + greaterThan(shape2.radius), + ); + }); + + flameTester.testGameWidget( + 'scales the sprite', + setUp: (game, tester) async { + final ball1 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, 10); + await ball1.add(BallScalingBehavior()); + + final ball2 = Ball.test(baseColor: baseColor) + ..initialPosition = Vector2(0, -10); + await ball2.add(BallScalingBehavior()); + + await game.ensureAddAll([ball1, ball2]); + game.update(1); + + await tester.pump(); + await game.ready(); + + final sprite1 = ball1.firstChild()!; + final sprite2 = ball2.firstChild()!; + expect( + sprite1.scale.x, + greaterThan(sprite2.scale.x), + ); + expect( + sprite1.scale.y, + greaterThan(sprite2.scale.y), + ); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/boundaries_test.dart b/packages/pinball_components/test/src/components/boundaries_test.dart index 4e2fb497..c119719e 100644 --- a/packages/pinball_components/test/src/components/boundaries_test.dart +++ b/packages/pinball_components/test/src/components/boundaries_test.dart @@ -17,17 +17,24 @@ void main() { Assets.images.boundary.outerBottom.keyName, Assets.images.boundary.bottom.keyName, ]; - final flameTester = FlameTester(TestGame.new); + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('loads correctly', (game) async { + final component = Boundaries(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); flameTester.testGameWidget( 'render correctly', setUp: (game, tester) async { await game.images.loadAll(assets); - await game.addFromBlueprint(Boundaries()); - await game.ready(); + final canvas = ZCanvasComponent(children: [Boundaries()]); + await game.ensureAdd(canvas); game.camera.followVector2(Vector2.zero()); game.camera.zoom = 3.2; + await tester.pump(); }, verify: (game, tester) async { await expectLater( 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 new file mode 100644 index 00000000..8d052fab --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_chomping_behavior_test.dart @@ -0,0 +1,68 @@ +// ignore_for_file: cascade_invocations + +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/material.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/chrome_dino/behaviors/behaviors.dart'; + +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 flameTester = FlameTester(TestGame.new); + + group( + 'ChromeDinoChompingBehavior', + () { + test('can be instantiated', () { + expect( + ChromeDinoChompingBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact sets ball sprite to be invisible and calls onChomp', + (game) async { + final ball = Ball(baseColor: Colors.red); + final behavior = ChromeDinoChompingBehavior(); + final bloc = _MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: true, + ), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAddAll([chromeDino, ball]); + + final contact = _MockContact(); + final fixture = _MockFixture(); + when(() => contact.fixtureA).thenReturn(fixture); + when(() => fixture.userData).thenReturn('inside_mouth'); + + behavior.beginContact(ball, contact); + + expect(ball.firstChild()!.getOpacity(), isZero); + + verify(() => bloc.onChomp(ball)).called(1); + }, + ); + }, + ); +} 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 new file mode 100644 index 00000000..69f57114 --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_mouth_opening_behavior_test.dart @@ -0,0 +1,66 @@ +// 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/chrome_dino/behaviors/behaviors.dart'; + +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); + + group( + 'ChromeDinoMouthOpeningBehavior', + () { + test('can be instantiated', () { + expect( + ChromeDinoMouthOpeningBehavior(), + isA(), + ); + }); + + flameTester.test( + 'preSolve disables contact when the mouth is open ' + 'and there is not ball in the mouth', + (game) async { + final behavior = ChromeDinoMouthOpeningBehavior(); + final bloc = _MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: true, + ), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final contact = _MockContact(); + final fixture = _MockFixture(); + when(() => contact.fixtureA).thenReturn(fixture); + when(() => fixture.userData).thenReturn('mouth_opening'); + + 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 new file mode 100644 index 00000000..1d0a55b4 --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_spitting_behavior_test.dart @@ -0,0 +1,110 @@ +// ignore_for_file: cascade_invocations + +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/material.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/chrome_dino/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'ChromeDinoSpittingBehavior', + () { + test('can be instantiated', () { + expect( + ChromeDinoSpittingBehavior(), + isA(), + ); + }); + + group('on the next time the mouth opens and status is chomping', () { + flameTester.test( + 'sets ball sprite to visible and sets a linear velocity', + (game) async { + final ball = Ball(baseColor: Colors.red); + final behavior = ChromeDinoSpittingBehavior(); + final bloc = _MockChromeDinoCubit(); + final streamController = StreamController(); + final chompingState = ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ball: ball, + ); + whenListen( + bloc, + streamController.stream, + initialState: chompingState, + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAddAll([chromeDino, ball]); + + streamController.add(chompingState.copyWith(isMouthOpen: false)); + streamController.add(chompingState.copyWith(isMouthOpen: true)); + await game.ready(); + + game + .descendants() + .whereType() + .single + .timer + .onTick!(); + + expect(ball.firstChild()!.getOpacity(), equals(1)); + expect(ball.body.linearVelocity, equals(Vector2(-50, 0))); + }, + ); + + flameTester.test( + 'calls onSpit', + (game) async { + final ball = Ball(baseColor: Colors.red); + final behavior = ChromeDinoSpittingBehavior(); + final bloc = _MockChromeDinoCubit(); + final streamController = StreamController(); + final chompingState = ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ball: ball, + ); + whenListen( + bloc, + streamController.stream, + initialState: chompingState, + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAddAll([chromeDino, ball]); + + streamController.add(chompingState.copyWith(isMouthOpen: false)); + streamController.add(chompingState.copyWith(isMouthOpen: true)); + await game.ready(); + + game + .descendants() + .whereType() + .single + .timer + .onTick!(); + + verify(bloc.onSpit).called(1); + }, + ); + }); + }, + ); +} 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 new file mode 100644 index 00000000..9b6a05b6 --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/behaviors/chrome_dino_swiveling_behavior_test.dart @@ -0,0 +1,171 @@ +// 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/chrome_dino/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'ChromeDinoSwivelingBehavior', + () { + const swivelPeriod = 98 / 48; + + test('can be instantiated', () { + expect( + ChromeDinoSwivelingBehavior(), + isA(), + ); + }); + + flameTester.test( + 'creates a RevoluteJoint', + (game) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = _MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState.inital(), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + expect( + game.world.joints.whereType().single, + isNotNull, + ); + }, + ); + + flameTester.test( + 'reverses swivel direction on each timer tick', + (game) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = _MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState.inital(), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final timer = behavior.timer; + final joint = game.world.joints.whereType().single; + + expect(joint.motorSpeed, isPositive); + + timer.onTick!(); + game.update(0); + expect(joint.motorSpeed, isNegative); + + timer.onTick!(); + game.update(0); + expect(joint.motorSpeed, isPositive); + }, + ); + + group('calls', () { + flameTester.testGameWidget( + 'onCloseMouth when joint angle is between limits ' + 'and mouth is open', + setUp: (game, tester) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = _MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: + const ChromeDinoState.inital().copyWith(isMouthOpen: true), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final joint = game.world.joints.whereType().single; + final angle = joint.jointAngle(); + expect( + angle < joint.upperLimit && angle > joint.lowerLimit, + isTrue, + ); + game.update(0); + + verify(bloc.onCloseMouth).called(1); + }, + ); + + flameTester.testGameWidget( + 'onOpenMouth when joint angle is greater than the upperLimit ' + 'and mouth is closed', + setUp: (game, tester) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = _MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: + const ChromeDinoState.inital().copyWith(isMouthOpen: false), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final joint = game.world.joints.whereType().single; + + game.update(swivelPeriod / 2); + await tester.pump(); + final angle = joint.jointAngle(); + expect(angle >= joint.upperLimit, isTrue); + + verify(bloc.onOpenMouth).called(1); + }, + ); + + flameTester.testGameWidget( + 'onOpenMouth when joint angle is less than the lowerLimit ' + 'and mouth is closed', + setUp: (game, tester) async { + final behavior = ChromeDinoSwivelingBehavior(); + final bloc = _MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: + const ChromeDinoState.inital().copyWith(isMouthOpen: false), + ); + + final chromeDino = ChromeDino.test(bloc: bloc); + await chromeDino.add(behavior); + await game.ensureAdd(chromeDino); + + final joint = game.world.joints.whereType().single; + + game.update(swivelPeriod * 1.5); + await tester.pump(); + final angle = joint.jointAngle(); + expect(angle <= joint.lowerLimit, isTrue); + + verify(bloc.onOpenMouth).called(1); + }, + ); + }); + }, + ); +} 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 new file mode 100644 index 00000000..4c1802ef --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/chrome_dino_test.dart @@ -0,0 +1,143 @@ +// 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/chrome_dino/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.animatronic.head.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('ChromeDino', () { + flameTester.test( + 'loads correctly', + (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + + expect(game.contains(chromeDino), isTrue); + }, + ); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.ensureAdd(ChromeDino()); + game.camera.followVector2(Vector2.zero()); + await tester.pump(); + }, + verify: (game, tester) async { + final swivelAnimationDuration = game + .descendants() + .whereType() + .first + .animation! + .totalDuration() / + 2; + game.update(swivelAnimationDuration); + await tester.pump(); + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/down.png'), + ); + + game.update(swivelAnimationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/middle.png'), + ); + + game.update(swivelAnimationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/up.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 = _MockChromeDinoCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: const ChromeDinoState.inital(), + ); + when(bloc.close).thenAnswer((_) async {}); + final chromeDino = ChromeDino.test(bloc: bloc); + + await game.ensureAdd(chromeDino); + game.remove(chromeDino); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('a ChromeDinoMouthOpeningBehavior', (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + expect( + chromeDino.children + .whereType() + .single, + isNotNull, + ); + }); + + flameTester.test('a ChromeDinoSwivelingBehavior', (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + expect( + chromeDino.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('a ChromeDinoChompingBehavior', (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + expect( + chromeDino.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('a ChromeDinoSpittingBehavior', (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + expect( + chromeDino.children.whereType().single, + isNotNull, + ); + }); + + flameTester.test('new children', (game) async { + final component = Component(); + final chromeDino = ChromeDino( + children: [component], + ); + await game.ensureAdd(chromeDino); + expect(chromeDino.children, contains(component)); + }); + }); + }); +} 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 new file mode 100644 index 00000000..5b31be74 --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_cubit_test.dart @@ -0,0 +1,84 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'ChromeDinoCubit', + () { + final ball = Ball(baseColor: Colors.red); + + blocTest( + 'onOpenMouth emits true', + build: ChromeDinoCubit.new, + act: (bloc) => bloc.onOpenMouth(), + expect: () => [ + isA().having( + (state) => state.isMouthOpen, + 'isMouthOpen', + true, + ) + ], + ); + + blocTest( + 'onCloseMouth emits false', + build: ChromeDinoCubit.new, + act: (bloc) => bloc.onCloseMouth(), + expect: () => [ + isA().having( + (state) => state.isMouthOpen, + 'isMouthOpen', + false, + ) + ], + ); + + blocTest( + 'onChomp emits ChromeDinoStatus.chomping and chomped ball ' + 'when the ball is not in the mouth', + build: ChromeDinoCubit.new, + act: (bloc) => bloc.onChomp(ball), + expect: () => [ + isA() + ..having( + (state) => state.status, + 'status', + ChromeDinoStatus.chomping, + ) + ..having( + (state) => state.ball, + 'ball', + ball, + ) + ], + ); + + blocTest( + '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: () => [ + isA().having( + (state) => state.status, + 'status', + ChromeDinoStatus.idle, + )..having( + (state) => state.ball, + 'ball', + null, + ) + ], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart new file mode 100644 index 00000000..d067674b --- /dev/null +++ b/packages/pinball_components/test/src/components/chrome_dino/cubit/chrome_dino_state_test.dart @@ -0,0 +1,88 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('ChromeDinoState', () { + test('supports value equality', () { + expect( + ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ), + equals( + const ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + const ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ), + isNotNull, + ); + }); + + test('initial is idle with mouth closed', () { + const initialState = ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: false, + ); + expect(ChromeDinoState.inital(), equals(initialState)); + }); + }); + + group('copyWith', () { + test( + 'copies correctly ' + 'when no argument specified', + () { + const chromeDinoState = ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ); + expect( + chromeDinoState.copyWith(), + equals(chromeDinoState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + final ball = Ball(baseColor: Colors.red); + const chromeDinoState = ChromeDinoState( + status: ChromeDinoStatus.chomping, + isMouthOpen: true, + ); + final otherChromeDinoState = ChromeDinoState( + status: ChromeDinoStatus.idle, + isMouthOpen: false, + ball: ball, + ); + expect(chromeDinoState, isNot(equals(otherChromeDinoState))); + + expect( + chromeDinoState.copyWith( + status: ChromeDinoStatus.idle, + isMouthOpen: false, + ball: ball, + ), + equals(otherChromeDinoState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/down.png b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/down.png new file mode 100644 index 00000000..eaeb458e Binary files /dev/null and b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/down.png differ diff --git a/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/middle.png b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/middle.png new file mode 100644 index 00000000..d8665644 Binary files /dev/null and b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/middle.png differ diff --git a/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/up.png b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/up.png new file mode 100644 index 00000000..a584b785 Binary files /dev/null and b/packages/pinball_components/test/src/components/chrome_dino/golden/chrome_dino/up.png differ diff --git a/packages/pinball_components/test/src/components/chrome_dino_test.dart b/packages/pinball_components/test/src/components/chrome_dino_test.dart deleted file mode 100644 index f97270b9..00000000 --- a/packages/pinball_components/test/src/components/chrome_dino_test.dart +++ /dev/null @@ -1,109 +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 '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.dino.animatronic.mouth.keyName, - Assets.images.dino.animatronic.head.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); - - group('ChromeDino', () { - flameTester.test( - 'loads correctly', - (game) async { - final chromeDino = ChromeDino(); - await game.ensureAdd(chromeDino); - - expect(game.contains(chromeDino), isTrue); - }, - ); - - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - await game.images.loadAll(assets); - await game.ensureAdd(ChromeDino()); - game.camera.followVector2(Vector2.zero()); - await tester.pump(); - }, - verify: (game, tester) async { - final sweepAnimationDuration = game - .descendants() - .whereType() - .first - .animation! - .totalDuration() / - 2; - - await expectLater( - find.byGame(), - matchesGoldenFile('golden/chrome_dino/up.png'), - ); - - game.update(sweepAnimationDuration * 0.25); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/chrome_dino/middle.png'), - ); - - game.update(sweepAnimationDuration * 0.25); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/chrome_dino/down.png'), - ); - }, - ); - - group('swivels', () { - flameTester.test( - 'up', - (game) async { - final chromeDino = ChromeDino(); - await game.ensureAdd(chromeDino); - game.camera.followVector2(Vector2.zero()); - - final sweepAnimationDuration = game - .descendants() - .whereType() - .first - .animation! - .totalDuration() / - 2; - game.update(sweepAnimationDuration * 1.5); - - expect(chromeDino.body.angularVelocity, isPositive); - }, - ); - - flameTester.test( - 'down', - (game) async { - final chromeDino = ChromeDino(); - await game.ensureAdd(chromeDino); - game.camera.followVector2(Vector2.zero()); - - final sweepAnimationDuration = game - .descendants() - .whereType() - .first - .animation! - .totalDuration() / - 2; - game.update(sweepAnimationDuration * 0.5); - - expect(chromeDino.body.angularVelocity, isNegative); - }, - ); - }); - }); -} 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 67764951..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 @@ -6,10 +6,13 @@ 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/bumping_behavior.dart'; import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart'; import '../../../helpers/helpers.dart'; +class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -47,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(), @@ -63,8 +66,39 @@ void main() { verify(bloc.close).called(1); }); - group('adds', () { - flameTester.test('adds new children', (game) async { + flameTester.test('adds a DashNestBumperBallContactBehavior', (game) async { + final dashNestBumper = DashNestBumper.a(); + await game.ensureAdd(dashNestBumper); + expect( + dashNestBumper.children + .whereType() + .single, + isNotNull, + ); + }); + + group("'main' adds", () { + flameTester.test('new children', (game) async { + final component = Component(); + final dashNestBumper = DashNestBumper.main( + children: [component], + ); + await game.ensureAdd(dashNestBumper); + expect(dashNestBumper.children, contains(component)); + }); + + flameTester.test('a BumpingBehavior', (game) async { + final dashNestBumper = DashNestBumper.main(); + await game.ensureAdd(dashNestBumper); + expect( + dashNestBumper.children.whereType().single, + isNotNull, + ); + }); + }); + + group("'a' adds", () { + flameTester.test('new children', (game) async { final component = Component(); final dashNestBumper = DashNestBumper.a( children: [component], @@ -73,13 +107,31 @@ void main() { expect(dashNestBumper.children, contains(component)); }); - flameTester.test('a DashNestBumperBallContactBehavior', (game) async { + flameTester.test('a BumpingBehavior', (game) async { final dashNestBumper = DashNestBumper.a(); await game.ensureAdd(dashNestBumper); expect( - dashNestBumper.children - .whereType() - .single, + dashNestBumper.children.whereType().single, + isNotNull, + ); + }); + }); + + group("'b' adds", () { + flameTester.test('new children', (game) async { + final component = Component(); + final dashNestBumper = DashNestBumper.b( + children: [component], + ); + await game.ensureAdd(dashNestBumper); + expect(dashNestBumper.children, contains(component)); + }); + + flameTester.test('a BumpingBehavior', (game) async { + final dashNestBumper = DashNestBumper.b(); + await game.ensureAdd(dashNestBumper); + expect( + dashNestBumper.children.whereType().single, isNotNull, ); }); diff --git a/packages/pinball_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart index ff64fb00..5e4471e5 100644 --- a/packages/pinball_components/test/src/components/dino_walls_test.dart +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -4,7 +4,6 @@ import 'package:flame_forge2d/flame_forge2d.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'; @@ -13,16 +12,22 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ Assets.images.dino.topWall.keyName, + Assets.images.dino.topWallTunnel.keyName, Assets.images.dino.bottomWall.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); + flameTester.test('loads correctly', (game) async { + final component = DinoWalls(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { await game.images.loadAll(assets); - await game.addFromBlueprint(DinoWalls()); - await game.ready(); + await game.ensureAdd(DinoWalls()); game.camera.followVector2(Vector2.zero()); game.camera.zoom = 6.5; @@ -36,18 +41,5 @@ void main() { ); }, ); - - flameTester.test( - 'loads correctly', - (game) async { - final dinoWalls = DinoWalls(); - await game.addFromBlueprint(dinoWalls); - await game.ready(); - - for (final wall in dinoWalls.components) { - expect(game.contains(wall), isTrue); - } - }, - ); }); } 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/dino-walls.png b/packages/pinball_components/test/src/components/golden/dino-walls.png index b47c453f..c7d55c04 100644 Binary files a/packages/pinball_components/test/src/components/golden/dino-walls.png and b/packages/pinball_components/test/src/components/golden/dino-walls.png differ diff --git a/packages/pinball_components/test/src/components/golden/kickers.png b/packages/pinball_components/test/src/components/golden/kickers.png index 23176923..1b019de9 100644 Binary files a/packages/pinball_components/test/src/components/golden/kickers.png and b/packages/pinball_components/test/src/components/golden/kickers.png differ diff --git a/packages/pinball_components/test/src/components/golden/score/1m.png b/packages/pinball_components/test/src/components/golden/score/1m.png new file mode 100644 index 00000000..bb2f5631 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/score/1m.png differ diff --git a/packages/pinball_components/test/src/components/golden/score/200k.png b/packages/pinball_components/test/src/components/golden/score/200k.png new file mode 100644 index 00000000..c25d116b Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/score/200k.png differ diff --git a/packages/pinball_components/test/src/components/golden/score/20k.png b/packages/pinball_components/test/src/components/golden/score/20k.png new file mode 100644 index 00000000..2a4446c3 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/score/20k.png differ diff --git a/packages/pinball_components/test/src/components/golden/score/5k.png b/packages/pinball_components/test/src/components/golden/score/5k.png new file mode 100644 index 00000000..8f2a7973 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/score/5k.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/golden/sparky-computer.png b/packages/pinball_components/test/src/components/golden/sparky-computer.png index 165a79da..1ade03c2 100644 Binary files a/packages/pinball_components/test/src/components/golden/sparky-computer.png and b/packages/pinball_components/test/src/components/golden/sparky-computer.png differ diff --git a/packages/pinball_components/test/src/components/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 bf261460..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,18 +34,18 @@ 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(), - initialState: GoogleLetterState.active, + initialState: GoogleLetterState.lit, ); final googleLetter = GoogleLetter.test(bloc: bloc); 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/cubit/google_letter_cubit_test.dart b/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart index 390aa192..812e86de 100644 --- a/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart +++ b/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart @@ -10,14 +10,14 @@ void main() { 'onBallContacted emits active', build: GoogleLetterCubit.new, act: (bloc) => bloc.onBallContacted(), - expect: () => [GoogleLetterState.active], + expect: () => [GoogleLetterState.lit], ); blocTest( 'onReset emits inactive', build: GoogleLetterCubit.new, act: (bloc) => bloc.onReset(), - expect: () => [GoogleLetterState.inactive], + expect: () => [GoogleLetterState.dimmed], ); }, ); 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 7ad0e64b..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,9 +10,25 @@ 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 flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.googleWord.letter1.lit.keyName, + Assets.images.googleWord.letter1.dimmed.keyName, + Assets.images.googleWord.letter2.lit.keyName, + Assets.images.googleWord.letter2.dimmed.keyName, + Assets.images.googleWord.letter3.lit.keyName, + Assets.images.googleWord.letter3.dimmed.keyName, + Assets.images.googleWord.letter4.lit.keyName, + Assets.images.googleWord.letter4.dimmed.keyName, + Assets.images.googleWord.letter5.lit.keyName, + Assets.images.googleWord.letter5.dimmed.keyName, + Assets.images.googleWord.letter6.lit.keyName, + Assets.images.googleWord.letter6.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group('Google Letter', () { flameTester.test( @@ -81,16 +97,6 @@ void main() { }, ); - flameTester.test('adds new children', (game) async { - final component = Component(); - final googleLetter = GoogleLetter( - 1, - children: [component], - ); - await game.ensureAdd(googleLetter); - expect(googleLetter.children, contains(component)); - }); - test('throws error when index out of range', () { expect(() => GoogleLetter(-1), throwsA(isA())); expect(() => GoogleLetter(6), throwsA(isA())); @@ -100,11 +106,11 @@ 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(), - initialState: GoogleLetterState.active, + initialState: GoogleLetterState.lit, ); when(bloc.close).thenAnswer((_) async {}); final googleLetter = GoogleLetter.test(bloc: bloc); @@ -116,15 +122,27 @@ void main() { verify(bloc.close).called(1); }); - flameTester.test('adds a GoogleLetterBallContactBehavior', (game) async { - final googleLetter = GoogleLetter(0); - await game.ensureAdd(googleLetter); - expect( - googleLetter.children - .whereType() - .single, - isNotNull, - ); + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final googleLetter = GoogleLetter( + 1, + children: [component], + ); + await game.ensureAdd(googleLetter); + expect(googleLetter.children, contains(component)); + }); + + flameTester.test('a GoogleLetterBallContactBehavior', (game) async { + final googleLetter = GoogleLetter(0); + await game.ensureAdd(googleLetter); + expect( + googleLetter.children + .whereType() + .single, + isNotNull, + ); + }); }); }); } 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 new file mode 100644 index 00000000..6fa6d4a7 --- /dev/null +++ b/packages/pinball_components/test/src/components/kicker/behaviors/kicker_ball_contact_behavior_test.dart @@ -0,0 +1,58 @@ +// 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/kicker/behaviors/behaviors.dart'; + +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); + + group( + 'KickerBallContactBehavior', + () { + test('can be instantiated', () { + expect( + KickerBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact emits onBallContacted when contacts with a ball', + (game) async { + final behavior = KickerBallContactBehavior(); + final bloc = _MockKickerCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: KickerState.lit, + ); + + final kicker = Kicker.test( + side: BoardSide.left, + bloc: bloc, + ); + await kicker.add(behavior); + await game.ensureAdd(kicker); + + behavior.beginContact(_MockBall(), _MockContact()); + + verify(kicker.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/kicker/behaviors/kicker_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/kicker/behaviors/kicker_blinking_behavior_test.dart new file mode 100644 index 00000000..3b6f0c20 --- /dev/null +++ b/packages/pinball_components/test/src/components/kicker/behaviors/kicker_blinking_behavior_test.dart @@ -0,0 +1,50 @@ +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/kicker/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockKickerCubit extends Mock implements KickerCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'KickerBlinkingBehavior', + () { + flameTester.testGameWidget( + 'calls onBlinked after 0.05 seconds when dimmed', + setUp: (game, tester) async { + final behavior = KickerBlinkingBehavior(); + final bloc = _MockKickerCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: KickerState.lit, + ); + + final kicker = Kicker.test( + side: BoardSide.left, + bloc: bloc, + ); + await kicker.add(behavior); + await game.ensureAdd(kicker); + + streamController.add(KickerState.dimmed); + await tester.pump(); + game.update(0.05); + + await streamController.close(); + verify(bloc.onBlinked).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/kicker/cubit/kicker_cubit_test.dart b/packages/pinball_components/test/src/components/kicker/cubit/kicker_cubit_test.dart new file mode 100644 index 00000000..ed1d4a46 --- /dev/null +++ b/packages/pinball_components/test/src/components/kicker/cubit/kicker_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( + 'KickerCubit', + () { + blocTest( + 'onBallContacted emits dimmed', + build: KickerCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [KickerState.dimmed], + ); + + blocTest( + 'onBlinked emits lit', + build: KickerCubit.new, + act: (bloc) => bloc.onBlinked(), + expect: () => [KickerState.lit], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/kicker_test.dart b/packages/pinball_components/test/src/components/kicker_test.dart index aebf9380..4d3fc14d 100644 --- a/packages/pinball_components/test/src/components/kicker_test.dart +++ b/packages/pinball_components/test/src/components/kicker_test.dart @@ -1,29 +1,44 @@ // ignore_for_file: cascade_invocations +import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/bumping_behavior.dart'; +import 'package:pinball_components/src/components/kicker/behaviors/behaviors.dart'; import '../../helpers/helpers.dart'; +class _MockKickerCubit extends Mock implements KickerCubit {} + void main() { group('Kicker', () { - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.kicker.left.lit.keyName, + Assets.images.kicker.left.dimmed.keyName, + Assets.images.kicker.right.lit.keyName, + Assets.images.kicker.right.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { + await game.images.loadAll(assets); final leftKicker = Kicker( side: BoardSide.left, - )..initialPosition = Vector2(-20, 0); + ) + ..initialPosition = Vector2(-20, 0) + ..renderBody = false; final rightKicker = Kicker( side: BoardSide.right, )..initialPosition = Vector2(20, 0); await game.ensureAddAll([leftKicker, rightKicker]); game.camera.followVector2(Vector2.zero()); + await tester.pump(); }, verify: (game, tester) async { await expectLater( @@ -36,8 +51,9 @@ void main() { flameTester.test( 'loads correctly', (game) async { - final kicker = Kicker( + final kicker = Kicker.test( side: BoardSide.left, + bloc: KickerCubit(), ); await game.ensureAdd(kicker); @@ -45,58 +61,72 @@ void main() { }, ); - flameTester.test('adds new children', (game) async { - final component = Component(); - final kicker = Kicker( + // 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 = _MockKickerCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: KickerState.lit, + ); + when(bloc.close).thenAnswer((_) async {}); + final kicker = Kicker.test( side: BoardSide.left, - children: [component], + bloc: bloc, ); + await game.ensureAdd(kicker); - expect(kicker.children, contains(component)); + game.remove(kicker); + await game.ready(); + + verify(bloc.close).called(1); }); - flameTester.test( - 'body is static', - (game) async { + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); final kicker = Kicker( side: BoardSide.left, + children: [component], ); await game.ensureAdd(kicker); + expect(kicker.children, contains(component)); + }); - expect(kicker.body.bodyType, equals(BodyType.static)); - }, - ); - - flameTester.test( - 'has restitution', - (game) async { + flameTester.test('a BumpingBehavior', (game) async { final kicker = Kicker( side: BoardSide.left, ); await game.ensureAdd(kicker); - - final totalRestitution = kicker.body.fixtures.fold( - 0, - (total, fixture) => total + fixture.restitution, + expect( + kicker.children.whereType().single, + isNotNull, ); - expect(totalRestitution, greaterThan(0)); - }, - ); + }); - flameTester.test( - 'has no friction', - (game) async { + flameTester.test('a KickerBallContactBehavior', (game) async { final kicker = Kicker( side: BoardSide.left, ); await game.ensureAdd(kicker); + expect( + kicker.children.whereType().single, + isNotNull, + ); + }); - final totalFriction = kicker.body.fixtures.fold( - 0, - (total, fixture) => total + fixture.friction, + flameTester.test('a KickerBlinkingBehavior', (game) async { + final kicker = Kicker( + side: BoardSide.left, ); - expect(totalFriction, equals(0)); - }, - ); + await game.ensureAdd(kicker); + expect( + kicker.children.whereType().single, + isNotNull, + ); + }); + }); }); } diff --git a/packages/pinball_components/test/src/components/launch_ramp_test.dart b/packages/pinball_components/test/src/components/launch_ramp_test.dart index 2defc168..44fa8609 100644 --- a/packages/pinball_components/test/src/components/launch_ramp_test.dart +++ b/packages/pinball_components/test/src/components/launch_ramp_test.dart @@ -4,19 +4,23 @@ import 'package:flame_forge2d/flame_forge2d.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('LaunchRamp', () { - final tester = FlameTester(TestGame.new); + final flameTester = FlameTester(TestGame.new); - tester.testGameWidget( + flameTester.test('loads correctly', (game) async { + final component = LaunchRamp(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - await game.addFromBlueprint(LaunchRamp()); - await game.ready(); + await game.ensureAdd(LaunchRamp()); game.camera.followVector2(Vector2.zero()); game.camera.zoom = 4.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 2d1b21be..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,14 +7,20 @@ 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, - required int insidePriority, + required int insideZIndex, required Layer insideLayer, }) : super( insideLayer: insideLayer, - insidePriority: insidePriority, + insideZIndex: insideZIndex, orientation: orientation, ); @@ -33,7 +39,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: Layer.spaceshipEntranceRamp, ); await game.ensureAdd(layerSensor); @@ -48,7 +54,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: Layer.spaceshipEntranceRamp, ); await game.ensureAdd(layerSensor); @@ -66,7 +72,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: pathwayLayer, )..layer = openingLayer; await game.ensureAdd(layerSensor); @@ -80,7 +86,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: pathwayLayer, )..layer = openingLayer; await game.ensureAdd(layerSensor); @@ -95,7 +101,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: pathwayLayer, )..layer = openingLayer; await game.ensureAdd(layerSensor); @@ -111,64 +117,63 @@ void main() { group('beginContact', () { late Ball ball; late Body body; + late int insideZIndex; + late Layer insideLayer; setUp(() { - ball = MockBall(); - body = MockBody(); + ball = _MockBall(); + body = _MockBody(); + insideZIndex = 1; + insideLayer = Layer.spaceshipEntranceRamp; when(() => ball.body).thenReturn(body); - when(() => ball.priority).thenReturn(1); when(() => ball.layer).thenReturn(Layer.board); }); flameTester.test( - 'changes ball layer and priority ' + 'changes ball layer and zIndex ' 'when a ball enters and exits a downward oriented LayerSensor', (game) async { final sensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, - insideLayer: Layer.spaceshipEntranceRamp, + insideZIndex: insidePriority, + insideLayer: insideLayer, )..initialPosition = Vector2(0, 10); when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); - sensor.beginContact(ball, MockContact()); - verify(() => ball.layer = sensor.insideLayer).called(1); - verify(() => ball.priority = sensor.insidePriority).called(1); - verify(ball.reorderChildren).called(1); + sensor.beginContact(ball, _MockContact()); + verify(() => ball.layer = insideLayer).called(1); + verify(() => ball.zIndex = insideZIndex).called(1); - when(() => ball.layer).thenReturn(sensor.insideLayer); + when(() => ball.layer).thenReturn(insideLayer); - sensor.beginContact(ball, MockContact()); + sensor.beginContact(ball, _MockContact()); verify(() => ball.layer = Layer.board); - verify(() => ball.priority = RenderPriority.ballOnBoard).called(1); - verify(ball.reorderChildren).called(1); + verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); }); flameTester.test( - 'changes ball layer and priority ' + 'changes ball layer and zIndex ' 'when a ball enters and exits an upward oriented LayerSensor', (game) async { final sensor = TestLayerSensor( orientation: LayerEntranceOrientation.up, - insidePriority: insidePriority, - insideLayer: Layer.spaceshipEntranceRamp, + insideZIndex: insidePriority, + insideLayer: insideLayer, )..initialPosition = Vector2(0, 10); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - sensor.beginContact(ball, MockContact()); - verify(() => ball.layer = sensor.insideLayer).called(1); - verify(() => ball.priority = sensor.insidePriority).called(1); - verify(ball.reorderChildren).called(1); + sensor.beginContact(ball, _MockContact()); + verify(() => ball.layer = insideLayer).called(1); + verify(() => ball.zIndex = insidePriority).called(1); - when(() => ball.layer).thenReturn(sensor.insideLayer); + when(() => ball.layer).thenReturn(insideLayer); - sensor.beginContact(ball, MockContact()); + sensor.beginContact(ball, _MockContact()); verify(() => ball.layer = Layer.board); - verify(() => ball.priority = RenderPriority.ballOnBoard).called(1); - verify(ball.reorderChildren).called(1); + 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/score_component_test.dart b/packages/pinball_components/test/src/components/score_component_test.dart new file mode 100644 index 00000000..69688874 --- /dev/null +++ b/packages/pinball_components/test/src/components/score_component_test.dart @@ -0,0 +1,202 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame/effects.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 assets = [ + Assets.images.score.fiveThousand.keyName, + Assets.images.score.twentyThousand.keyName, + Assets.images.score.twoHundredThousand.keyName, + Assets.images.score.oneMillion.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('ScoreComponent', () { + flameTester.testGameWidget( + 'loads correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + game.camera.followVector2(Vector2.zero()); + await game.ensureAdd( + ScoreComponent( + points: Points.oneMillion, + position: Vector2.zero(), + ), + ); + }, + verify: (game, tester) async { + final texts = game.descendants().whereType().length; + expect(texts, equals(1)); + }, + ); + + flameTester.testGameWidget( + 'has a movement effect', + setUp: (game, tester) async { + await game.images.loadAll(assets); + game.camera.followVector2(Vector2.zero()); + await game.ensureAdd( + ScoreComponent( + points: Points.oneMillion, + position: Vector2.zero(), + ), + ); + + game.update(0.5); + await tester.pump(); + }, + verify: (game, tester) async { + final text = game.descendants().whereType().first; + expect(text.firstChild(), isNotNull); + }, + ); + + flameTester.testGameWidget( + 'is removed once finished', + setUp: (game, tester) async { + await game.images.loadAll(assets); + game.camera.followVector2(Vector2.zero()); + await game.ensureAdd( + ScoreComponent( + points: Points.oneMillion, + position: Vector2.zero(), + ), + ); + + game.update(1); + game.update(0); // Ensure all component removals + await tester.pump(); + }, + verify: (game, tester) async { + expect(game.children.length, equals(0)); + }, + ); + + group('renders correctly', () { + flameTester.testGameWidget( + '5000 points', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.ensureAdd( + ScoreComponent( + points: Points.fiveThousand, + position: Vector2.zero(), + ), + ); + + game.camera + ..followVector2(Vector2.zero()) + ..zoom = 8; + + await tester.pump(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/score/5k.png'), + ); + }, + ); + + flameTester.testGameWidget( + '20000 points', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.ensureAdd( + ScoreComponent( + points: Points.twentyThousand, + position: Vector2.zero(), + ), + ); + + game.camera + ..followVector2(Vector2.zero()) + ..zoom = 8; + + await tester.pump(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/score/20k.png'), + ); + }, + ); + + flameTester.testGameWidget( + '200000 points', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.ensureAdd( + ScoreComponent( + points: Points.twoHundredThousand, + position: Vector2.zero(), + ), + ); + + game.camera + ..followVector2(Vector2.zero()) + ..zoom = 8; + + await tester.pump(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/score/200k.png'), + ); + }, + ); + + flameTester.testGameWidget( + '1000000 points', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.ensureAdd( + ScoreComponent( + points: Points.oneMillion, + position: Vector2.zero(), + ), + ); + + game.camera + ..followVector2(Vector2.zero()) + ..zoom = 8; + + await tester.pump(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/score/1m.png'), + ); + }, + ); + }); + }); + + group('PointsX', () { + test('5k value return 5000', () { + expect(Points.fiveThousand.value, 5000); + }); + + test('20k value return 20000', () { + expect(Points.twentyThousand.value, 20000); + }); + + test('200k value return 200000', () { + expect(Points.twoHundredThousand.value, 200000); + }); + + test('1m value return 1000000', () { + expect(Points.oneMillion.value, 1000000); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/score_text_effects_test.dart b/packages/pinball_components/test/src/components/score_text_effects_test.dart deleted file mode 100644 index 7f828f1d..00000000 --- a/packages/pinball_components/test/src/components/score_text_effects_test.dart +++ /dev/null @@ -1,75 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame/components.dart'; -import 'package:flame/effects.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('ScoreText', () { - final flameTester = FlameTester(TestGame.new); - - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - game.camera.followVector2(Vector2.zero()); - await game.ensureAdd( - ScoreText( - text: '123', - position: Vector2.zero(), - color: Colors.white, - ), - ); - }, - verify: (game, tester) async { - final texts = game.descendants().whereType().length; - expect(texts, equals(1)); - }, - ); - - flameTester.testGameWidget( - 'has a movement effect', - setUp: (game, tester) async { - game.camera.followVector2(Vector2.zero()); - await game.ensureAdd( - ScoreText( - text: '123', - position: Vector2.zero(), - color: Colors.white, - ), - ); - - game.update(0.5); - await tester.pump(); - }, - verify: (game, tester) async { - final text = game.descendants().whereType().first; - expect(text.firstChild(), isNotNull); - }, - ); - - flameTester.testGameWidget( - 'is removed once finished', - setUp: (game, tester) async { - game.camera.followVector2(Vector2.zero()); - await game.ensureAdd( - ScoreText( - text: '123', - position: Vector2.zero(), - color: Colors.white, - ), - ); - - game.update(1); - game.update(0); // Ensure all component removals - }, - verify: (game, tester) async { - expect(game.children.length, equals(0)); - }, - ); - }); -} 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/slingshot_test.dart b/packages/pinball_components/test/src/components/slingshot_test.dart index 69296f78..21885550 100644 --- a/packages/pinball_components/test/src/components/slingshot_test.dart +++ b/packages/pinball_components/test/src/components/slingshot_test.dart @@ -4,7 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.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 'package:pinball_components/src/components/bumping_behavior.dart'; import '../../helpers/helpers.dart'; @@ -15,14 +15,18 @@ void main() { Assets.images.slingshot.lower.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); - const length = 2.0; - const angle = 0.0; + + flameTester.test('loads correctly', (game) async { + final component = Slingshots(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { await game.images.loadAll(assets); - await game.addFromBlueprint(Slingshots()); + await game.ensureAdd(Slingshots()); game.camera.followVector2(Vector2.zero()); await game.ready(); await tester.pump(); @@ -35,68 +39,12 @@ void main() { }, ); - flameTester.test( - 'loads correctly', - (game) async { - final slingshot = Slingshot( - length: length, - angle: angle, - spritePath: assets.first, - ); - await game.ensureAdd(slingshot); - - expect(game.contains(slingshot), isTrue); - }, - ); - - flameTester.test( - 'body is static', - (game) async { - final slingshot = Slingshot( - length: length, - angle: angle, - spritePath: assets.first, - ); - await game.ensureAdd(slingshot); - - expect(slingshot.body.bodyType, equals(BodyType.static)); - }, - ); - - flameTester.test( - 'has restitution', - (game) async { - final slingshot = Slingshot( - length: length, - angle: angle, - spritePath: assets.first, - ); - await game.ensureAdd(slingshot); - - final totalRestitution = slingshot.body.fixtures.fold( - 0, - (total, fixture) => total + fixture.restitution, - ); - expect(totalRestitution, greaterThan(0)); - }, - ); - - flameTester.test( - 'has no friction', - (game) async { - final slingshot = Slingshot( - length: length, - angle: angle, - spritePath: assets.first, - ); - await game.ensureAdd(slingshot); - - final totalFriction = slingshot.body.fixtures.fold( - 0, - (total, fixture) => total + fixture.friction, - ); - expect(totalFriction, equals(0)); - }, - ); + flameTester.test('adds BumpingBehavior', (game) async { + final slingshots = Slingshots(); + await game.ensureAdd(slingshots); + for (final slingshot in slingshots.children) { + expect(slingshot.firstChild(), isNotNull); + } + }); }); } diff --git a/packages/pinball_components/test/src/components/spaceship_rail_test.dart b/packages/pinball_components/test/src/components/spaceship_rail_test.dart index a24b0a17..65e9dbd7 100644 --- a/packages/pinball_components/test/src/components/spaceship_rail_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_rail_test.dart @@ -4,7 +4,6 @@ import 'package:flame_forge2d/flame_forge2d.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'; @@ -17,12 +16,17 @@ void main() { ]; final flameTester = FlameTester(() => TestGame(assets)); + flameTester.test('loads correctly', (game) async { + final component = SpaceshipRail(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { await game.images.loadAll(assets); - await game.addFromBlueprint(SpaceshipRail()); - await game.ready(); + await game.ensureAdd(SpaceshipRail()); await tester.pump(); game.camera.followVector2(Vector2.zero()); @@ -35,18 +39,5 @@ void main() { ); }, ); - - flameTester.test( - 'loads correctly', - (game) async { - final spaceshipRail = SpaceshipRail(); - await game.addFromBlueprint(spaceshipRail); - await game.ready(); - - for (final element in spaceshipRail.components) { - expect(game.contains(element), isTrue); - } - }, - ); }); } diff --git a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart index 1f5a231a..0f2ce13a 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart @@ -25,18 +25,11 @@ void main() { final flameTester = FlameTester(() => TestGame(assets)); group('SpaceshipRamp', () { - flameTester.test( - 'loads correctly', - (game) async { - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - - for (final component in spaceshipRamp.components) { - expect(game.contains(component), isTrue); - } - }, - ); + flameTester.test('loads correctly', (game) async { + final component = SpaceshipRamp(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); group('renders correctly', () { const goldenFilePath = 'golden/spaceship_ramp/'; @@ -46,16 +39,14 @@ void main() { 'inactive sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.inactive, ); @@ -73,17 +64,15 @@ void main() { 'active1 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp.progress(); + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component.progress(); await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active1, ); @@ -101,19 +90,17 @@ void main() { 'active2 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component ..progress() ..progress(); await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active2, ); @@ -131,20 +118,18 @@ void main() { 'active3 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component ..progress() ..progress() ..progress(); await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active3, ); @@ -162,10 +147,11 @@ void main() { 'active4 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component ..progress() ..progress() ..progress() @@ -173,10 +159,7 @@ void main() { await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active4, ); @@ -194,10 +177,11 @@ void main() { 'active5 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component ..progress() ..progress() ..progress() @@ -206,10 +190,7 @@ void main() { await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active5, ); 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 88bd8145..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,18 +34,18 @@ 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(), - initialState: SparkyBumperState.active, + initialState: SparkyBumperState.lit, ); final sparkyBumper = SparkyBumper.test(bloc: bloc); 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 0d938820..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); @@ -17,22 +19,22 @@ void main() { 'SparkyBumperBlinkingBehavior', () { flameTester.testGameWidget( - 'calls onBlinked after 0.05 seconds when inactive', + '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, streamController.stream, - initialState: SparkyBumperState.active, + initialState: SparkyBumperState.lit, ); final sparkyBumper = SparkyBumper.test(bloc: bloc); await sparkyBumper.add(behavior); await game.ensureAdd(sparkyBumper); - streamController.add(SparkyBumperState.inactive); + streamController.add(SparkyBumperState.dimmed); await tester.pump(); game.update(0.05); diff --git a/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart index 4192f806..6310dca2 100644 --- a/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart +++ b/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart @@ -7,17 +7,17 @@ void main() { 'SparkyBumperCubit', () { blocTest( - 'onBallContacted emits inactive', + 'onBallContacted emits dimmed', build: SparkyBumperCubit.new, act: (bloc) => bloc.onBallContacted(), - expect: () => [SparkyBumperState.inactive], + expect: () => [SparkyBumperState.dimmed], ); blocTest( - 'onBlinked emits active', + 'onBlinked emits lit', build: SparkyBumperCubit.new, act: (bloc) => bloc.onBlinked(), - expect: () => [SparkyBumperState.active], + expect: () => [SparkyBumperState.lit], ); }, ); 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 225b5922..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 @@ -6,19 +6,22 @@ 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/bumping_behavior.dart'; import 'package:pinball_components/src/components/sparky_bumper/behaviors/behaviors.dart'; import '../../../helpers/helpers.dart'; +class _MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); @@ -45,11 +48,11 @@ 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(), - initialState: SparkyBumperState.active, + initialState: SparkyBumperState.lit, ); when(bloc.close).thenAnswer((_) async {}); final sparkyBumper = SparkyBumper.test(bloc: bloc); @@ -62,6 +65,30 @@ void main() { }); group('adds', () { + flameTester.test('a SparkyBumperBallContactBehavior', (game) async { + final sparkyBumper = SparkyBumper.a(); + await game.ensureAdd(sparkyBumper); + expect( + sparkyBumper.children + .whereType() + .single, + isNotNull, + ); + }); + + flameTester.test('a SparkyBumperBlinkingBehavior', (game) async { + final sparkyBumper = SparkyBumper.a(); + await game.ensureAdd(sparkyBumper); + expect( + sparkyBumper.children + .whereType() + .single, + isNotNull, + ); + }); + }); + + group("'a' adds", () { flameTester.test('new children', (game) async { final component = Component(); final sparkyBumper = SparkyBumper.a( @@ -71,16 +98,54 @@ void main() { expect(sparkyBumper.children, contains(component)); }); - flameTester.test('a SparkyBumperBallContactBehavior', (game) async { + flameTester.test('a BumpingBehavior', (game) async { final sparkyBumper = SparkyBumper.a(); await game.ensureAdd(sparkyBumper); expect( - sparkyBumper.children - .whereType() - .single, + sparkyBumper.children.whereType().single, + isNotNull, + ); + }); + }); + + group("'b' adds", () { + flameTester.test('new children', (game) async { + final component = Component(); + final sparkyBumper = SparkyBumper.b( + children: [component], + ); + await game.ensureAdd(sparkyBumper); + expect(sparkyBumper.children, contains(component)); + }); + + flameTester.test('a BumpingBehavior', (game) async { + final sparkyBumper = SparkyBumper.b(); + await game.ensureAdd(sparkyBumper); + expect( + sparkyBumper.children.whereType().single, isNotNull, ); }); + + group("'c' adds", () { + flameTester.test('new children', (game) async { + final component = Component(); + final sparkyBumper = SparkyBumper.c( + children: [component], + ); + await game.ensureAdd(sparkyBumper); + expect(sparkyBumper.children, contains(component)); + }); + + flameTester.test('a BumpingBehavior', (game) async { + final sparkyBumper = SparkyBumper.c(); + await game.ensureAdd(sparkyBumper); + expect( + sparkyBumper.children.whereType().single, + isNotNull, + ); + }); + }); }); }); } diff --git a/packages/pinball_components/test/src/components/sparky_computer_test.dart b/packages/pinball_components/test/src/components/sparky_computer_test.dart index 6b19481e..ffba79b6 100644 --- a/packages/pinball_components/test/src/components/sparky_computer_test.dart +++ b/packages/pinball_components/test/src/components/sparky_computer_test.dart @@ -4,21 +4,35 @@ import 'package:flame_forge2d/flame_forge2d.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('SparkyComputer', () { - final tester = FlameTester(TestGame.new); + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.glow.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); - tester.testGameWidget( + flameTester.test('loads correctly', (game) async { + final component = SparkyComputer(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - await game.addFromBlueprint(SparkyComputer()); - await game.ready(); + await game.images.loadAll(assets); + await game.ensureAdd(SparkyComputer()); + await tester.pump(); - game.camera.followVector2(Vector2(-15, -50)); + game.camera + ..followVector2(Vector2(0, -20)) + ..zoom = 7; }, verify: (game, tester) async { await expectLater( diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart index 7eb4c3a9..66d34b14 100644 --- a/packages/pinball_flame/lib/pinball_flame.dart +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -1,8 +1,8 @@ library pinball_flame; -export 'src/blueprint.dart'; export 'src/component_controller.dart'; export 'src/contact_behavior.dart'; export 'src/keyboard_input_controller.dart'; export 'src/parent_is_a.dart'; export 'src/sprite_animation.dart'; +export 'src/z_canvas_component.dart'; diff --git a/packages/pinball_flame/lib/src/blueprint.dart b/packages/pinball_flame/lib/src/blueprint.dart deleted file mode 100644 index c7bd5a5e..00000000 --- a/packages/pinball_flame/lib/src/blueprint.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; - -// TODO(erickzanardo): Keeping this inside our code base so we can experiment -// with the idea, but this is a potential upstream change on Flame. - -/// {@template blueprint} -/// A [Blueprint] is a virtual way of grouping [Component]s that are related. -/// {@endtemplate blueprint} -class Blueprint { - /// {@macro blueprint} - Blueprint({ - Iterable? components, - Iterable? blueprints, - }) { - if (components != null) _components.addAll(components); - if (blueprints != null) { - _blueprints.addAll(blueprints); - for (final blueprint in blueprints) { - _components.addAll(blueprint.components); - } - } - } - - final List _components = []; - - final List _blueprints = []; - - Future _addToParent(Component parent) async { - await parent.addAll(_components); - } - - /// Returns a copy of the components built by this blueprint. - List get components => List.unmodifiable(_components); - - /// Returns a copy of the blueprints built by this blueprint. - List get blueprints => List.unmodifiable(_blueprints); -} - -/// Adds helper methods regarding [Blueprint]s to [FlameGame]. -extension FlameGameBlueprint on Component { - /// Shortcut to add a [Blueprint]s components to its parent. - Future addFromBlueprint(Blueprint blueprint) async { - await blueprint._addToParent(this); - } -} diff --git a/packages/pinball_flame/lib/src/z_canvas_component.dart b/packages/pinball_flame/lib/src/z_canvas_component.dart new file mode 100644 index 00000000..911c3e93 --- /dev/null +++ b/packages/pinball_flame/lib/src/z_canvas_component.dart @@ -0,0 +1,249 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flame/components.dart'; + +/// {@template z_canvas_component} +/// Draws [ZIndex] components after the all non-[ZIndex] components have been +/// drawn. +/// {@endtemplate} +class ZCanvasComponent extends Component { + /// {@macro z_canvas_component} + ZCanvasComponent({ + Iterable? children, + }) : _zCanvas = ZCanvas(), + super(children: children); + + final ZCanvas _zCanvas; + + @override + void renderTree(Canvas canvas) { + _zCanvas.canvas = canvas; + super.renderTree(_zCanvas); + _zCanvas.render(); + } +} + +/// Apply to any [Component] that will be rendered according to a +/// [ZIndex.zIndex]. +/// +/// [ZIndex] components must be descendants of a [ZCanvasComponent]. +/// +/// {@macro z_canvas.render} +mixin ZIndex on Component { + /// The z-index of this component. + /// + /// The higher the value, the later the component will be drawn. Hence, + /// rendering in front of [Component]s with lower [zIndex] values. + int zIndex = 0; + + @override + void renderTree( + Canvas canvas, + ) { + if (canvas is ZCanvas) { + canvas.buffer(this); + } else { + super.renderTree(canvas); + } + } +} + +/// The [ZCanvas] allows to postpone the rendering of [ZIndex] components. +/// +/// You should not use this class directly. +class ZCanvas implements Canvas { + /// The [Canvas] to render to. + /// + /// This is set by [ZCanvasComponent] when rendering. + late Canvas canvas; + + final List _zBuffer = []; + + /// Postpones the rendering of [ZIndex] component and its children. + void buffer(ZIndex component) => _zBuffer.add(component); + + /// Renders all [ZIndex] components and their children. + /// + /// {@template z_canvas.render} + /// The rendering order is defined by the parent [ZIndex]. The children of + /// the same parent are rendered in the order they were added. + /// + /// If two [Component]s ever overlap each other, and have the same + /// [ZIndex.zIndex], there is no guarantee that the first one will be rendered + /// before the second one. + /// {@endtemplate} + void render() => _zBuffer + ..sort((a, b) => a.zIndex.compareTo(b.zIndex)) + ..whereType().forEach(_render) + ..clear(); + + void _render(Component component) => component.renderTree(canvas); + + @override + void clipPath(Path path, {bool doAntiAlias = true}) => + canvas.clipPath(path, doAntiAlias: doAntiAlias); + + @override + void clipRRect(RRect rrect, {bool doAntiAlias = true}) => + canvas.clipRRect(rrect, doAntiAlias: doAntiAlias); + + @override + void clipRect( + Rect rect, { + ClipOp clipOp = ClipOp.intersect, + bool doAntiAlias = true, + }) => + canvas.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias); + + @override + void drawArc( + Rect rect, + double startAngle, + double sweepAngle, + bool useCenter, + Paint paint, + ) => + canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint); + + @override + void drawAtlas( + Image atlas, + List transforms, + List rects, + List? colors, + BlendMode? blendMode, + Rect? cullRect, + Paint paint, + ) => + canvas.drawAtlas( + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ); + + @override + void drawCircle(Offset c, double radius, Paint paint) => canvas.drawCircle( + c, + radius, + paint, + ); + + @override + void drawColor(Color color, BlendMode blendMode) => + canvas.drawColor(color, blendMode); + + @override + void drawDRRect(RRect outer, RRect inner, Paint paint) => + canvas.drawDRRect(outer, inner, paint); + + @override + void drawImage(Image image, Offset offset, Paint paint) => + canvas.drawImage(image, offset, paint); + + @override + void drawImageNine(Image image, Rect center, Rect dst, Paint paint) => + canvas.drawImageNine(image, center, dst, paint); + + @override + void drawImageRect(Image image, Rect src, Rect dst, Paint paint) => + canvas.drawImageRect(image, src, dst, paint); + + @override + void drawLine(Offset p1, Offset p2, Paint paint) => + canvas.drawLine(p1, p2, paint); + + @override + void drawOval(Rect rect, Paint paint) => canvas.drawOval(rect, paint); + + @override + void drawPaint(Paint paint) => canvas.drawPaint(paint); + + @override + void drawParagraph(Paragraph paragraph, Offset offset) => + canvas.drawParagraph(paragraph, offset); + + @override + void drawPath(Path path, Paint paint) => canvas.drawPath(path, paint); + + @override + void drawPicture(Picture picture) => canvas.drawPicture(picture); + + @override + void drawPoints(PointMode pointMode, List points, Paint paint) => + canvas.drawPoints(pointMode, points, paint); + + @override + void drawRRect(RRect rrect, Paint paint) => canvas.drawRRect(rrect, paint); + + @override + void drawRawAtlas( + Image atlas, + Float32List rstTransforms, + Float32List rects, + Int32List? colors, + BlendMode? blendMode, + Rect? cullRect, + Paint paint, + ) => + canvas.drawRawAtlas( + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ); + + @override + void drawRawPoints(PointMode pointMode, Float32List points, Paint paint) => + canvas.drawRawPoints(pointMode, points, paint); + + @override + void drawRect(Rect rect, Paint paint) => canvas.drawRect(rect, paint); + + @override + void drawShadow( + Path path, + Color color, + double elevation, + bool transparentOccluder, + ) => + canvas.drawShadow(path, color, elevation, transparentOccluder); + + @override + void drawVertices(Vertices vertices, BlendMode blendMode, Paint paint) => + canvas.drawVertices(vertices, blendMode, paint); + + @override + int getSaveCount() => canvas.getSaveCount(); + + @override + void restore() => canvas.restore(); + + @override + void rotate(double radians) => canvas.rotate(radians); + + @override + void save() => canvas.save(); + + @override + void saveLayer(Rect? bounds, Paint paint) => canvas.saveLayer(bounds, paint); + + @override + void scale(double sx, [double? sy]) => canvas.scale(sx, sy); + + @override + void skew(double sx, double sy) => canvas.skew(sx, sy); + + @override + void transform(Float64List matrix4) => canvas.transform(matrix4); + + @override + void translate(double dx, double dy) => canvas.translate(dx, dy); +} 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/blueprint_test.dart b/packages/pinball_flame/test/src/blueprint_test.dart deleted file mode 100644 index 402d5059..00000000 --- a/packages/pinball_flame/test/src/blueprint_test.dart +++ /dev/null @@ -1,86 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('Blueprint', () { - final flameTester = FlameTester(FlameGame.new); - - test('correctly sets and gets components', () { - final component1 = Component(); - final component2 = Component(); - final blueprint = Blueprint( - components: [ - component1, - component2, - ], - ); - - expect(blueprint.components.length, 2); - expect(blueprint.components, contains(component1)); - expect(blueprint.components, contains(component2)); - }); - - test('correctly sets and gets blueprints', () { - final blueprint2 = Blueprint( - components: [Component()], - ); - final blueprint1 = Blueprint( - components: [Component()], - blueprints: [blueprint2], - ); - - expect(blueprint1.blueprints, contains(blueprint2)); - }); - - flameTester.test('adds the components to parent on attach', (game) async { - final blueprint = Blueprint( - components: [ - Component(), - Component(), - ], - ); - await game.addFromBlueprint(blueprint); - await game.ready(); - - for (final component in blueprint.components) { - expect(game.children.contains(component), isTrue); - } - }); - - flameTester.test('adds components from a child Blueprint', (game) async { - final childBlueprint = Blueprint( - components: [ - Component(), - Component(), - ], - ); - final parentBlueprint = Blueprint( - components: [ - Component(), - Component(), - ], - blueprints: [ - childBlueprint, - ], - ); - - await game.addFromBlueprint(parentBlueprint); - await game.ready(); - - for (final component in childBlueprint.components) { - expect(game.children, contains(component)); - expect(parentBlueprint.components, contains(component)); - } - for (final component in parentBlueprint.components) { - expect(game.children, contains(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/rendering/golden/rendering/blue_red.png b/packages/pinball_flame/test/src/rendering/golden/rendering/blue_red.png new file mode 100644 index 00000000..4ca86375 Binary files /dev/null and b/packages/pinball_flame/test/src/rendering/golden/rendering/blue_red.png differ diff --git a/packages/pinball_flame/test/src/rendering/golden/rendering/red_blue.png b/packages/pinball_flame/test/src/rendering/golden/rendering/red_blue.png new file mode 100644 index 00000000..a657024f Binary files /dev/null and b/packages/pinball_flame/test/src/rendering/golden/rendering/red_blue.png differ diff --git a/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart b/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart new file mode 100644 index 00000000..b6007bc5 --- /dev/null +++ b/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart @@ -0,0 +1,385 @@ +// ignore_for_file: cascade_invocations + +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart' hide Image; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestCircleComponent extends CircleComponent with ZIndex { + _TestCircleComponent(Color color) + : super( + paint: Paint()..color = color, + radius: 10, + ); +} + +class _MockCanvas extends Mock implements Canvas {} + +class _MockImage extends Mock implements Image {} + +class _MockPicture extends Mock implements Picture {} + +class _MockParagraph extends Mock implements Paragraph {} + +class _MockVertices extends Mock implements Vertices {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(FlameGame.new); + const goldenPrefix = 'golden/rendering/'; + + group('ZCanvasComponent', () { + flameTester.test('loads correctly', (game) async { + final component = ZCanvasComponent(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( + 'red circle renders behind blue circle', + setUp: (game, tester) async { + final canvas = ZCanvasComponent( + children: [ + _TestCircleComponent(Colors.blue)..zIndex = 1, + _TestCircleComponent(Colors.red)..zIndex = 0, + ], + ); + await game.ensureAdd(canvas); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenPrefix}red_blue.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'blue circle renders behind red circle', + setUp: (game, tester) async { + final canvas = ZCanvasComponent( + children: [ + _TestCircleComponent(Colors.blue)..zIndex = 0, + _TestCircleComponent(Colors.red)..zIndex = 1 + ], + ); + await game.ensureAdd(canvas); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenPrefix}blue_red.png'), + ); + }, + ); + }); + + group('ZCanvas', () { + late Canvas canvas; + late Path path; + late RRect rRect; + late Rect rect; + late Paint paint; + late Image atlas; + late BlendMode blendMode; + late Color color; + late Offset offset; + late Float64List float64list; + late Float32List float32list; + late Int32List int32list; + late Picture picture; + late Paragraph paragraph; + late Vertices vertices; + + setUp(() { + canvas = _MockCanvas(); + path = Path(); + rRect = RRect.zero; + rect = Rect.zero; + paint = Paint(); + atlas = _MockImage(); + blendMode = BlendMode.clear; + color = Colors.black; + offset = Offset.zero; + float64list = Float64List(1); + float32list = Float32List(1); + int32list = Int32List(1); + picture = _MockPicture(); + paragraph = _MockParagraph(); + vertices = _MockVertices(); + }); + + test("clipPath calls Canvas's clipPath", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.clipPath(path, doAntiAlias: false); + verify( + () => canvas.clipPath(path, doAntiAlias: false), + ).called(1); + }); + + test("clipRRect calls Canvas's clipRRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.clipRRect(rRect, doAntiAlias: false); + verify( + () => canvas.clipRRect(rRect, doAntiAlias: false), + ).called(1); + }); + + test("clipRect calls Canvas's clipRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.clipRect(rect, doAntiAlias: false); + verify( + () => canvas.clipRect(rect, doAntiAlias: false), + ).called(1); + }); + + test("drawArc calls Canvas's drawArc", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawArc(rect, 0, 1, false, paint); + verify( + () => canvas.drawArc(rect, 0, 1, false, paint), + ).called(1); + }); + + test("drawAtlas calls Canvas's drawAtlas", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint); + verify( + () => canvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint), + ).called(1); + }); + + test("drawCircle calls Canvas's drawCircle", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawCircle(offset, 0, paint); + verify( + () => canvas.drawCircle(offset, 0, paint), + ).called(1); + }); + + test("drawColor calls Canvas's drawColor", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawColor(color, blendMode); + verify( + () => canvas.drawColor(color, blendMode), + ).called(1); + }); + + test("drawDRRect calls Canvas's drawDRRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawDRRect(rRect, rRect, paint); + verify( + () => canvas.drawDRRect(rRect, rRect, paint), + ).called(1); + }); + + test("drawImage calls Canvas's drawImage", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawImage(atlas, offset, paint); + verify( + () => canvas.drawImage(atlas, offset, paint), + ).called(1); + }); + + test("drawImageNine calls Canvas's drawImageNine", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawImageNine(atlas, rect, rect, paint); + verify( + () => canvas.drawImageNine(atlas, rect, rect, paint), + ).called(1); + }); + + test("drawImageRect calls Canvas's drawImageRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawImageRect(atlas, rect, rect, paint); + verify( + () => canvas.drawImageRect(atlas, rect, rect, paint), + ).called(1); + }); + + test("drawLine calls Canvas's drawLine", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawLine(offset, offset, paint); + verify( + () => canvas.drawLine(offset, offset, paint), + ).called(1); + }); + + test("drawOval calls Canvas's drawOval", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawOval(rect, paint); + verify( + () => canvas.drawOval(rect, paint), + ).called(1); + }); + + test("drawPaint calls Canvas's drawPaint", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawPaint(paint); + verify( + () => canvas.drawPaint(paint), + ).called(1); + }); + + test("drawParagraph calls Canvas's drawParagraph", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawParagraph(paragraph, offset); + verify( + () => canvas.drawParagraph(paragraph, offset), + ).called(1); + }); + + test("drawPath calls Canvas's drawPath", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawPath(path, paint); + verify( + () => canvas.drawPath(path, paint), + ).called(1); + }); + + test("drawPicture calls Canvas's drawPicture", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawPicture(picture); + verify( + () => canvas.drawPicture(picture), + ).called(1); + }); + + test("drawPoints calls Canvas's drawPoints", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawPoints(PointMode.points, [offset], paint); + verify( + () => canvas.drawPoints(PointMode.points, [offset], paint), + ).called(1); + }); + + test("drawRRect calls Canvas's drawRRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawRRect(rRect, paint); + verify( + () => canvas.drawRRect(rRect, paint), + ).called(1); + }); + + test("drawRawAtlas calls Canvas's drawRawAtlas", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawRawAtlas( + atlas, + float32list, + float32list, + int32list, + BlendMode.clear, + rect, + paint, + ); + verify( + () => canvas.drawRawAtlas( + atlas, + float32list, + float32list, + int32list, + BlendMode.clear, + rect, + paint, + ), + ).called(1); + }); + + test("drawRawPoints calls Canvas's drawRawPoints", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawRawPoints(PointMode.points, float32list, paint); + verify( + () => canvas.drawRawPoints(PointMode.points, float32list, paint), + ).called(1); + }); + + test("drawRect calls Canvas's drawRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawRect(rect, paint); + verify( + () => canvas.drawRect(rect, paint), + ).called(1); + }); + + test("drawShadow calls Canvas's drawShadow", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawShadow(path, color, 0, false); + verify( + () => canvas.drawShadow(path, color, 0, false), + ).called(1); + }); + + test("drawVertices calls Canvas's drawVertices", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawVertices(vertices, blendMode, paint); + verify( + () => canvas.drawVertices(vertices, blendMode, paint), + ).called(1); + }); + + test("getSaveCount calls Canvas's getSaveCount", () { + final zcanvas = ZCanvas()..canvas = canvas; + when(() => canvas.getSaveCount()).thenReturn(1); + zcanvas.getSaveCount(); + verify(() => canvas.getSaveCount()).called(1); + }); + + test("restore calls Canvas's restore", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.restore(); + verify(() => canvas.restore()).called(1); + }); + + test("rotate calls Canvas's rotate", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.rotate(0); + verify(() => canvas.rotate(0)).called(1); + }); + + test("save calls Canvas's save", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.save(); + verify(() => canvas.save()).called(1); + }); + + test("saveLayer calls Canvas's saveLayer", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.saveLayer(rect, paint); + verify(() => canvas.saveLayer(rect, paint)).called(1); + }); + + test("scale calls Canvas's scale", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.scale(0, 0); + verify(() => canvas.scale(0, 0)).called(1); + }); + + test("skew calls Canvas's skew", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.skew(0, 0); + verify(() => canvas.skew(0, 0)).called(1); + }); + + test("transform calls Canvas's transform", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.transform(float64list); + verify(() => canvas.transform(float64list)).called(1); + }); + + test("translate calls Canvas's translate", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.translate(0, 0); + verify(() => canvas.translate(0, 0)).called(1); + }); + }); +} 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/assets/images/button/pinball_button.png b/packages/pinball_ui/assets/images/button/pinball_button.png new file mode 100644 index 00000000..62373b85 Binary files /dev/null and b/packages/pinball_ui/assets/images/button/pinball_button.png differ diff --git a/packages/pinball_ui/lib/gen/assets.gen.dart b/packages/pinball_ui/lib/gen/assets.gen.dart index 41c45ece..8972e8e0 100644 --- a/packages/pinball_ui/lib/gen/assets.gen.dart +++ b/packages/pinball_ui/lib/gen/assets.gen.dart @@ -10,9 +10,18 @@ import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); + $AssetsImagesButtonGen get button => const $AssetsImagesButtonGen(); $AssetsImagesDialogGen get dialog => const $AssetsImagesDialogGen(); } +class $AssetsImagesButtonGen { + const $AssetsImagesButtonGen(); + + /// File path: assets/images/button/pinball_button.png + AssetGenImage get pinballButton => + const AssetGenImage('assets/images/button/pinball_button.png'); +} + class $AssetsImagesDialogGen { const $AssetsImagesDialogGen(); diff --git a/packages/pinball_ui/lib/pinball_ui.dart b/packages/pinball_ui/lib/pinball_ui.dart index 332286ed..eacb5681 100644 --- a/packages/pinball_ui/lib/pinball_ui.dart +++ b/packages/pinball_ui/lib/pinball_ui.dart @@ -6,3 +6,4 @@ export 'package:url_launcher_platform_interface/url_launcher_platform_interface. export 'src/dialog/dialog.dart'; export 'src/external_links/external_links.dart'; export 'src/theme/theme.dart'; +export 'src/widgets/widgets.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/dialog.dart b/packages/pinball_ui/lib/src/dialog/dialog.dart index 7a224272..4731eb5f 100644 --- a/packages/pinball_ui/lib/src/dialog/dialog.dart +++ b/packages/pinball_ui/lib/src/dialog/dialog.dart @@ -1 +1,2 @@ +export 'pinball_dialog.dart'; export 'pixelated_decoration.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/pinball_dialog.dart b/packages/pinball_ui/lib/src/dialog/pinball_dialog.dart new file mode 100644 index 00000000..8ff04754 --- /dev/null +++ b/packages/pinball_ui/lib/src/dialog/pinball_dialog.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template pinball_dialog} +/// Pinball-themed dialog. +/// {@endtemplate} +class PinballDialog extends StatelessWidget { + /// {@macro pinball_dialog} + const PinballDialog({ + Key? key, + required this.title, + required this.child, + this.subtitle, + }) : super(key: key); + + /// Title shown in the dialog. + final String title; + + /// Optional subtitle shown below the [title]. + final String? subtitle; + + /// Body of the dialog. + final Widget child; + + @override + Widget build(BuildContext context) { + final height = MediaQuery.of(context).size.height * 0.5; + return Center( + child: SizedBox( + height: height, + width: height * 1.4, + child: PixelatedDecoration( + header: subtitle != null + ? _TitleAndSubtitle(title: title, subtitle: subtitle!) + : _Title(title: title), + body: child, + ), + ), + ); + } +} + +class _Title extends StatelessWidget { + const _Title({Key? key, required this.title}) : super(key: key); + + final String title; + + @override + Widget build(BuildContext context) => Text( + title, + style: Theme.of(context).textTheme.headline3!.copyWith( + fontWeight: FontWeight.bold, + color: PinballColors.darkBlue, + ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ); +} + +class _TitleAndSubtitle extends StatelessWidget { + const _TitleAndSubtitle({ + Key? key, + required this.title, + required this.subtitle, + }) : super(key: key); + + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _Title(title: title), + Text( + subtitle, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: textTheme.headline3!.copyWith(fontWeight: FontWeight.normal), + ), + ], + ); + } +} diff --git a/packages/pinball_ui/lib/src/theme/pinball_colors.dart b/packages/pinball_ui/lib/src/theme/pinball_colors.dart index 7018ee3c..d6029422 100644 --- a/packages/pinball_ui/lib/src/theme/pinball_colors.dart +++ b/packages/pinball_ui/lib/src/theme/pinball_colors.dart @@ -9,4 +9,9 @@ abstract class PinballColors { static const Color red = Color(0xFFF03939); static const Color blue = Color(0xFF4B94F6); static const Color transparent = Color(0x00000000); + static const Color loadingDarkRed = Color(0xFFE33B2D); + static const Color loadingLightRed = Color(0xFFEC5E2B); + static const Color loadingDarkBlue = Color(0xFF4087F8); + static const Color loadingLightBlue = Color(0xFF6CCAE4); + static const Color crtBackground = Color(0xFF274E54); } diff --git a/packages/pinball_ui/lib/src/theme/pinball_text_style.dart b/packages/pinball_ui/lib/src/theme/pinball_text_style.dart index 378078fa..5e0a7fa2 100644 --- a/packages/pinball_ui/lib/src/theme/pinball_text_style.dart +++ b/packages/pinball_ui/lib/src/theme/pinball_text_style.dart @@ -19,10 +19,11 @@ abstract class PinballTextStyle { fontSize: 24, package: _fontPackage, fontFamily: _primaryFontFamily, + color: PinballColors.white, ); static const headline3 = TextStyle( - color: PinballColors.white, + color: PinballColors.darkBlue, fontSize: 20, package: _fontPackage, fontFamily: _primaryFontFamily, diff --git a/packages/pinball_ui/lib/src/widgets/animated_ellipsis_text.dart b/packages/pinball_ui/lib/src/widgets/animated_ellipsis_text.dart new file mode 100644 index 00000000..9b52d604 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/animated_ellipsis_text.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// {@tempalte animated_ellipsis_text} +/// Every 500 milliseconds, it will add a new `.` at the end of the given +/// [text]. Once 3 `.` have been added (e.g. `Loading...`), it will reset to +/// zero ellipsis and start over again. +/// {@endtemplate} +class AnimatedEllipsisText extends StatefulWidget { + /// {@macro animated_ellipsis_text} + const AnimatedEllipsisText( + this.text, { + Key? key, + this.style, + }) : super(key: key); + + /// The text that will be animated. + final String text; + + /// Optional [TextStyle] of the given [text]. + final TextStyle? style; + + @override + State createState() => _AnimatedEllipsisText(); +} + +class _AnimatedEllipsisText extends State + with SingleTickerProviderStateMixin { + late final Timer timer; + var _numberOfEllipsis = 0; + + @override + void initState() { + super.initState(); + timer = Timer.periodic(const Duration(milliseconds: 500), (_) { + setState(() { + _numberOfEllipsis++; + _numberOfEllipsis = _numberOfEllipsis % 4; + }); + }); + } + + @override + void dispose() { + if (timer.isActive) timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text( + '${widget.text}${_numberOfEllipsis.toEllipsis()}', + style: widget.style, + ); + } +} + +extension on int { + String toEllipsis() => '.' * this; +} diff --git a/packages/pinball_ui/lib/src/widgets/crt_background.dart b/packages/pinball_ui/lib/src/widgets/crt_background.dart new file mode 100644 index 00000000..202af1d3 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/crt_background.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template crt_background} +/// [BoxDecoration] that provides a CRT-like background efffect. +/// {@endtemplate} +class CrtBackground extends BoxDecoration { + /// {@macro crt_background} + const CrtBackground() + : super( + gradient: const LinearGradient( + begin: Alignment(1, 0.015), + stops: [0.0, 0.5, 0.5, 1], + colors: [ + PinballColors.darkBlue, + PinballColors.darkBlue, + PinballColors.crtBackground, + PinballColors.crtBackground, + ], + tileMode: TileMode.repeated, + ), + ); +} diff --git a/packages/pinball_ui/lib/src/widgets/pinball_button.dart b/packages/pinball_ui/lib/src/widgets/pinball_button.dart new file mode 100644 index 00000000..dd4685c1 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/pinball_button.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/gen/gen.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template pinball_button} +/// Pinball-themed button with pixel art. +/// {@endtemplate} +class PinballButton extends StatelessWidget { + /// {@macro pinball_button} + const PinballButton({ + Key? key, + required this.text, + required this.onTap, + }) : super(key: key); + + /// Text of the button. + final String text; + + /// Tap callback. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + 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), + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/pinball_ui/lib/src/widgets/pinball_loading_indicator.dart b/packages/pinball_ui/lib/src/widgets/pinball_loading_indicator.dart new file mode 100644 index 00000000..ac9b4f46 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/pinball_loading_indicator.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template pinball_loading_indicator} +/// Pixel-art loading indicator +/// {@endtemplate} +class PinballLoadingIndicator extends StatelessWidget { + /// {@macro pinball_loading_indicator} + const PinballLoadingIndicator({ + Key? key, + required this.value, + }) : assert( + value >= 0.0 && value <= 1.0, + 'Progress must be between 0 and 1', + ), + super(key: key); + + /// Progress value + final double value; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _InnerIndicator(value: value, widthFactor: 0.95), + _InnerIndicator(value: value, widthFactor: 0.98), + _InnerIndicator(value: value), + _InnerIndicator(value: value), + _InnerIndicator(value: value, widthFactor: 0.98), + _InnerIndicator(value: value, widthFactor: 0.95) + ], + ); + } +} + +class _InnerIndicator extends StatelessWidget { + const _InnerIndicator({ + Key? key, + required this.value, + this.widthFactor = 1.0, + }) : super(key: key); + + final double value; + final double widthFactor; + + @override + Widget build(BuildContext context) { + return FractionallySizedBox( + widthFactor: widthFactor, + child: Column( + children: [ + LinearProgressIndicator( + backgroundColor: PinballColors.loadingDarkBlue, + color: PinballColors.loadingDarkRed, + value: value, + ), + LinearProgressIndicator( + backgroundColor: PinballColors.loadingLightBlue, + color: PinballColors.loadingLightRed, + value: value, + ), + ], + ), + ); + } +} diff --git a/packages/pinball_ui/lib/src/widgets/widgets.dart b/packages/pinball_ui/lib/src/widgets/widgets.dart new file mode 100644 index 00000000..3aa96c3e --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/widgets.dart @@ -0,0 +1,4 @@ +export 'animated_ellipsis_text.dart'; +export 'crt_background.dart'; +export 'pinball_button.dart'; +export 'pinball_loading_indicator.dart'; diff --git a/packages/pinball_ui/pubspec.yaml b/packages/pinball_ui/pubspec.yaml index a89f7a67..747b1b8f 100644 --- a/packages/pinball_ui/pubspec.yaml +++ b/packages/pinball_ui/pubspec.yaml @@ -23,6 +23,7 @@ flutter: generate: true assets: - assets/images/dialog/ + - assets/images/button/ fonts: - family: PixeloidSans fonts: diff --git a/packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart b/packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart new file mode 100644 index 00000000..85e2c4da --- /dev/null +++ b/packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart @@ -0,0 +1,44 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballDialog', () { + group('with title only', () { + testWidgets('renders the title and the body', (tester) async { + tester.binding.window.physicalSizeTestValue = const Size(2000, 4000); + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + await tester.pumpWidget( + const MaterialApp( + home: PinballDialog(title: 'title', child: Placeholder()), + ), + ); + expect(find.byType(PixelatedDecoration), findsOneWidget); + expect(find.text('title'), findsOneWidget); + expect(find.byType(Placeholder), findsOneWidget); + }); + }); + + group('with title and subtitle', () { + testWidgets('renders the title, subtitle and the body', (tester) async { + tester.binding.window.physicalSizeTestValue = const Size(2000, 4000); + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + await tester.pumpWidget( + MaterialApp( + home: PinballDialog( + title: 'title', + subtitle: 'sub', + child: Icon(Icons.home), + ), + ), + ); + expect(find.byType(PixelatedDecoration), findsOneWidget); + expect(find.text('title'), findsOneWidget); + expect(find.text('sub'), findsOneWidget); + expect(find.byType(Icon), findsOneWidget); + }); + }); + }); +} 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/packages/pinball_ui/test/src/theme/pinball_colors_test.dart b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart index 7e6bc4e0..469ab142 100644 --- a/packages/pinball_ui/test/src/theme/pinball_colors_test.dart +++ b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart @@ -31,5 +31,25 @@ void main() { test('transparent is 0x00000000', () { expect(PinballColors.transparent, const Color(0x00000000)); }); + + test('loadingDarkRed is 0xFFE33B2D', () { + expect(PinballColors.loadingDarkRed, const Color(0xFFE33B2D)); + }); + + test('loadingLightRed is 0xFFEC5E2B', () { + expect(PinballColors.loadingLightRed, const Color(0xFFEC5E2B)); + }); + + test('loadingDarkBlue is 0xFF4087F8', () { + expect(PinballColors.loadingDarkBlue, const Color(0xFF4087F8)); + }); + + test('loadingLightBlue is 0xFF6CCAE4', () { + expect(PinballColors.loadingLightBlue, const Color(0xFF6CCAE4)); + }); + + test('crtBackground is 0xFF274E54', () { + expect(PinballColors.crtBackground, const Color(0xFF274E54)); + }); }); } diff --git a/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart b/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart index 60b382f3..2af092b2 100644 --- a/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart +++ b/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart @@ -14,10 +14,10 @@ void main() { expect(style.fontSize, 24); }); - test('headline3 has fontSize 20 and white color', () { + test('headline3 has fontSize 20 and dark blue color', () { const style = PinballTextStyle.headline3; expect(style.fontSize, 20); - expect(style.color, PinballColors.white); + expect(style.color, PinballColors.darkBlue); }); test('headline4 has fontSize 16 and white color', () { diff --git a/packages/pinball_ui/test/src/widgets/animated_ellipsis_text_test.dart b/packages/pinball_ui/test/src/widgets/animated_ellipsis_text_test.dart new file mode 100644 index 00000000..3800cfed --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/animated_ellipsis_text_test.dart @@ -0,0 +1,30 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('AnimatedEllipsisText', () { + testWidgets( + 'adds a new `.` every 500ms and ' + 'resets back to zero after adding 3', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AnimatedEllipsisText('test'), + ), + ), + ); + expect(find.text('test'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('test.'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('test..'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('test...'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('test'), findsOneWidget); + }); + }); +} diff --git a/packages/pinball_ui/test/src/widgets/crt_background_test.dart b/packages/pinball_ui/test/src/widgets/crt_background_test.dart new file mode 100644 index 00000000..65f27456 --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/crt_background_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('CrtBackground', () { + test('is a BoxDecoration with a LinearGradient', () { + // ignore: prefer_const_constructors + final crtBg = CrtBackground(); + const expectedGradient = LinearGradient( + begin: Alignment(1, 0.015), + stops: [0.0, 0.5, 0.5, 1], + colors: [ + PinballColors.darkBlue, + PinballColors.darkBlue, + PinballColors.crtBackground, + PinballColors.crtBackground, + ], + tileMode: TileMode.repeated, + ); + expect(crtBg, isA()); + expect(crtBg.gradient, expectedGradient); + }); + }); +} diff --git a/packages/pinball_ui/test/src/widgets/pinball_button_test.dart b/packages/pinball_ui/test/src/widgets/pinball_button_test.dart new file mode 100644 index 00000000..064fbb6a --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/pinball_button_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballButton', () { + testWidgets('renders the given text and responds to taps', (tester) async { + var wasTapped = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PinballButton( + text: 'test', + onTap: () { + wasTapped = true; + }, + ), + ), + ), + ), + ); + await tester.tap(find.text('test')); + expect(wasTapped, isTrue); + }); + }); +} diff --git a/packages/pinball_ui/test/src/widgets/pinball_loading_indicator_test.dart b/packages/pinball_ui/test/src/widgets/pinball_loading_indicator_test.dart new file mode 100644 index 00000000..a2cc6d1a --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/pinball_loading_indicator_test.dart @@ -0,0 +1,45 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballLoadingIndicator', () { + group('assert value', () { + test('throws error if value <= 0.0', () { + expect( + () => PinballLoadingIndicator(value: -0.5), + throwsA(isA()), + ); + }); + + test('throws error if value >= 1.0', () { + expect( + () => PinballLoadingIndicator(value: 1.5), + throwsA(isA()), + ); + }); + }); + + testWidgets( + 'renders 12 LinearProgressIndicators and ' + '6 FractionallySizedBox to indicate progress', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PinballLoadingIndicator(value: 0.75), + ), + ), + ); + expect(find.byType(FractionallySizedBox), findsNWidgets(6)); + expect(find.byType(LinearProgressIndicator), findsNWidgets(12)); + final progressIndicators = tester.widgetList( + find.byType(LinearProgressIndicator), + ); + for (final i in progressIndicators) { + expect(i.value, 0.75); + } + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index db5233c3..ffbd3899 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -401,13 +401,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" - mockingjay: - dependency: "direct dev" - description: - name: mockingjay - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" mocktail: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index fa08f453..b98c84a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,6 @@ dev_dependencies: flame_test: ^1.3.0 flutter_test: sdk: flutter - mockingjay: ^0.3.0 mocktail: ^0.3.0 very_good_analysis: ^2.4.0 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..ca1cedff 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -1,10 +1,3 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; @@ -13,7 +6,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 +21,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/game/assets_manager/cubit/assets_manager_cubit_test.dart b/test/assets_manager/cubit/assets_manager_cubit_test.dart similarity index 93% rename from test/game/assets_manager/cubit/assets_manager_cubit_test.dart rename to test/assets_manager/cubit/assets_manager_cubit_test.dart index d0afee34..27d9cedb 100644 --- a/test/game/assets_manager/cubit/assets_manager_cubit_test.dart +++ b/test/assets_manager/cubit/assets_manager_cubit_test.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; void main() { group('AssetsManagerCubit', () { diff --git a/test/game/assets_manager/cubit/assets_manager_state_test.dart b/test/assets_manager/cubit/assets_manager_state_test.dart similarity index 98% rename from test/game/assets_manager/cubit/assets_manager_state_test.dart rename to test/assets_manager/cubit/assets_manager_state_test.dart index 12a42485..4882f880 100644 --- a/test/game/assets_manager/cubit/assets_manager_state_test.dart +++ b/test/assets_manager/cubit/assets_manager_state_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; void main() { group('AssetsManagerState', () { diff --git a/test/assets_manager/views/assets_loading_page_test.dart b/test/assets_manager/views/assets_loading_page_test.dart new file mode 100644 index 00000000..a6210e0c --- /dev/null +++ b/test/assets_manager/views/assets_loading_page_test.dart @@ -0,0 +1,38 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +import '../../helpers/helpers.dart'; + +class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} + +void main() { + late AssetsManagerCubit assetsManagerCubit; + + setUp(() { + final initialAssetsState = AssetsManagerState( + loadables: [Future.value()], + loaded: const [], + ); + assetsManagerCubit = _MockAssetsManagerCubit(); + whenListen( + assetsManagerCubit, + Stream.value(initialAssetsState), + initialState: initialAssetsState, + ); + }); + + group('AssetsLoadingPage', () { + testWidgets('renders an animated text and a pinball loading indicator', + (tester) async { + await tester.pumpApp( + const AssetsLoadingPage(), + assetsManagerCubit: assetsManagerCubit, + ); + expect(find.byType(AnimatedEllipsisText), findsOneWidget); + expect(find.byType(PinballLoadingIndicator), findsOneWidget); + }); + }); +} 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 61% rename from test/game/components/android_acres_test.dart rename to test/game/components/android_acres/android_acres_test.dart index aef6a812..73025551 100644 --- a/test/game/components/android_acres_test.dart +++ b/test/game/components/android_acres/android_acres_test.dart @@ -2,11 +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 'package:pinball_flame/pinball_flame.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -38,21 +38,31 @@ void main() { ); group('AndroidAcres', () { - flameTester.test( - 'loads correctly', - (game) async { - await game.addFromBlueprint(AndroidAcres()); - await game.ready(); - }, - ); + flameTester.test('loads correctly', (game) async { + final component = AndroidAcres(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); group('loads', () { flameTester.test( - 'a Spaceship', + 'an AndroidSpaceship', + (game) async { + await game.ensureAdd(AndroidAcres()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'an AndroidAnimatronic', (game) async { + await game.ensureAdd(AndroidAcres()); expect( - AndroidAcres().blueprints.whereType().single, - isNotNull, + game.descendants().whereType().length, + equals(1), ); }, ); @@ -60,9 +70,10 @@ void main() { flameTester.test( 'a SpaceshipRamp', (game) async { + await game.ensureAdd(AndroidAcres()); expect( - AndroidAcres().blueprints.whereType().single, - isNotNull, + game.descendants().whereType().length, + equals(1), ); }, ); @@ -70,9 +81,10 @@ void main() { flameTester.test( 'a SpaceshipRail', (game) async { + await game.ensureAdd(AndroidAcres()); expect( - AndroidAcres().blueprints.whereType().single, - isNotNull, + game.descendants().whereType().length, + equals(1), ); }, ); @@ -80,10 +92,7 @@ void main() { flameTester.test( 'three AndroidBumper', (game) async { - final androidZone = AndroidAcres(); - await game.addFromBlueprint(androidZone); - await game.ready(); - + await game.ensureAdd(AndroidAcres()); expect( game.descendants().whereType().length, equals(3), @@ -91,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/bottom_group_test.dart b/test/game/components/bottom_group_test.dart index 3254f155..1d9e58ab 100644 --- a/test/game/components/bottom_group_test.dart +++ b/test/game/components/bottom_group_test.dart @@ -10,6 +10,10 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ + Assets.images.kicker.left.lit.keyName, + Assets.images.kicker.left.dimmed.keyName, + Assets.images.kicker.right.lit.keyName, + Assets.images.kicker.right.dimmed.keyName, Assets.images.baseboard.left.keyName, Assets.images.baseboard.right.keyName, Assets.images.flipper.left.keyName, diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index c84ddaa7..17178e87 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -14,8 +14,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; @@ -23,6 +23,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(); @@ -32,7 +40,7 @@ void main() { setUp(() { ball = Ball(baseColor: const Color(0xFF00FFFF)); - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -47,7 +55,7 @@ void main() { test('can be instantiated', () { expect( - BallController(MockBall()), + BallController(_MockBall()), isA(), ); }); @@ -112,9 +120,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 {}); @@ -128,9 +136,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 {}); @@ -144,9 +152,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/dino_desert_test.dart b/test/game/components/dino_desert/dino_desert_test.dart new file mode 100644 index 00000000..20c9ad38 --- /dev/null +++ b/test/game/components/dino_desert/dino_desert_test.dart @@ -0,0 +1,92 @@ +// ignore_for_file: cascade_invocations + +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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.dino.animatronic.head.keyName, + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.topWall.keyName, + Assets.images.dino.topWallTunnel.keyName, + Assets.images.dino.bottomWall.keyName, + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + ]; + + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('DinoDesert', () { + flameTester.test('loads correctly', (game) async { + final component = DinoDesert(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + group('loads', () { + flameTester.test( + 'a ChromeDino', + (game) async { + await game.ensureAdd(DinoDesert()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'DinoWalls', + (game) async { + await game.ensureAdd(DinoDesert()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameTester.test( + 'Slingshots', + (game) async { + await game.ensureAdd(DinoDesert()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + }); + + group('adds', () { + flameTester.test( + 'ScoringBehavior to ChromeDino', + (game) async { + await game.ensureAdd(DinoDesert()); + + final chromeDino = game.descendants().whereType().single; + expect( + chromeDino.firstChild(), + isNotNull, + ); + }, + ); + + flameTester.test('a ChromeDinoBonusBehavior', (game) async { + final dinoDesert = DinoDesert(); + await game.ensureAdd(dinoDesert); + expect( + 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 c1834516..f9e2988d 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 @@ -9,15 +9,19 @@ import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.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'; +class _MockGameBloc extends Mock implements GameBloc {} + void main() { group('FlutterForestBonusBehavior', () { late GameBloc gameBloc; + final assets = [Assets.images.dash.animatronic.keyName]; setUp(() { - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -30,9 +34,14 @@ void main() { blocBuilder: () => gameBloc, ); + 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 = [ @@ -40,13 +49,18 @@ void main() { DashNestBumper.test(bloc: DashNestBumperCubit()), DashNestBumper.test(bloc: DashNestBumperCubit()), ]; - await parent.addAll(bumpers); - await game.ensureAdd(parent); + 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(); verify( @@ -56,8 +70,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 = [ @@ -65,18 +81,67 @@ void main() { DashNestBumper.test(bloc: DashNestBumperCubit()), DashNestBumper.test(bloc: DashNestBumperCubit()), ]; - await parent.addAll(bumpers); - await game.ensureAdd(parent); + 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), + ); + }, + ); + 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( - game.descendants().whereType().single, - isNotNull, + 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 350b1434..7feeb021 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -4,9 +4,19 @@ import 'package:flame/game.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_theme/pinball_theme.dart'; -import '../../helpers/helpers.dart'; +class _MockPinballGame extends Mock implements PinballGame {} + +class _MockBackbox extends Mock implements Backbox {} + +class _MockCameraController extends Mock implements CameraController {} + +class _MockActiveOverlaysNotifier extends Mock + implements ActiveOverlaysNotifier {} + +class _MockPinballAudio extends Mock implements PinballAudio {} void main() { group('GameFlowController', () { @@ -21,7 +31,7 @@ void main() { final previous = GameState.initial(); expect( - GameFlowController(MockPinballGame()).listenWhen(previous, state), + GameFlowController(_MockPinballGame()).listenWhen(previous, state), isTrue, ); }); @@ -32,14 +42,16 @@ void main() { late Backbox backbox; late CameraController cameraController; late GameFlowController gameFlowController; + late PinballAudio pinballAudio; late ActiveOverlaysNotifier overlays; setUp(() { - game = MockPinballGame(); - backbox = MockBackbox(); - cameraController = MockCameraController(); + game = _MockPinballGame(); + backbox = _MockBackbox(); + cameraController = _MockCameraController(); gameFlowController = GameFlowController(game); - overlays = MockActiveOverlaysNotifier(); + overlays = _MockActiveOverlaysNotifier(); + pinballAudio = _MockPinballAudio(); when( () => backbox.initialsInput( @@ -57,6 +69,7 @@ void main() { when(game.firstChild).thenReturn(cameraController); when(() => game.overlays).thenReturn(overlays); when(() => game.characterTheme).thenReturn(DashTheme()); + when(() => game.audio).thenReturn(pinballAudio); }); test( @@ -93,6 +106,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 deca61ee..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 @@ -3,21 +3,37 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../../../helpers/helpers.dart'; +class _MockGameBloc extends Mock implements GameBloc {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.googleWord.letter1.lit.keyName, + Assets.images.googleWord.letter1.dimmed.keyName, + Assets.images.googleWord.letter2.lit.keyName, + Assets.images.googleWord.letter2.dimmed.keyName, + Assets.images.googleWord.letter3.lit.keyName, + Assets.images.googleWord.letter3.dimmed.keyName, + Assets.images.googleWord.letter4.lit.keyName, + Assets.images.googleWord.letter4.dimmed.keyName, + Assets.images.googleWord.letter5.lit.keyName, + Assets.images.googleWord.letter5.dimmed.keyName, + Assets.images.googleWord.letter6.lit.keyName, + Assets.images.googleWord.letter6.dimmed.keyName, + ]; group('GoogleWordBonusBehaviors', () { late GameBloc gameBloc; setUp(() { - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -28,6 +44,7 @@ void main() { final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, blocBuilder: () => gameBloc, + assets: assets, ); flameBlocTester.testGameWidget( diff --git a/test/game/components/google_word/google_word_test.dart b/test/game/components/google_word/google_word_test.dart index 2d7d04e5..11751238 100644 --- a/test/game/components/google_word/google_word_test.dart +++ b/test/game/components/google_word/google_word_test.dart @@ -1,6 +1,9 @@ +// 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:pinball/game/components/google_word/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -8,7 +11,21 @@ import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballTestGame.new); + final assets = [ + Assets.images.googleWord.letter1.lit.keyName, + Assets.images.googleWord.letter1.dimmed.keyName, + Assets.images.googleWord.letter2.lit.keyName, + Assets.images.googleWord.letter2.dimmed.keyName, + Assets.images.googleWord.letter3.lit.keyName, + Assets.images.googleWord.letter3.dimmed.keyName, + Assets.images.googleWord.letter4.lit.keyName, + Assets.images.googleWord.letter4.dimmed.keyName, + Assets.images.googleWord.letter5.lit.keyName, + Assets.images.googleWord.letter5.dimmed.keyName, + Assets.images.googleWord.letter6.lit.keyName, + Assets.images.googleWord.letter6.dimmed.keyName, + ]; + final flameTester = FlameTester(() => EmptyPinballTestGame(assets: assets)); group('GoogleWord', () { flameTester.test( @@ -22,5 +39,14 @@ void main() { expect(letters.length, equals(word.length)); }, ); + + flameTester.test('adds a GoogleWordBonusBehavior', (game) async { + final googleWord = GoogleWord(position: Vector2.zero()); + await game.ensureAdd(googleWord); + expect( + googleWord.children.whereType().single, + isNotNull, + ); + }); }); } 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 a4f3502c..40a952f1 100644 --- a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart +++ b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart @@ -3,15 +3,22 @@ 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:mockingjay/mockingjay.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import '../../../../helpers/helpers.dart'; +class _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 4fb07f40..485183aa 100644 --- a/test/game/components/scoring_behavior_test.dart +++ b/test/game/components/scoring_behavior_test.dart @@ -8,6 +8,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; @@ -16,7 +17,25 @@ 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 = [ + Assets.images.score.fiveThousand.keyName, + Assets.images.score.twentyThousand.keyName, + Assets.images.score.twoHundredThousand.keyName, + Assets.images.score.oneMillion.keyName, + ]; + group('ScoringBehavior', () { group('beginContact', () { late GameBloc bloc; @@ -25,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)); @@ -40,7 +58,7 @@ void main() { audio: audio, ), blocBuilder: () { - bloc = MockGameBloc(); + bloc = _MockGameBloc(); const state = GameState( score: 0, multiplier: 1, @@ -50,21 +68,23 @@ void main() { whenListen(bloc, Stream.value(state), initialState: state); return bloc; }, + assets: assets, ); flameBlocTester.testGameWidget( 'emits Scored event with points', setUp: (game, tester) async { - const points = 20; + const points = Points.oneMillion; final scoringBehavior = ScoringBehavior(points: points); await parent.add(scoringBehavior); - await game.ensureAdd(parent); + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); - scoringBehavior.beginContact(ball, MockContact()); + scoringBehavior.beginContact(ball, _MockContact()); verify( () => bloc.add( - const Scored(points: points), + Scored(points: points.value), ), ).called(1); }, @@ -73,33 +93,34 @@ void main() { flameBlocTester.testGameWidget( 'plays score sound', setUp: (game, tester) async { - const points = 20; - final scoringBehavior = ScoringBehavior(points: points); + final scoringBehavior = ScoringBehavior(points: Points.oneMillion); await parent.add(scoringBehavior); - await game.ensureAdd(parent); + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); - scoringBehavior.beginContact(ball, MockContact()); + scoringBehavior.beginContact(ball, _MockContact()); verify(audio.score).called(1); }, ); flameBlocTester.testGameWidget( - "adds a ScoreText component at Ball's position with points", + "adds a ScoreComponent at Ball's position with points", setUp: (game, tester) async { - const points = 20; + const points = Points.oneMillion; final scoringBehavior = ScoringBehavior(points: points); await parent.add(scoringBehavior); - await game.ensureAdd(parent); + 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(); + final scoreText = game.descendants().whereType(); expect(scoreText.length, equals(1)); expect( - scoreText.first.text, - equals(points.toString()), + scoreText.first.points, + equals(points), ); expect( scoreText.first.position, diff --git a/test/game/components/sparky_fire_zone_test.dart b/test/game/components/sparky_scorch_test.dart similarity index 58% rename from test/game/components/sparky_fire_zone_test.dart rename to test/game/components/sparky_scorch_test.dart index 9b254617..7d9c8c77 100644 --- a/test/game/components/sparky_fire_zone_test.dart +++ b/test/game/components/sparky_scorch_test.dart @@ -1,43 +1,54 @@ // 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'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; +class _MockControlledBall extends Mock implements ControlledBall {} + +class _MockBallController extends Mock implements BallController {} + +class _MockContact extends Mock implements Contact {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.glow.keyName, Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, ]; final flameTester = FlameTester( () => EmptyPinballTestGame(assets: assets), ); - group('SparkyFireZone', () { + group('SparkyScorch', () { flameTester.test('loads correctly', (game) async { - await game.addFromBlueprint(SparkyFireZone()); - await game.ready(); + final component = SparkyScorch(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); }); group('loads', () { flameTester.test( 'a SparkyComputer', (game) async { + await game.ensureAdd(SparkyScorch()); expect( - SparkyFireZone().blueprints.whereType().single, - isNotNull, + game.descendants().whereType().length, + equals(1), ); }, ); @@ -45,13 +56,10 @@ void main() { flameTester.test( 'a SparkyAnimatronic', (game) async { - final sparkyFireZone = SparkyFireZone(); - await game.addFromBlueprint(sparkyFireZone); - await game.ready(); - + await game.ensureAdd(SparkyScorch()); expect( - game.descendants().whereType().single, - isNotNull, + game.descendants().whereType().length, + equals(1), ); }, ); @@ -59,10 +67,7 @@ void main() { flameTester.test( 'three SparkyBumper', (game) async { - final sparkyFireZone = SparkyFireZone(); - await game.addFromBlueprint(sparkyFireZone); - await game.ready(); - + await game.ensureAdd(SparkyScorch()); expect( game.descendants().whereType().length, equals(3), @@ -75,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 {}); @@ -85,7 +90,7 @@ void main() { SparkyAnimatronic(), ]); - sensor.beginContact(ball, MockContact()); + sensor.beginContact(ball, _MockContact()); verify(() => ball.controller.turboCharge()).called(1); }); @@ -93,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([ @@ -103,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 6575d0ae..5246bc94 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'; @@ -12,6 +11,20 @@ import 'package:pinball_components/pinball_components.dart'; 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 = [ @@ -34,6 +47,7 @@ void main() { Assets.images.dino.animatronic.mouth.keyName, Assets.images.dino.animatronic.head.keyName, Assets.images.dino.topWall.keyName, + Assets.images.dino.topWallTunnel.keyName, Assets.images.dino.bottomWall.keyName, Assets.images.dash.animatronic.keyName, Assets.images.dash.bumper.a.active.keyName, @@ -44,17 +58,27 @@ void main() { Assets.images.dash.bumper.main.inactive.keyName, Assets.images.flipper.left.keyName, Assets.images.flipper.right.keyName, - Assets.images.googleWord.letter1.keyName, - Assets.images.googleWord.letter2.keyName, - Assets.images.googleWord.letter3.keyName, - Assets.images.googleWord.letter4.keyName, - Assets.images.googleWord.letter5.keyName, - Assets.images.googleWord.letter6.keyName, - Assets.images.kicker.left.keyName, - Assets.images.kicker.right.keyName, + Assets.images.googleWord.letter1.lit.keyName, + Assets.images.googleWord.letter1.dimmed.keyName, + Assets.images.googleWord.letter2.lit.keyName, + Assets.images.googleWord.letter2.dimmed.keyName, + Assets.images.googleWord.letter3.lit.keyName, + Assets.images.googleWord.letter3.dimmed.keyName, + Assets.images.googleWord.letter4.lit.keyName, + Assets.images.googleWord.letter4.dimmed.keyName, + Assets.images.googleWord.letter5.lit.keyName, + Assets.images.googleWord.letter5.dimmed.keyName, + Assets.images.googleWord.letter6.lit.keyName, + Assets.images.googleWord.letter6.dimmed.keyName, + Assets.images.kicker.left.lit.keyName, + Assets.images.kicker.left.dimmed.keyName, + Assets.images.kicker.right.lit.keyName, + Assets.images.kicker.right.dimmed.keyName, Assets.images.launchRamp.ramp.keyName, Assets.images.launchRamp.foregroundRailing.keyName, Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.multiball.lit.keyName, + Assets.images.multiball.dimmed.keyName, Assets.images.multiplier.x2.lit.keyName, Assets.images.multiplier.x2.dimmed.keyName, Assets.images.multiplier.x3.lit.keyName, @@ -88,28 +112,23 @@ void main() { Assets.images.android.ramp.arrow.active5.keyName, Assets.images.android.rail.main.keyName, Assets.images.android.rail.exit.keyName, - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, Assets.images.sparky.animatronic.keyName, Assets.images.sparky.computer.top.keyName, Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.glow.keyName, Assets.images.sparky.animatronic.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, ]; late GameBloc gameBloc; setUp(() { - gameBloc = MockGameBloc(); + gameBloc = _MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -138,7 +157,7 @@ void main() { (game) async { await game.ready(); expect( - game.children.whereType().length, + game.descendants().whereType().length, equals(1), ); }, @@ -149,18 +168,18 @@ void main() { (game) async { await game.ready(); expect( - game.children.whereType().length, + game.descendants().whereType().length, equals(1), ); }, ); flameBlocTester.test( - 'has only one Plunger', + 'has only one Launcher', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.descendants().whereType().length, equals(1), ); }, @@ -169,16 +188,31 @@ void main() { flameBlocTester.test('has one FlutterForest', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.descendants().whereType().length, equals(1), ); }); + flameBlocTester.test( + 'has only one Multiballs', + (game) async { + await game.ready(); + + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameBlocTester.test( 'one GoogleWord', (game) async { await game.ready(); - expect(game.children.whereType().length, equals(1)); + expect( + game.descendants().whereType().length, + equals(1), + ); }, ); @@ -189,7 +223,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(), @@ -197,7 +231,7 @@ void main() { await game.ready(); expect( - game.controller.listenWhen(MockGameState(), newState), + game.controller.listenWhen(_MockGameState(), newState), isTrue, ); }, @@ -206,7 +240,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( @@ -214,7 +248,7 @@ void main() { greaterThan(0), ); expect( - game.controller.listenWhen(MockGameState(), newState), + game.controller.listenWhen(_MockGameState(), newState), isFalse, ); }, @@ -225,7 +259,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(), @@ -237,7 +271,7 @@ void main() { isTrue, ); expect( - game.controller.listenWhen(MockGameState(), newState), + game.controller.listenWhen(_MockGameState(), newState), isFalse, ); }, @@ -253,7 +287,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(); @@ -273,14 +307,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); @@ -296,14 +330,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); @@ -319,14 +353,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); @@ -338,7 +372,7 @@ void main() { expect(flippers.first.body.linearVelocity.y, isNegative); - final tapUpEvent = MockTapUpInfo(); + final tapUpEvent = _MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); game.onTapUp(tapUpEvent); @@ -350,14 +384,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); @@ -379,13 +413,13 @@ void main() { flameTester.test('tap down moves plunger down', (game) async { await game.ready(); - 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); @@ -393,54 +427,9 @@ void main() { game.onTapDown(tapDownEvent); - expect(plunger.body.linearVelocity.y, equals(7)); - }); - - flameTester.test('tap up releases plunger', (game) async { - 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)); - - 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); }); }); }); @@ -449,13 +438,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); @@ -466,7 +455,7 @@ void main() { await game.ready(); expect( - game.children.whereType().length, + game.descendants().whereType().length, equals(previousBalls.length + 1), ); }, diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index f8b62d05..0ed6e744 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -4,12 +4,22 @@ 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/assets_manager/assets_manager.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 +29,8 @@ void main() { setUp(() async { await Future.wait(game.preLoadAssets()); - characterThemeCubit = MockCharacterThemeCubit(); - gameBloc = MockGameBloc(); + characterThemeCubit = _MockCharacterThemeCubit(); + gameBloc = _MockGameBloc(); whenListen( characterThemeCubit, @@ -47,7 +57,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 [], @@ -57,7 +67,6 @@ void main() { Stream.value(initialAssetsState), initialState: initialAssetsState, ); - await tester.pumpApp( PinballGameView( game: game, @@ -65,22 +74,15 @@ void main() { assetsManagerCubit: assetsManagerCubit, characterThemeCubit: characterThemeCubit, ); - - expect( - find.byWidgetPredicate( - (widget) => - widget is LinearProgressIndicator && widget.value == 0.0, - ), - findsOneWidget, - ); + expect(find.byType(AssetsLoadingPage), findsOneWidget); }, ); 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 +170,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 11e249c7..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,22 +13,21 @@ 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(); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); + const animationDuration = 6; setUp(() async { // 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; @@ -87,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( @@ -102,7 +100,7 @@ void main() { await tester.pump(); - await Future.delayed(const Duration(seconds: 4)); + await Future.delayed(const Duration(seconds: animationDuration)); await tester.pump(); @@ -111,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, ); @@ -133,7 +131,7 @@ void main() { .state(find.byType(BonusAnimation)) .didUpdateWidget(secondAnimation); - await Future.delayed(const Duration(seconds: 4)); + await Future.delayed(const Duration(seconds: animationDuration)); await tester.pump(); diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart index 79cc4f33..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; @@ -147,7 +146,7 @@ void main() { await tester.pump(); // TODO(arturplaczek): remove magic number once this is merged: // https://github.com/flame-engine/flame/pull/1564 - await Future.delayed(const Duration(seconds: 4)); + await Future.delayed(const Duration(seconds: 6)); await expectLater(find.byType(ScoreView), findsOneWidget); }); diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 0345978d..1d7070e0 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,45 +1,68 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/flame.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_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; late GameFlowController gameFlowController; + late CharacterThemeCubit characterThemeCubit; - setUp(() { - game = MockPinballGame(); - gameFlowController = MockGameFlowController(); - + setUp(() async { + Flame.images.prefix = ''; + await Flame.images.load(const DashTheme().animation.keyName); + 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(); + whenListen( + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), + ); + when(() => characterThemeCubit.state) + .thenReturn(const CharacterThemeState.initial()); when(() => game.gameFlowController).thenReturn(gameFlowController); when(gameFlowController.start).thenAnswer((_) {}); }); testWidgets('renders correctly', (tester) async { await tester.pumpApp(PlayButtonOverlay(game: game)); - expect(find.text('Play'), findsOneWidget); }); - testWidgets('calls gameFlowController.start when taped', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - + testWidgets('calls gameFlowController.start when tapped', (tester) async { + await tester.pumpApp( + PlayButtonOverlay(game: game), + characterThemeCubit: characterThemeCubit, + ); await tester.tap(find.text('Play')); await tester.pump(); - verify(gameFlowController.start).called(1); }); testWidgets('displays CharacterSelectionDialog when tapped', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - + await tester.pumpApp( + PlayButtonOverlay(game: game), + characterThemeCubit: characterThemeCubit, + ); await tester.tap(find.text('Play')); await tester.pump(); - expect(find.byType(CharacterSelectionDialog), findsOneWidget); }); }); 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 58b4b126..febf8d36 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -1,15 +1,7 @@ -// -// Copyright (c) 2021, Very Good Ventures -// Use of this source code is governed by an MIT-style -// https://opensource.org/licenses/MIT. -// https://verygood.ventures -// license that can be found in the LICENSE file or at export 'builders.dart'; export 'fakes.dart'; export 'forge2d.dart'; export 'key_testers.dart'; -export 'mocks.dart'; -export 'navigator.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 bae757b6..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 MockBackbox extends Mock implements Backbox {} - -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/navigator.dart b/test/helpers/navigator.dart deleted file mode 100644 index 5a8ea52e..00000000 --- a/test/helpers/navigator.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'helpers.dart'; - -Future expectNavigatesToRoute( - WidgetTester tester, - Route route, { - bool hasFlameGameInside = false, -}) async { - // ignore: avoid_dynamic_calls - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context).push(route); - }, - child: const Text('Tap me'), - ); - }, - ), - ), - ); - - await tester.tap(find.text('Tap me')); - if (hasFlameGameInside) { - // We can't use pumpAndSettle here because the page renders a Flame game - // which is an infinity animation, so it will timeout - await tester.pump(); // Runs the button action - await tester.pump(); // Runs the navigation - } else { - await tester.pumpAndSettle(); - } - - expect(find.byType(Type), findsOneWidget); -} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index be67d4d0..a7d7ae67 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -1,17 +1,11 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mockingjay/mockingjay.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; @@ -19,26 +13,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), @@ -51,7 +50,6 @@ MockAssetsManagerCubit _buildDefaultAssetsManagerCubit() { extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { - MockNavigator? navigator, GameBloc? gameBloc, StartGameBloc? startGameBloc, AssetsManagerCubit? assetsManagerCubit, @@ -64,7 +62,7 @@ extension PumpApp on WidgetTester { MultiRepositoryProvider( providers: [ RepositoryProvider.value( - value: leaderboardRepository ?? MockLeaderboardRepository(), + value: leaderboardRepository ?? _MockLeaderboardRepository(), ), RepositoryProvider.value( value: pinballAudio ?? _buildDefaultPinballAudio(), @@ -73,13 +71,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(), @@ -92,9 +90,7 @@ extension PumpApp on WidgetTester { GlobalMaterialLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, - home: navigator != null - ? MockNavigatorProvider(navigator: navigator, child: widget) - : widget, + home: widget, ), ), ), diff --git a/test/helpers/test_games.dart b/test/helpers/test_games.dart index 79193b1d..7041a2e7 100644 --- a/test/helpers/test_games.dart +++ b/test/helpers/test_games.dart @@ -10,7 +10,7 @@ import 'package:pinball/l10n/l10n.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 _MockAppLocalizations extends Mock implements AppLocalizations {} @@ -28,7 +28,7 @@ class PinballTestGame extends PinballGame { AppLocalizations? l10n, }) : _assets = assets, super( - audio: audio ?? MockPinballAudio(), + audio: audio ?? _MockPinballAudio(), characterTheme: theme ?? const DashTheme(), l10n: l10n ?? _MockAppLocalizations(), ); @@ -51,7 +51,7 @@ class DebugPinballTestGame extends DebugPinballGame { AppLocalizations? l10n, }) : _assets = assets, super( - audio: audio ?? MockPinballAudio(), + audio: audio ?? _MockPinballAudio(), characterTheme: theme ?? const DashTheme(), l10n: l10n ?? _MockAppLocalizations(), ); diff --git a/test/how_to_play/how_to_play_dialog_test.dart b/test/how_to_play/how_to_play_dialog_test.dart new file mode 100644 index 00000000..232aa1d5 --- /dev/null +++ b/test/how_to_play/how_to_play_dialog_test.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:platform_helper/platform_helper.dart'; + +import '../helpers/helpers.dart'; + +class _MockPinballAudio extends Mock implements PinballAudio {} + +class _MockPlatformHelper extends Mock implements PlatformHelper {} + +void main() { + group('HowToPlayDialog', () { + late AppLocalizations l10n; + late PlatformHelper platformHelper; + + setUp(() async { + l10n = await AppLocalizations.delegate.load(const Locale('en')); + platformHelper = _MockPlatformHelper(); + }); + + testWidgets( + 'can be instantiated without passing in a platform helper', + (tester) async { + await tester.pumpApp(HowToPlayDialog()); + expect(find.byType(HowToPlayDialog), findsOneWidget); + }, + ); + + testWidgets('displays content for desktop', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => false); + await tester.pumpApp( + HowToPlayDialog( + platformHelper: platformHelper, + ), + ); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.launchControls), findsOneWidget); + expect(find.text(l10n.flipperControls), findsOneWidget); + expect(find.byType(RotatedBox), findsNWidgets(7)); // controls + }); + + testWidgets('displays content for mobile', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => true); + await tester.pumpApp( + HowToPlayDialog( + platformHelper: platformHelper, + ), + ); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.tapAndHoldRocket), findsOneWidget); + expect(find.text(l10n.tapLeftRightScreen), findsOneWidget); + }); + + testWidgets('disappears after 3 seconds', (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(); + expect(find.byType(HowToPlayDialog), findsOneWidget); + await tester.pump(const Duration(seconds: 4)); + 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); + }); + + testWidgets( + 'plays the I/O Pinball voice over audio on dismiss', + (tester) async { + final audio = _MockPinballAudio(); + await tester.pumpApp( + Builder( + builder: (context) { + return TextButton( + onPressed: () => showHowToPlayDialog(context), + child: const Text('test'), + ); + }, + ), + pinballAudio: audio, + ); + expect(find.byType(HowToPlayDialog), findsNothing); + await tester.tap(find.text('test')); + await tester.pumpAndSettle(); + + await tester.tapAt(Offset.zero); + await tester.pumpAndSettle(); + verify(audio.ioPinballVoiceOver).called(1); + }, + ); + }); +} diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index dc5d70ea..28033030 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -1,162 +1,123 @@ -// ignore_for_file: prefer_const_constructors - -import 'dart:async'; - import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/flame.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/select_character/select_character.dart'; -import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; +import 'package:pinball_ui/pinball_ui.dart'; import '../../helpers/helpers.dart'; +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} + void main() { + TestWidgetsFlutterBinding.ensureInitialized(); late CharacterThemeCubit characterThemeCubit; - setUp(() { - characterThemeCubit = MockCharacterThemeCubit(); + setUp(() async { + Flame.images.prefix = ''; + await Flame.images.load(const DashTheme().animation.keyName); + 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(); whenListen( characterThemeCubit, const Stream.empty(), initialState: const CharacterThemeState.initial(), ); + when(() => characterThemeCubit.state) + .thenReturn(const CharacterThemeState.initial()); }); - group('CharacterSelectionPage', () { - testWidgets('renders CharacterSelectionView', (tester) async { - await tester.pumpApp( - CharacterSelectionDialog(), - characterThemeCubit: characterThemeCubit, - ); - expect(find.byType(CharacterSelectionView), findsOneWidget); - }); - - testWidgets('route returns a valid navigation route', (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( + group('CharacterSelectionDialog', () { + group('showCharacterSelectionDialog', () { + testWidgets('inflates the dialog', (tester) async { + await tester.pumpApp( + Builder( builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context) - .push(CharacterSelectionDialog.route()); - }, - child: Text('Tap me'), + return TextButton( + onPressed: () => showCharacterSelectionDialog(context), + child: const Text('test'), ); }, ), - ), - characterThemeCubit: characterThemeCubit, - ); - - await tester.tap(find.text('Tap me')); - await tester.pumpAndSettle(); - - expect(find.byType(CharacterSelectionDialog), findsOneWidget); + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.text('test')); + await tester.pump(); + expect(find.byType(CharacterSelectionDialog), findsOneWidget); + }); }); - }); - group('CharacterSelectionView', () { - testWidgets('renders correctly', (tester) async { - const titleText = 'Choose your character!'; + testWidgets('selecting a new character calls characterSelected on cubit', + (tester) async { await tester.pumpApp( - CharacterSelectionView(), + const CharacterSelectionDialog(), characterThemeCubit: characterThemeCubit, ); - - expect(find.text(titleText), findsOneWidget); - expect(find.byType(CharacterImageButton), findsNWidgets(4)); - expect(find.byType(TextButton), findsOneWidget); + await tester.tap(find.byKey(const Key('sparky_character_selection'))); + await tester.pump(); + verify( + () => characterThemeCubit.characterSelected(const SparkyTheme()), + ).called(1); }); - testWidgets('calls characterSelected when a character image is tapped', - (tester) async { - const sparkyButtonKey = Key('characterSelectionPage_sparkyButton'); - + testWidgets( + 'tapping the select button dismisses the character ' + 'dialog and shows the how to play dialog', (tester) async { await tester.pumpApp( - CharacterSelectionView(), + const CharacterSelectionDialog(), characterThemeCubit: characterThemeCubit, ); - - await tester.tap(find.byKey(sparkyButtonKey)); - - verify(() => characterThemeCubit.characterSelected(SparkyTheme())) - .called(1); + await tester.tap(find.byType(PinballButton)); + await tester.pumpAndSettle(); + expect(find.byType(CharacterSelectionDialog), findsNothing); + expect(find.byType(HowToPlayDialog), findsOneWidget); }); - group('HowToPlayDialog', () { - testWidgets( - 'is displayed for 3 seconds when start is tapped', + testWidgets('updating the selected character updates the preview', (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context) - .push(CharacterSelectionDialog.route()); - }, - child: Text('Tap me'), - ); - }, - ), - ), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('Tap me')); - await tester.pumpAndSettle(); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsOneWidget); - await tester.pump(Duration(seconds: 3)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsNothing); - }, - ); - - testWidgets( - 'can be dismissed manually before 3 seconds have passed', - (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context) - .push(CharacterSelectionDialog.route()); - }, - child: Text('Tap me'), - ); - }, - ), - ), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('Tap me')); - await tester.pumpAndSettle(); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsOneWidget); - await tester.tapAt(Offset(1, 1)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsNothing); - }, - ); + await tester.pumpApp(_TestCharacterPreview()); + expect(find.text('Dash'), findsOneWidget); + await tester.tap(find.text('test')); + await tester.pump(); + expect(find.text('Android'), findsOneWidget); }); }); +} - testWidgets('CharacterImageButton renders correctly', (tester) async { - await tester.pumpApp( - CharacterImageButton(DashTheme()), - characterThemeCubit: characterThemeCubit, - ); +class _TestCharacterPreview extends StatefulWidget { + @override + State createState() => _TestCharacterPreviewState(); +} - expect(find.byType(Image), findsOneWidget); - }); +class _TestCharacterPreviewState extends State<_TestCharacterPreview> { + late CharacterTheme currentCharacter; + + @override + void initState() { + super.initState(); + currentCharacter = const DashTheme(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: SelectedCharacter(currentCharacter: currentCharacter)), + TextButton( + onPressed: () { + setState(() { + currentCharacter = const AndroidTheme(); + }); + }, + child: const Text('test'), + ) + ], + ); + } } 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/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart deleted file mode 100644 index 1de4c2ad..00000000 --- a/test/start_game/widgets/how_to_play_dialog_test.dart +++ /dev/null @@ -1,69 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/start_game/start_game.dart'; -import 'package:platform_helper/platform_helper.dart'; - -import '../../helpers/helpers.dart'; - -class MockPlatformHelper extends Mock implements PlatformHelper {} - -void main() { - group('HowToPlayDialog', () { - late AppLocalizations l10n; - late PlatformHelper platformHelper; - - setUp(() async { - l10n = await AppLocalizations.delegate.load(Locale('en')); - platformHelper = MockPlatformHelper(); - }); - - testWidgets( - 'can be instantiated without passing in a platform helper', - (tester) async { - await tester.pumpApp(HowToPlayDialog()); - expect(find.byType(HowToPlayDialog), findsOneWidget); - }, - ); - - testWidgets('displays content for desktop', (tester) async { - when(() => platformHelper.isMobile).thenAnswer((_) => false); - await tester.pumpApp( - HowToPlayDialog( - platformHelper: platformHelper, - ), - ); - expect(find.text(l10n.howToPlay), findsOneWidget); - expect(find.text(l10n.tipsForFlips), findsOneWidget); - expect(find.text(l10n.launchControls), findsOneWidget); - expect(find.text(l10n.flipperControls), findsOneWidget); - expect(find.byType(KeyButton), findsNWidgets(7)); - }); - - testWidgets('displays content for mobile', (tester) async { - when(() => platformHelper.isMobile).thenAnswer((_) => true); - await tester.pumpApp( - HowToPlayDialog( - platformHelper: platformHelper, - ), - ); - expect(find.text(l10n.howToPlay), findsOneWidget); - expect(find.text(l10n.tipsForFlips), findsOneWidget); - expect(find.text(l10n.tapAndHoldRocket), findsOneWidget); - expect(find.text(l10n.tapLeftRightScreen), findsOneWidget); - }); - }); - - group('KeyButton', () { - testWidgets('renders correctly', (tester) async { - await tester.pumpApp( - KeyButton(control: Control.a), - ); - - expect(find.text('A'), findsOneWidget); - }); - }); -} 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"> + + - -