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 aeb5742e..8a3a6dbd 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/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..81896c74 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'; 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/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/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); + }, + ); + }); +}