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/lib/game/components/android_acres.dart b/lib/game/components/android_acres/android_acres.dart similarity index 69% rename from lib/game/components/android_acres.dart rename to lib/game/components/android_acres/android_acres.dart index e7330c1f..3d1a8154 100644 --- a/lib/game/components/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -1,6 +1,8 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -16,6 +18,11 @@ class AndroidAcres extends Component { SpaceshipRamp(), SpaceshipRail(), AndroidSpaceship(position: Vector2(-26.5, -28.5)), + AndroidAnimatronic( + children: [ + ScoringBehavior(points: Points.twoHundredThousand), + ], + )..initialPosition = Vector2(-26, -28.25), AndroidBumper.a( children: [ ScoringBehavior(points: Points.twentyThousand), @@ -31,6 +38,13 @@ class AndroidAcres extends Component { ScoringBehavior(points: Points.twentyThousand), ], )..initialPosition = Vector2(-20.5, -13.8), + AndroidSpaceshipBonusBehavior(), ], ); + + /// Creates [AndroidAcres] without any children. + /// + /// This can be used for testing [AndroidAcres]'s behaviors in isolation. + @visibleForTesting + AndroidAcres.test(); } diff --git a/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart new file mode 100644 index 00000000..833ac8e4 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart @@ -0,0 +1,27 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus. +class AndroidSpaceshipBonusBehavior extends Component + with HasGameRef, ParentIsA { + @override + void onMount() { + super.onMount(); + final androidSpaceship = parent.firstChild()!; + + // TODO(alestiago): Refactor subscription management once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1538 + androidSpaceship.bloc.stream.listen((state) { + final listenWhen = state == AndroidSpaceshipState.withBonus; + if (!listenWhen) return; + + gameRef + .read() + .add(const BonusActivated(GameBonus.androidSpaceship)); + androidSpaceship.bloc.onBonusAwarded(); + }); + } +} diff --git a/lib/game/components/android_acres/behaviors/behaviors.dart b/lib/game/components/android_acres/behaviors/behaviors.dart new file mode 100644 index 00000000..e4ac5981 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'android_spaceship_bonus_behavior.dart'; diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 2801a4b3..b0b81239 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,4 +1,4 @@ -export 'android_acres.dart'; +export 'android_acres/android_acres.dart'; export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; diff --git a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart index b884410e..86857ee4 100644 --- a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -3,7 +3,10 @@ import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] +/// Bonus obtained at the [FlutterForest]. +/// +/// When all [DashNestBumper]s are hit at least once three times, the [Signpost] +/// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest] /// is awarded, and the [DashNestBumper.main] releases a new [Ball]. class FlutterForestBonusBehavior extends Component with ParentIsA, HasGameRef { @@ -12,28 +15,36 @@ class FlutterForestBonusBehavior extends Component super.onMount(); final bumpers = parent.children.whereType(); + final signpost = parent.firstChild()!; + final animatronic = parent.firstChild()!; + final canvas = gameRef.firstChild()!; + for (final bumper in bumpers) { // TODO(alestiago): Refactor subscription management once the following is // merged: // https://github.com/flame-engine/flame/pull/1538 bumper.bloc.stream.listen((state) { - final achievedBonus = bumpers.every( + final activatedAllBumpers = bumpers.every( (bumper) => bumper.bloc.state == DashNestBumperState.active, ); - if (achievedBonus) { - gameRef - .read() - .add(const BonusActivated(GameBonus.dashNest)); - gameRef.firstChild()!.add( - ControlledBall.bonus(characterTheme: gameRef.characterTheme) - ..initialPosition = Vector2(17.2, -52.7), - ); - parent.firstChild()?.playing = true; - + if (activatedAllBumpers) { + signpost.bloc.onProgressed(); for (final bumper in bumpers) { bumper.bloc.onReset(); } + + if (signpost.bloc.isFullyProgressed()) { + gameRef + .read() + .add(const BonusActivated(GameBonus.dashNest)); + canvas.add( + ControlledBall.bonus(characterTheme: gameRef.characterTheme) + ..initialPosition = Vector2(17.2, -52.7), + ); + animatronic.playing = true; + signpost.bloc.onProgressed(); + } } }); } diff --git a/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/music/background.ogg b/packages/pinball_audio/assets/music/background.ogg deleted file mode 100644 index 72482f6d..00000000 Binary files a/packages/pinball_audio/assets/music/background.ogg and /dev/null differ diff --git a/packages/pinball_audio/assets/sfx/google.mp3 b/packages/pinball_audio/assets/sfx/google.mp3 new file mode 100644 index 00000000..34167d44 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/google.mp3 differ diff --git a/packages/pinball_audio/assets/sfx/google.ogg b/packages/pinball_audio/assets/sfx/google.ogg deleted file mode 100644 index dafaa8d4..00000000 Binary files a/packages/pinball_audio/assets/sfx/google.ogg and /dev/null differ diff --git a/packages/pinball_audio/assets/sfx/plim.mp3 b/packages/pinball_audio/assets/sfx/plim.mp3 new file mode 100644 index 00000000..a726024d Binary files /dev/null and b/packages/pinball_audio/assets/sfx/plim.mp3 differ diff --git a/packages/pinball_audio/assets/sfx/plim.ogg b/packages/pinball_audio/assets/sfx/plim.ogg deleted file mode 100644 index 137c22b7..00000000 Binary files a/packages/pinball_audio/assets/sfx/plim.ogg and /dev/null differ diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart index 0e6e120e..1b3bdfb9 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -8,14 +8,14 @@ import 'package:flutter/widgets.dart'; class $AssetsMusicGen { const $AssetsMusicGen(); - String get background => 'assets/music/background.ogg'; + String get background => 'assets/music/background.mp3'; } class $AssetsSfxGen { const $AssetsSfxGen(); - String get google => 'assets/sfx/google.ogg'; - String get plim => 'assets/sfx/plim.ogg'; + String get google => 'assets/sfx/google.mp3'; + String get plim => 'assets/sfx/plim.mp3'; } class Assets { diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 393934f0..81d53bee 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -91,11 +91,11 @@ void main() { verify( () => preCacheSingleAudio - .onCall('packages/pinball_audio/assets/sfx/google.ogg'), + .onCall('packages/pinball_audio/assets/sfx/google.mp3'), ).called(1); verify( () => preCacheSingleAudio - .onCall('packages/pinball_audio/assets/music/background.ogg'), + .onCall('packages/pinball_audio/assets/music/background.mp3'), ).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/signpost/active1.png b/packages/pinball_components/assets/images/signpost/active1.png index 1addb228..78997bf6 100644 Binary files a/packages/pinball_components/assets/images/signpost/active1.png and b/packages/pinball_components/assets/images/signpost/active1.png differ diff --git a/packages/pinball_components/assets/images/signpost/active2.png b/packages/pinball_components/assets/images/signpost/active2.png index 081a936c..39caa821 100644 Binary files a/packages/pinball_components/assets/images/signpost/active2.png and b/packages/pinball_components/assets/images/signpost/active2.png differ diff --git a/packages/pinball_components/assets/images/signpost/active3.png b/packages/pinball_components/assets/images/signpost/active3.png index 8d781dfb..f43c190c 100644 Binary files a/packages/pinball_components/assets/images/signpost/active3.png and b/packages/pinball_components/assets/images/signpost/active3.png differ diff --git a/packages/pinball_components/assets/images/signpost/inactive.png b/packages/pinball_components/assets/images/signpost/inactive.png index 6043454b..9fa23330 100644 Binary files a/packages/pinball_components/assets/images/signpost/inactive.png and b/packages/pinball_components/assets/images/signpost/inactive.png differ diff --git a/packages/pinball_components/lib/src/components/android_animatronic.dart b/packages/pinball_components/lib/src/components/android_animatronic.dart new file mode 100644 index 00000000..772d88c4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_animatronic.dart @@ -0,0 +1,71 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template android_animatronic} +/// Animated Android that sits on top of the [AndroidSpaceship]. +/// {@endtemplate} +class AndroidAnimatronic extends BodyComponent + with InitialPosition, Layered, ZIndex { + /// {@macro android_animatronic} + AndroidAnimatronic({Iterable? children}) + : super( + children: [ + _AndroidAnimatronicSpriteAnimationComponent(), + ...?children, + ], + renderBody: false, + ) { + layer = Layer.spaceship; + zIndex = ZIndexes.androidHead; + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: 3.1, + minorRadius: 2, + )..rotate(1.4); + final bodyDef = BodyDef(position: initialPosition); + + return world.createBody(bodyDef)..createFixtureFromShape(shape); + } +} + +class _AndroidAnimatronicSpriteAnimationComponent + extends SpriteAnimationComponent with HasGameRef { + _AndroidAnimatronicSpriteAnimationComponent() + : super( + anchor: Anchor.center, + position: Vector2(-0.24, -2.6), + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.android.spaceship.animatronic.keyName, + ); + + const amountPerRow = 18; + const amountPerColumn = 4; + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + + animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/android_spaceship.dart b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart similarity index 72% rename from packages/pinball_components/lib/src/components/android_spaceship.dart rename to packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart index aa592d1d..4d98b419 100644 --- a/packages/pinball_components/lib/src/components/android_spaceship.dart +++ b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart @@ -5,17 +5,25 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/android_spaceship/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'cubit/android_spaceship_cubit.dart'; + class AndroidSpaceship extends Component { - AndroidSpaceship({required Vector2 position}) - : super( + AndroidSpaceship({ + required Vector2 position, + }) : bloc = AndroidSpaceshipCubit(), + super( children: [ _SpaceshipSaucer()..initialPosition = position, _SpaceshipSaucerSpriteAnimationComponent()..position = position, _LightBeamSpriteComponent()..position = position + Vector2(2.5, 5), - _AndroidHead()..initialPosition = position + Vector2(0.5, 0.25), + AndroidSpaceshipEntrance( + children: [AndroidSpaceshipEntranceBallContactBehavior()], + ), _SpaceshipHole( outsideLayer: Layer.spaceshipExitRail, outsidePriority: ZIndexes.ballOnSpaceshipRail, @@ -26,6 +34,27 @@ class AndroidSpaceship extends Component { )..initialPosition = position - Vector2(-7.5, -1.1), ], ); + + /// Creates an [AndroidSpaceship] without any children. + /// + /// This can be used for testing [AndroidSpaceship]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + AndroidSpaceship.test({ + required this.bloc, + Iterable? children, + }) : super(children: children); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + final AndroidSpaceshipCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } } class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { @@ -123,62 +152,32 @@ class _LightBeamSpriteComponent extends SpriteComponent } } -class _AndroidHead extends BodyComponent with InitialPosition, Layered, ZIndex { - _AndroidHead() +class AndroidSpaceshipEntrance extends BodyComponent + with ParentIsA, Layered { + AndroidSpaceshipEntrance({Iterable? children}) : super( - children: [_AndroidHeadSpriteAnimationComponent()], + children: children, renderBody: false, ) { layer = Layer.spaceship; - zIndex = ZIndexes.androidHead; } @override Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: 3.1, - minorRadius: 2, - )..rotate(1.4); - final bodyDef = BodyDef(position: initialPosition); - - return world.createBody(bodyDef)..createFixtureFromShape(shape); - } -} - -class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent - with HasGameRef { - _AndroidHeadSpriteAnimationComponent() - : super( - anchor: Anchor.center, - position: Vector2(-0.24, -2.6), - ); - - @override - Future onLoad() async { - await super.onLoad(); - - final spriteSheet = gameRef.images.fromCache( - Assets.images.android.spaceship.animatronic.keyName, + final shape = PolygonShape() + ..setAsBox( + 2, + 0.1, + Vector2(-27.4, -37.2), + -0.12, + ); + final fixtureDef = FixtureDef( + shape, + isSensor: true, ); + final bodyDef = BodyDef(); - const amountPerRow = 18; - const amountPerColumn = 4; - final textureSize = Vector2( - spriteSheet.width / amountPerRow, - spriteSheet.height / amountPerColumn, - ); - size = textureSize / 10; - - animation = SpriteAnimation.fromFrameData( - spriteSheet, - SpriteAnimationData.sequenced( - amount: amountPerRow * amountPerColumn, - amountPerRow: amountPerRow, - stepTime: 1 / 24, - textureSize: textureSize, - ), - ); + return world.createBody(bodyDef)..createFixture(fixtureDef); } } diff --git a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart new file mode 100644 index 00000000..58a8b3c3 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart @@ -0,0 +1,16 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class AndroidSpaceshipEntranceBallContactBehavior + extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + parent.parent.bloc.onBallEntered(); + } +} diff --git a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/behaviors.dart new file mode 100644 index 00000000..cbf54e5d --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'android_spaceship_entrance_ball_contact_behavior.dart.dart'; diff --git a/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart new file mode 100644 index 00000000..ad9de251 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_cubit.dart @@ -0,0 +1,13 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'android_spaceship_state.dart'; + +class AndroidSpaceshipCubit extends Cubit { + AndroidSpaceshipCubit() : super(AndroidSpaceshipState.withoutBonus); + + void onBallEntered() => emit(AndroidSpaceshipState.withBonus); + + void onBonusAwarded() => emit(AndroidSpaceshipState.withoutBonus); +} diff --git a/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_state.dart b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_state.dart new file mode 100644 index 00000000..aae41c17 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship/cubit/android_spaceship_state.dart @@ -0,0 +1,8 @@ +// ignore_for_file: public_member_api_docs + +part of 'android_spaceship_cubit.dart'; + +enum AndroidSpaceshipState { + withoutBonus, + withBonus, +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 5b661691..c5ea7f9f 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,5 +1,6 @@ +export 'android_animatronic.dart'; export 'android_bumper/android_bumper.dart'; -export 'android_spaceship.dart'; +export 'android_spaceship/android_spaceship.dart'; export 'backboard/backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; @@ -27,7 +28,7 @@ export 'plunger.dart'; export 'rocket.dart'; export 'score_component.dart'; export 'shapes/shapes.dart'; -export 'signpost.dart'; +export 'signpost/signpost.dart'; export 'slingshot.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; diff --git a/packages/pinball_components/lib/src/components/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/sandbox/lib/stories/android_acres/android_spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart index 6799ef29..976f4894 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart @@ -17,7 +17,7 @@ class AndroidSpaceshipGame extends BallGame { ); static const description = ''' - Shows how the AndroidSpaceship is rendered. + Shows how the AndroidSpaceship and AndroidAnimatronic are rendered. - Activate the "trace" parameter to overlay the body. - Tap anywhere on the screen to spawn a Ball into the game. @@ -28,8 +28,11 @@ class AndroidSpaceshipGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2.zero()); - await add( - AndroidSpaceship(position: Vector2.zero()), + await addAll( + [ + AndroidSpaceship(position: Vector2.zero()), + AndroidAnimatronic(), + ], ); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart index 349dd811..020311d3 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/signpost_game.dart @@ -34,6 +34,6 @@ class SignpostGame extends BallGame { @override void onTap() { super.onTap(); - firstChild()!.progress(); + firstChild()!.bloc.onProgressed(); } } diff --git a/packages/pinball_components/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_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..45a38e8d --- /dev/null +++ b/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart @@ -0,0 +1,55 @@ +// 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_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 { +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'AndroidSpaceshipEntranceBallContactBehavior', + () { + test('can be instantiated', () { + expect( + AndroidSpaceshipEntranceBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact calls onBallEntered when entrance contacts with a ball', + (game) async { + final behavior = AndroidSpaceshipEntranceBallContactBehavior(); + final bloc = _MockAndroidSpaceshipCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: AndroidSpaceshipState.withoutBonus, + ); + + final entrance = AndroidSpaceshipEntrance(); + final androidSpaceship = AndroidSpaceship.test( + bloc: bloc, + children: [entrance], + ); + await entrance.add(behavior); + await game.ensureAdd(androidSpaceship); + + behavior.beginContact(MockBall(), MockContact()); + + verify(androidSpaceship.bloc.onBallEntered).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart b/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart new file mode 100644 index 00000000..47b763af --- /dev/null +++ b/packages/pinball_components/test/src/components/android_spaceship/cubit/android_spaceship_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'AndroidSpaceshipCubit', + () { + blocTest( + 'onBallEntered emits withBonus', + build: AndroidSpaceshipCubit.new, + act: (bloc) => bloc.onBallEntered(), + expect: () => [AndroidSpaceshipState.withBonus], + ); + + blocTest( + 'onBonusAwarded emits withoutBonus', + build: AndroidSpaceshipCubit.new, + act: (bloc) => bloc.onBonusAwarded(), + expect: () => [AndroidSpaceshipState.withoutBonus], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/android_spaceship_test.dart b/packages/pinball_components/test/src/components/android_spaceship_test.dart deleted file mode 100644 index 7e7eda96..00000000 --- a/packages/pinball_components/test/src/components/android_spaceship_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame/components.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('AndroidSpaceship', () { - final assets = [ - Assets.images.android.spaceship.saucer.keyName, - Assets.images.android.spaceship.animatronic.keyName, - Assets.images.android.spaceship.lightBeam.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); - - flameTester.test('loads correctly', (game) async { - final component = AndroidSpaceship(position: Vector2.zero()); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); - }); - - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final canvas = ZCanvasComponent( - children: [AndroidSpaceship(position: Vector2.zero())], - ); - await game.ensureAdd(canvas); - game.camera.followVector2(Vector2.zero()); - await game.ready(); - await tester.pump(); - }, - verify: (game, tester) async { - final animationDuration = game - .descendants() - .whereType() - .last - .animation! - .totalDuration(); - - await expectLater( - find.byGame(), - matchesGoldenFile('golden/android_spaceship/start.png'), - ); - - game.update(animationDuration * 0.5); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/android_spaceship/middle.png'), - ); - - game.update(animationDuration * 0.5); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/android_spaceship/end.png'), - ); - }, - ); - }); -} diff --git a/packages/pinball_components/test/src/components/golden/android_animatronic/end.png b/packages/pinball_components/test/src/components/golden/android_animatronic/end.png new file mode 100644 index 00000000..3d54999f Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_animatronic/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png b/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png new file mode 100644 index 00000000..44916338 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_animatronic/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_animatronic/start.png b/packages/pinball_components/test/src/components/golden/android_animatronic/start.png new file mode 100644 index 00000000..95580e91 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_animatronic/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/end.png b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png index c2a0631a..a64b4724 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_spaceship/end.png and b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png index c6651abd..90361e22 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png and b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/start.png b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png index 25e8863a..649a8654 100644 Binary files a/packages/pinball_components/test/src/components/golden/android_spaceship/start.png and b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png b/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png index 1d3daa81..035a152f 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/finished.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png b/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png index f0312ae5..23c1142d 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/in_between.png differ diff --git a/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png b/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png index 5fd65077..200ab49f 100644 Binary files a/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png and b/packages/pinball_components/test/src/components/golden/camera_zoom/no_zoom.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active1.png b/packages/pinball_components/test/src/components/golden/signpost/active1.png index f11af5a8..0e0f9e79 100644 Binary files a/packages/pinball_components/test/src/components/golden/signpost/active1.png and b/packages/pinball_components/test/src/components/golden/signpost/active1.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active2.png b/packages/pinball_components/test/src/components/golden/signpost/active2.png index 6ddf8786..9dfae564 100644 Binary files a/packages/pinball_components/test/src/components/golden/signpost/active2.png and b/packages/pinball_components/test/src/components/golden/signpost/active2.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/active3.png b/packages/pinball_components/test/src/components/golden/signpost/active3.png index 5e9b0005..a99c9e48 100644 Binary files a/packages/pinball_components/test/src/components/golden/signpost/active3.png and b/packages/pinball_components/test/src/components/golden/signpost/active3.png differ diff --git a/packages/pinball_components/test/src/components/golden/signpost/inactive.png b/packages/pinball_components/test/src/components/golden/signpost/inactive.png index 7ed00fba..7f089716 100644 Binary files a/packages/pinball_components/test/src/components/golden/signpost/inactive.png and b/packages/pinball_components/test/src/components/golden/signpost/inactive.png differ diff --git a/packages/pinball_components/test/src/components/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/test/game/components/android_acres_test.dart b/test/game/components/android_acres/android_acres_test.dart similarity index 79% rename from test/game/components/android_acres_test.dart rename to test/game/components/android_acres/android_acres_test.dart index 4c5e4cb7..73025551 100644 --- a/test/game/components/android_acres_test.dart +++ b/test/game/components/android_acres/android_acres_test.dart @@ -2,10 +2,11 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -45,7 +46,7 @@ void main() { group('loads', () { flameTester.test( - 'a Spaceship', + 'an AndroidSpaceship', (game) async { await game.ensureAdd(AndroidAcres()); expect( @@ -55,6 +56,17 @@ void main() { }, ); + flameTester.test( + 'an AndroidAnimatronic', + (game) async { + await game.ensureAdd(AndroidAcres()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameTester.test( 'a SpaceshipRamp', (game) async { @@ -88,5 +100,14 @@ void main() { }, ); }); + + flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async { + final androidAcres = AndroidAcres(); + await game.ensureAdd(androidAcres); + expect( + androidAcres.children.whereType().single, + isNotNull, + ); + }); }); } diff --git a/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart b/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart new file mode 100644 index 00000000..eb8ad211 --- /dev/null +++ b/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart @@ -0,0 +1,79 @@ +// 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'; + +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/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart index 3481cb38..2de84edd 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 @@ -16,6 +16,7 @@ import '../../../../helpers/helpers.dart'; void main() { group('FlutterForestBonusBehavior', () { late GameBloc gameBloc; + final assets = [Assets.images.dash.animatronic.keyName]; setUp(() { gameBloc = MockGameBloc(); @@ -31,9 +32,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 = [ @@ -41,12 +47,18 @@ void main() { DashNestBumper.test(bloc: DashNestBumperCubit()), DashNestBumper.test(bloc: DashNestBumperCubit()), ]; + final animatronic = DashAnimatronic(); + final signpost = Signpost.test(bloc: SignpostCubit()); await game.ensureAdd(ZCanvasComponent(children: [parent])); - await parent.ensureAddAll([...bumpers, behavior]); + await parent.ensureAddAll([...bumpers, animatronic, signpost]); + await parent.ensureAdd(behavior); - for (final bumper in bumpers) { - bumper.bloc.onBallContacted(); - } + expect(game.descendants().whereType(), equals(bumpers)); + bumpers.forEach(_contactedBumper); + await tester.pump(); + bumpers.forEach(_contactedBumper); + await tester.pump(); + bumpers.forEach(_contactedBumper); await tester.pump(); verify( @@ -56,8 +68,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 +79,68 @@ void main() { DashNestBumper.test(bloc: DashNestBumperCubit()), DashNestBumper.test(bloc: DashNestBumperCubit()), ]; + final animatronic = DashAnimatronic(); + final signpost = Signpost.test(bloc: SignpostCubit()); await game.ensureAdd(ZCanvasComponent(children: [parent])); + await parent.ensureAddAll([...bumpers, animatronic, signpost]); await parent.ensureAdd(behavior); - for (final bumper in bumpers) { - bumper.bloc.onBallContacted(); - } + expect(game.descendants().whereType(), equals(bumpers)); + bumpers.forEach(_contactedBumper); + await tester.pump(); + bumpers.forEach(_contactedBumper); + await tester.pump(); + bumpers.forEach(_contactedBumper); + await tester.pump(); + await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + 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), + ); - // expect( - // game.descendants().whereType().single, - // isNotNull, - // ); + bumpers.forEach(_contactedBumper); + await tester.pump(); + expect( + signpost.bloc.state, + equals(SignpostState.active2), + ); + + bumpers.forEach(_contactedBumper); + await tester.pump(); + expect( + signpost.bloc.state, + equals(SignpostState.inactive), + ); }, ); }); diff --git a/test/game/components/flutter_forest/flutter_forest_test.dart b/test/game/components/flutter_forest/flutter_forest_test.dart index 4f32e0f4..5761a9eb 100644 --- a/test/game/components/flutter_forest/flutter_forest_test.dart +++ b/test/game/components/flutter_forest/flutter_forest_test.dart @@ -4,6 +4,7 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import '../../../helpers/helpers.dart'; @@ -31,8 +32,8 @@ void main() { 'loads correctly', (game) async { final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - expect(game.contains(flutterForest), isTrue); + await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); + expect(game.descendants(), contains(flutterForest)); }, ); @@ -41,10 +42,9 @@ void main() { 'a Signpost', (game) async { final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - + await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); expect( - flutterForest.descendants().whereType().length, + game.descendants().whereType().length, equals(1), ); }, @@ -54,11 +54,10 @@ void main() { 'a DashAnimatronic', (game) async { final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - + await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); expect( - flutterForest.firstChild(), - isNotNull, + game.descendants().whereType().length, + equals(1), ); }, ); @@ -67,10 +66,9 @@ void main() { 'three DashNestBumper', (game) async { final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - + await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); expect( - flutterForest.descendants().whereType().length, + game.descendants().whereType().length, equals(3), ); },