diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 8fcab789..1ba8457e 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -82,6 +82,9 @@ class GameState extends Equatable { /// The score displayed at the game. int get displayScore => roundScore + totalScore; + /// The max multiplier in game. + bool get isMaxMultiplier => multiplier == 6; + GameState copyWith({ int? totalScore, int? roundScore, diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index fd59ace3..77f30069 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -16,41 +16,44 @@ class AndroidAcres extends Component { AndroidAcres() : super( children: [ + SpaceshipRamp( + children: [ + RampShotBehavior(points: Points.fiveThousand), + RampBonusBehavior(points: Points.oneMillion), + RampProgressBehavior(), + RampMultiplierBehavior(), + RampResetBehavior(), + ], + ), + SpaceshipRail(), + AndroidBumper.a( + children: [ + ScoringContactBehavior(points: Points.twentyThousand), + BumperNoiseBehavior(), + ], + )..initialPosition = Vector2(-25.2, 1.5), + AndroidBumper.b( + children: [ + ScoringContactBehavior(points: Points.twentyThousand), + BumperNoiseBehavior(), + ], + )..initialPosition = Vector2(-32.9, -9.3), + AndroidBumper.cow( + children: [ + ScoringContactBehavior(points: Points.twentyThousand), + BumperNoiseBehavior(), + CowBumperNoiseBehavior(), + ], + )..initialPosition = Vector2(-20.7, -13), FlameBlocProvider( create: AndroidSpaceshipCubit.new, children: [ - SpaceshipRamp( - children: [ - RampShotBehavior(points: Points.fiveThousand), - RampBonusBehavior(points: Points.oneMillion), - ], - ), - SpaceshipRail(), AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidAnimatronic( children: [ ScoringContactBehavior(points: Points.twoHundredThousand), ], )..initialPosition = Vector2(-26, -28.25), - AndroidBumper.a( - children: [ - ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), - ], - )..initialPosition = Vector2(-25.2, 1.5), - AndroidBumper.b( - children: [ - ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), - ], - )..initialPosition = Vector2(-32.9, -9.3), - AndroidBumper.cow( - children: [ - ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), - CowBumperNoiseBehavior(), - ], - )..initialPosition = Vector2(-20.7, -13), AndroidSpaceshipBonusBehavior(), ], ), diff --git a/lib/game/components/android_acres/behaviors/behaviors.dart b/lib/game/components/android_acres/behaviors/behaviors.dart index 91b1e132..c4e44a6e 100644 --- a/lib/game/components/android_acres/behaviors/behaviors.dart +++ b/lib/game/components/android_acres/behaviors/behaviors.dart @@ -1,3 +1,6 @@ export 'android_spaceship_bonus_behavior.dart'; export 'ramp_bonus_behavior.dart'; +export 'ramp_multiplier_behavior.dart'; +export 'ramp_progress_behavior.dart'; +export 'ramp_reset_behavior.dart'; export 'ramp_shot_behavior.dart'; diff --git a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart index bc28650f..6984ca68 100644 --- a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart +++ b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart @@ -1,60 +1,40 @@ -import 'dart:async'; - import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; /// {@template ramp_bonus_behavior} /// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp]. /// {@endtemplate} -class RampBonusBehavior extends Component with ParentIsA { +class RampBonusBehavior extends Component + with FlameBlocListenable { /// {@macro ramp_bonus_behavior} RampBonusBehavior({ required Points points, }) : _points = points, super(); - /// Creates a [RampBonusBehavior]. - /// - /// This can be used for testing [RampBonusBehavior] in isolation. - @visibleForTesting - RampBonusBehavior.test({ - required Points points, - required this.subscription, - }) : _points = points, - super(); - final Points _points; - /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. - @visibleForTesting - StreamSubscription? subscription; - @override - void onMount() { - super.onMount(); - - subscription = subscription ?? - parent.bloc.stream.listen((state) { - final achievedOneMillionPoints = state.hits % 10 == 0; - - if (achievedOneMillionPoints) { - parent.add( - ScoringBehavior( - points: _points, - position: Vector2(0, -60), - duration: 2, - ), - ); - } - }); + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + final hitsIncreased = previousState.hits < newState.hits; + final achievedOneMillionPoints = newState.hits % 10 == 0; + + return hitsIncreased && achievedOneMillionPoints; } @override - void onRemove() { - subscription?.cancel(); - super.onRemove(); + void onNewState(SpaceshipRampState state) { + parent!.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -60), + duration: 2, + ), + ); } } diff --git a/lib/game/components/android_acres/behaviors/ramp_multiplier_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_multiplier_behavior.dart new file mode 100644 index 00000000..90211376 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_multiplier_behavior.dart @@ -0,0 +1,27 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Increases the multiplier when a [Ball] is shot 5 times into the +/// [SpaceshipRamp]. +class RampMultiplierBehavior extends Component + with FlameBlocListenable { + @override + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + final hitsIncreased = previousState.hits < newState.hits; + final achievedFiveShots = newState.hits % 5 == 0; + final notMaxMultiplier = + !readBloc().state.isMaxMultiplier; + return hitsIncreased & achievedFiveShots && notMaxMultiplier; + } + + @override + void onNewState(SpaceshipRampState state) { + readBloc().add(const MultiplierIncreased()); + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_progress_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_progress_behavior.dart new file mode 100644 index 00000000..fab67b4e --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_progress_behavior.dart @@ -0,0 +1,34 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Changes arrow lit when a [Ball] is shot into the [SpaceshipRamp]. +class RampProgressBehavior extends Component + with FlameBlocListenable { + @override + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + return previousState.hits < newState.hits; + } + + @override + void onNewState(SpaceshipRampState state) { + final gameBloc = readBloc(); + final spaceshipCubit = readBloc(); + + final canProgress = !gameBloc.state.isMaxMultiplier || + (gameBloc.state.isMaxMultiplier && !state.arrowFullyLit); + + if (canProgress) { + spaceshipCubit.onProgressed(); + } + + if (spaceshipCubit.state.arrowFullyLit && !gameBloc.state.isMaxMultiplier) { + spaceshipCubit.onProgressed(); + } + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_reset_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_reset_behavior.dart new file mode 100644 index 00000000..314a4be7 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_reset_behavior.dart @@ -0,0 +1,19 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Reset [SpaceshipRamp] state when GameState.rounds changes. +class RampResetBehavior extends Component + with FlameBlocListenable { + @override + bool listenWhen(GameState previousState, GameState newState) { + return previousState.rounds != newState.rounds; + } + + @override + void onNewState(GameState state) { + readBloc().onReset(); + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart index b15f5e30..b71e9a46 100644 --- a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart +++ b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart @@ -1,64 +1,36 @@ -import 'dart:async'; - import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flutter/cupertino.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; /// {@template ramp_shot_behavior} /// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. /// {@endtemplate} class RampShotBehavior extends Component - with ParentIsA, FlameBlocReader { + with FlameBlocListenable { /// {@macro ramp_shot_behavior} RampShotBehavior({ required Points points, }) : _points = points, super(); - /// Creates a [RampShotBehavior]. - /// - /// This can be used for testing [RampShotBehavior] in isolation. - @visibleForTesting - RampShotBehavior.test({ - required Points points, - required this.subscription, - }) : _points = points, - super(); - final Points _points; - /// Subscription to [SpaceshipRampState] at [SpaceshipRamp]. - @visibleForTesting - StreamSubscription? subscription; - @override - void onMount() { - super.onMount(); - - subscription = subscription ?? - parent.bloc.stream.listen((state) { - final achievedOneMillionPoints = state.hits % 10 == 0; - - if (!achievedOneMillionPoints) { - bloc.add(const MultiplierIncreased()); - - parent.add( - ScoringBehavior( - points: _points, - position: Vector2(0, -45), - ), - ); - } - }); + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + return previousState.hits < newState.hits; } @override - void onRemove() { - subscription?.cancel(); - super.onRemove(); + void onNewState(SpaceshipRampState state) { + parent!.add( + ScoringBehavior( + points: _points, + position: Vector2(0, -45), + ), + ); } } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart index db98a30a..bb641a82 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart @@ -16,7 +16,8 @@ class RampBallAscendingContactBehavior if (other is! Ball) return; if (other.body.linearVelocity.y < 0) { - parent.parent.bloc.onAscendingBallEntered(); + readBloc() + .onAscendingBallEntered(); } } } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart index c3dc9e3e..8dc0f69f 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart @@ -6,9 +6,17 @@ part 'spaceship_ramp_state.dart'; class SpaceshipRampCubit extends Cubit { SpaceshipRampCubit() : super(const SpaceshipRampState.initial()); - void onAscendingBallEntered() { + void onAscendingBallEntered() => emit(state.copyWith(hits: state.hits + 1)); + + void onProgressed() { + final index = ArrowLightState.values.indexOf(state.lightState); emit( - state.copyWith(hits: state.hits + 1), + state.copyWith( + lightState: + ArrowLightState.values[(index + 1) % ArrowLightState.values.length], + ), ); } + + void onReset() => emit(const SpaceshipRampState.initial()); } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart index 2979f05f..f73110da 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart @@ -1,22 +1,55 @@ +// ignore_for_file: comment_references + part of 'spaceship_ramp_cubit.dart'; class SpaceshipRampState extends Equatable { const SpaceshipRampState({ required this.hits, + required this.lightState, }) : assert(hits >= 0, "Hits can't be negative"); - const SpaceshipRampState.initial() : this(hits: 0); + const SpaceshipRampState.initial() + : this( + hits: 0, + lightState: ArrowLightState.inactive, + ); final int hits; + final ArrowLightState lightState; + + bool get arrowFullyLit => lightState == ArrowLightState.active5; SpaceshipRampState copyWith({ int? hits, + ArrowLightState? lightState, }) { return SpaceshipRampState( hits: hits ?? this.hits, + lightState: lightState ?? this.lightState, ); } @override - List get props => [hits]; + List get props => [hits, lightState]; +} + +/// Indicates the state of the arrow on the [SpaceshipRamp]. +enum ArrowLightState { + /// Arrow with no lights lit up. + inactive, + + /// Arrow with 1 light lit up. + active1, + + /// Arrow with 2 lights lit up. + active2, + + /// Arrow with 3 lights lit up. + active3, + + /// Arrow with 4 lights lit up. + active4, + + /// Arrow with all 5 lights lit up. + active5, } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart index 07a5e79b..8044b79a 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/gen/assets.gen.dart'; @@ -19,30 +20,32 @@ class SpaceshipRamp extends Component { Iterable? children, }) : this._( children: children, - bloc: SpaceshipRampCubit(), ); SpaceshipRamp._({ Iterable? children, - required this.bloc, }) : super( children: [ - _SpaceshipRampOpening( - outsideLayer: Layer.spaceship, - outsidePriority: ZIndexes.ballOnSpaceship, - rotation: math.pi, - ) - ..initialPosition = Vector2(-13.7, -18.6) - ..layer = Layer.spaceshipEntranceRamp, - _SpaceshipRampBackground(), - SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5), - _SpaceshipRampForegroundRailing(), - SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), - _SpaceshipRampBackgroundRailingSpriteComponent(), - SpaceshipRampArrowSpriteComponent( - current: bloc.state.hits, + FlameBlocProvider( + create: SpaceshipRampCubit.new, + children: [ + _SpaceshipRampOpening( + outsideLayer: Layer.spaceship, + outsidePriority: ZIndexes.ballOnSpaceship, + rotation: math.pi, + ) + ..initialPosition = Vector2(-13.7, -18.6) + ..layer = Layer.spaceshipEntranceRamp, + _SpaceshipRampBackground(), + SpaceshipRampBoardOpening() + ..initialPosition = Vector2(3.4, -39.5), + _SpaceshipRampForegroundRailing(), + SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), + _SpaceshipRampBackgroundRailingSpriteComponent(), + SpaceshipRampArrowSpriteComponent(), + ...?children, + ], ), - ...?children, ], ); @@ -51,16 +54,8 @@ class SpaceshipRamp extends Component { /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation. @visibleForTesting SpaceshipRamp.test({ - required this.bloc, - }) : super(); - - final SpaceshipRampCubit bloc; - - @override - void onRemove() { - bloc.close(); - super.onRemove(); - } + Iterable? children, + }) : super(children: children); } class _SpaceshipRampBackground extends BodyComponent @@ -167,82 +162,71 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// {@endtemplate} @visibleForTesting -class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent - with HasGameRef, ParentIsA, ZIndex { +class SpaceshipRampArrowSpriteComponent + extends SpriteGroupComponent + with + HasGameRef, + ZIndex, + FlameBlocListenable { /// {@macro spaceship_ramp_arrow_sprite_component} - SpaceshipRampArrowSpriteComponent({ - required int current, - }) : super( + SpaceshipRampArrowSpriteComponent() + : super( anchor: Anchor.center, position: Vector2(-3.9, -56.5), - current: current, ) { zIndex = ZIndexes.spaceshipRampArrow; } + @override + bool listenWhen( + SpaceshipRampState previousState, + SpaceshipRampState newState, + ) { + return previousState.lightState != newState.lightState; + } + + @override + void onNewState(SpaceshipRampState state) { + current = state.lightState; + } + @override Future onLoad() async { await super.onLoad(); - parent.bloc.stream.listen((state) { - current = state.hits % SpaceshipRampArrowSpriteState.values.length; - }); - - final sprites = {}; + final sprites = {}; this.sprites = sprites; - for (final spriteState in SpaceshipRampArrowSpriteState.values) { - sprites[spriteState.index] = Sprite( + for (final spriteState in ArrowLightState.values) { + sprites[spriteState] = Sprite( gameRef.images.fromCache(spriteState.path), ); } - current = 0; + current = ArrowLightState.inactive; size = sprites[current]!.originalSize / 10; } } -/// Indicates the state of the arrow on the [SpaceshipRamp]. -@visibleForTesting -enum SpaceshipRampArrowSpriteState { - /// Arrow with no dashes lit up. - inactive, - - /// Arrow with 1 light lit up. - active1, - - /// Arrow with 2 lights lit up. - active2, - - /// Arrow with 3 lights lit up. - active3, - - /// Arrow with 4 lights lit up. - active4, - - /// Arrow with all 5 lights lit up. - active5, -} - -extension on SpaceshipRampArrowSpriteState { +extension on ArrowLightState { String get path { switch (this) { - case SpaceshipRampArrowSpriteState.inactive: + case ArrowLightState.inactive: return Assets.images.android.ramp.arrow.inactive.keyName; - case SpaceshipRampArrowSpriteState.active1: + case ArrowLightState.active1: return Assets.images.android.ramp.arrow.active1.keyName; - case SpaceshipRampArrowSpriteState.active2: + case ArrowLightState.active2: return Assets.images.android.ramp.arrow.active2.keyName; - case SpaceshipRampArrowSpriteState.active3: + case ArrowLightState.active3: return Assets.images.android.ramp.arrow.active3.keyName; - case SpaceshipRampArrowSpriteState.active4: + case ArrowLightState.active4: return Assets.images.android.ramp.arrow.active4.keyName; - case SpaceshipRampArrowSpriteState.active5: + case ArrowLightState.active5: return Assets.images.android.ramp.arrow.active5.keyName; } } } class SpaceshipRampBoardOpening extends BodyComponent - with Layered, ZIndex, InitialPosition, ParentIsA { + with Layered, ZIndex, InitialPosition { SpaceshipRampBoardOpening() : super( renderBody: false, diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart index 1027002a..b19aef56 100644 --- a/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart @@ -44,9 +44,9 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { await super.onLoad(); camera.followVector2(Vector2(-12, -50)); - await add( - _spaceshipRamp = SpaceshipRamp(), - ); + + _spaceshipRamp = SpaceshipRamp(); + await add(_spaceshipRamp); await traceAllBodies(); } @@ -57,7 +57,9 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { ) { if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.space) { - _spaceshipRamp.bloc.onAscendingBallEntered(); + _spaceshipRamp + .readBloc() + .onProgressed(); return KeyEventResult.handled; } diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart index d1f03ce7..9051059c 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart @@ -1,14 +1,47 @@ // ignore_for_file: cascade_invocations import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; - -import '../../../../helpers/helpers.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + 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, + ]); + } + + Future pump( + SpaceshipRamp children, { + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: bloc, + children: [ + ZCanvasComponent(children: [children]), + ], + ), + ); + } +} class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} @@ -20,20 +53,8 @@ class _MockContact extends Mock implements Contact {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - 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, - ]; - - final flameTester = FlameTester(() => TestGame(assets)); + + final flameTester = FlameTester(_TestGame.new); group( 'RampBallAscendingContactBehavior', @@ -67,16 +88,18 @@ void main() { initialState: const SpaceshipRampState.initial(), ); - final parent = SpaceshipRampBoardOpening.test(); + final opening = SpaceshipRampBoardOpening.test(); final spaceshipRamp = SpaceshipRamp.test( - bloc: bloc, + children: [opening], ); when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); - await spaceshipRamp.add(parent); - await game.ensureAddAll([spaceshipRamp, ball]); - await parent.add(behavior); + await game.pump( + spaceshipRamp, + bloc: bloc, + ); + await opening.ensureAdd(behavior); behavior.beginContact(ball, _MockContact()); @@ -95,16 +118,18 @@ void main() { initialState: const SpaceshipRampState.initial(), ); - final parent = SpaceshipRampBoardOpening.test(); + final opening = SpaceshipRampBoardOpening.test(); final spaceshipRamp = SpaceshipRamp.test( - bloc: bloc, + children: [opening], ); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - await spaceshipRamp.add(parent); - await game.ensureAddAll([spaceshipRamp, ball]); - await parent.add(behavior); + await game.pump( + spaceshipRamp, + bloc: bloc, + ); + await opening.ensureAdd(behavior); behavior.beginContact(ball, _MockContact()); diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart index b7e899fe..1e951ad4 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart @@ -15,9 +15,70 @@ void main() { ..onAscendingBallEntered() ..onAscendingBallEntered(), expect: () => [ - SpaceshipRampState(hits: 1), - SpaceshipRampState(hits: 2), - SpaceshipRampState(hits: 3), + isA().having((state) => state.hits, 'hits', 1), + isA().having((state) => state.hits, 'hits', 2), + isA().having((state) => state.hits, 'hits', 3), + ], + ); + }); + + group('onProgressed', () { + blocTest( + 'emits next arrow lightState', + build: SpaceshipRampCubit.new, + act: (bloc) => bloc + ..onProgressed() + ..onProgressed() + ..onProgressed() + ..onProgressed() + ..onProgressed() + ..onProgressed(), + expect: () => [ + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active1, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active2, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active3, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active4, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.active5, + ), + isA().having( + (state) => state.lightState, + 'lightState', + ArrowLightState.inactive, + ), + ], + ); + }); + + group('onReset', () { + blocTest( + 'emits initial state', + build: SpaceshipRampCubit.new, + seed: () => SpaceshipRampState( + hits: 100, + lightState: ArrowLightState.active3, + ), + act: (bloc) => bloc.onReset(), + expect: () => [ + SpaceshipRampState.initial(), ], ); }); diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart index 536f4e8e..04142506 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart @@ -7,9 +7,15 @@ void main() { group('SpaceshipRampState', () { test('supports value equality', () { expect( - SpaceshipRampState(hits: 0), + SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ), equals( - SpaceshipRampState(hits: 0), + SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ), ), ); }); @@ -17,19 +23,41 @@ void main() { group('constructor', () { test('can be instantiated', () { expect( - SpaceshipRampState(hits: 0), + SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ), isNotNull, ); }); + + test( + 'throws AssertionError ' + 'when hits is negative', + () { + expect( + () => SpaceshipRampState( + hits: -1, + lightState: ArrowLightState.inactive, + ), + throwsAssertionError, + ); + }, + ); }); test( - 'throws AssertionError ' - 'when hits is negative', + 'arrowFullyLit returns true when lightState is last one', () { expect( - () => SpaceshipRampState(hits: -1), - throwsAssertionError, + SpaceshipRampState.initial().arrowFullyLit, + isFalse, + ); + expect( + SpaceshipRampState.initial() + .copyWith(lightState: ArrowLightState.active5) + .arrowFullyLit, + isTrue, ); }, ); @@ -39,7 +67,10 @@ void main() { 'throws AssertionError ' 'when hits is decreased', () { - const rampState = SpaceshipRampState(hits: 0); + const rampState = SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ); expect( () => rampState.copyWith(hits: rampState.hits - 1), throwsAssertionError, @@ -51,7 +82,10 @@ void main() { 'copies correctly ' 'when no argument specified', () { - const rampState = SpaceshipRampState(hits: 0); + const rampState = SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ); expect( rampState.copyWith(), equals(rampState), @@ -63,12 +97,21 @@ void main() { 'copies correctly ' 'when all arguments specified', () { - const rampState = SpaceshipRampState(hits: 0); - final otherRampState = SpaceshipRampState(hits: rampState.hits + 1); + const rampState = SpaceshipRampState( + hits: 0, + lightState: ArrowLightState.inactive, + ); + final otherRampState = SpaceshipRampState( + hits: rampState.hits + 1, + lightState: ArrowLightState.active1, + ); expect(rampState, isNot(equals(otherRampState))); expect( - rampState.copyWith(hits: rampState.hits + 1), + rampState.copyWith( + hits: otherRampState.hits, + lightState: otherRampState.lightState, + ), equals(otherRampState), ); }, diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart index 1c9c968d..e1dafc59 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart @@ -1,7 +1,10 @@ -// ignore_for_file: cascade_invocations +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -10,7 +13,38 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import '../../../helpers/helpers.dart'; +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + 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, + ]); + } + + Future pump( + SpaceshipRamp children, { + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameBlocProvider.value( + value: bloc, + children: [ + ZCanvasComponent(children: [children]), + ], + ), + ); + } +} class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} @@ -22,264 +56,74 @@ class _MockManifold extends Mock implements Manifold {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - 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, - ]; - final flameTester = FlameTester(() => TestGame(assets)); + + final flameTester = FlameTester(_TestGame.new); group('SpaceshipRamp', () { flameTester.test( 'loads correctly', (game) async { - final spaceshipRamp = SpaceshipRamp(); - await game.ensureAdd(spaceshipRamp); - expect(game.children, contains(spaceshipRamp)); + final bloc = _MockSpaceshipRampCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SpaceshipRampState.initial(), + ); + final ramp = SpaceshipRamp.test(); + await game.pump(ramp, bloc: bloc); + expect(game.descendants(), contains(ramp)); }, ); - group('renders correctly', () { - const goldenFilePath = '../golden/spaceship_ramp/'; - final centerForSpaceshipRamp = Vector2(-13, -55); - - flameTester.testGameWidget( - 'inactive sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; - expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.inactive, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}inactive.png'), - ); - }, - ); - - flameTester.testGameWidget( - 'active1 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc.onAscendingBallEntered(); - - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; - expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active1, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active1.png'), - ); - }, - ); - - flameTester.testGameWidget( - 'active2 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc - ..onAscendingBallEntered() - ..onAscendingBallEntered(); - - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; - expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active2, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active2.png'), - ); - }, - ); + group('adds', () { + flameTester.test('a FlameBlocProvider', (game) async { + final ramp = SpaceshipRamp(); + await game.ensureAdd(ramp); + expect( + ramp.children + .whereType< + FlameBlocProvider>() + .single, + isNotNull, + ); + }); - flameTester.testGameWidget( - 'active3 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); + flameTester.test( + 'a SpaceshipRampBoardOpening', + (game) async { final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered(); + await game.ensureAdd(ramp); - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active3, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active3.png'), + game.descendants().whereType().length, + equals(1), ); }, ); - flameTester.testGameWidget( - 'active4 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); + flameTester.test( + 'a SpaceshipRampArrowSpriteComponent', + (game) async { final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered(); - - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; - expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active4, - ); + await game.ensureAdd(ramp); - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active4.png'), - ); - }, - ); - - flameTester.testGameWidget( - 'active5 sprite', - setUp: (game, tester) async { - await game.images.loadAll(assets); - final ramp = SpaceshipRamp(); - final canvas = ZCanvasComponent(children: [ramp]); - await game.ensureAdd(canvas); - - ramp.bloc - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered() - ..onAscendingBallEntered(); - - await game.ready(); - await tester.pump(); - - final index = ramp.children - .whereType() - .first - .current; expect( - SpaceshipRampArrowSpriteState.values[index!], - SpaceshipRampArrowSpriteState.active5, - ); - - game.camera.followVector2(centerForSpaceshipRamp); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('${goldenFilePath}active5.png'), + game + .descendants() + .whereType() + .length, + equals(1), ); }, ); - }); - - flameTester.test('closes bloc when removed', (game) async { - final bloc = _MockSpaceshipRampCubit(); - whenListen( - bloc, - const Stream.empty(), - initialState: const SpaceshipRampState.initial(), - ); - when(bloc.close).thenAnswer((_) async {}); - - final ramp = SpaceshipRamp.test( - bloc: bloc, - ); - - await game.ensureAdd(ramp); - game.remove(ramp); - await game.ready(); - verify(bloc.close).called(1); - }); - - group('adds', () { flameTester.test('new children', (game) async { final component = Component(); final ramp = SpaceshipRamp(children: [component]); await game.ensureAdd(ramp); - expect(ramp.children, contains(component)); + + expect(ramp.descendants(), contains(component)); }); }); }); @@ -332,17 +176,19 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit()); final component = SpaceshipRampBoardOpening(); - await game.ensureAdd(parent); + final parent = SpaceshipRamp.test(); + await game.pump(parent, bloc: _MockSpaceshipRampCubit()); + await parent.ensureAdd(component); expect(parent.children, contains(component)); }); flameTester.test('adds a RampBallAscendingContactBehavior', (game) async { - final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit()); final component = SpaceshipRampBoardOpening(); - await game.ensureAdd(parent); + final parent = SpaceshipRamp.test(); + await game.pump(parent, bloc: _MockSpaceshipRampCubit()); + await parent.ensureAdd(component); expect( component.children.whereType().length, @@ -350,4 +196,36 @@ void main() { ); }); }); + + group('SpaceshipRampArrowSpriteComponent', () { + flameTester.test( + 'changes current state ' + 'when SpaceshipRampState changes lightState', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + final arrow = SpaceshipRampArrowSpriteComponent(); + final ramp = SpaceshipRamp.test(children: [arrow]); + await game.pump( + ramp, + bloc: bloc, + ); + + expect(arrow.current, ArrowLightState.inactive); + + streamController + .add(state.copyWith(lightState: ArrowLightState.active1)); + + await game.ready(); + + expect(arrow.current, ArrowLightState.active1); + }, + ); + }); } diff --git a/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart index cb6c2784..dc0d0e28 100644 --- a/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart +++ b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; @@ -36,14 +37,22 @@ class _TestGame extends Forge2DGame { } Future pump( - SpaceshipRamp child, { + List children, { + required SpaceshipRampCubit bloc, required GameBloc gameBloc, }) async { await ensureAdd( - FlameBlocProvider.value( - value: gameBloc, + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], children: [ - ZCanvasComponent(children: [child]), + ZCanvasComponent(children: children), ], ), ); @@ -54,20 +63,17 @@ class _MockGameBloc extends Mock implements GameBloc {} class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} -class _MockStreamSubscription extends Mock - implements StreamSubscription {} - void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('RampBonusBehavior', () { - const shotPoints = Points.oneMillion; + late GameBloc gameBloc; - late GameBloc gameBloc; + setUp(() { + gameBloc = _MockGameBloc(); + }); - setUp(() { - gameBloc = _MockGameBloc(); - }); + group('RampBonusBehavior', () { + const shotPoints = Points.oneMillion; final flameTester = FlameTester(_TestGame.new); @@ -75,22 +81,23 @@ void main() { 'when hits are multiples of 10 times adds a ScoringBehavior', (game) async { final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); final streamController = StreamController(); whenListen( bloc, streamController.stream, - initialState: SpaceshipRampState(hits: 9), + initialState: state, ); - final behavior = RampBonusBehavior(points: shotPoints); - final parent = SpaceshipRamp.test(bloc: bloc); + final behavior = RampBonusBehavior(points: shotPoints); + final parent = SpaceshipRamp.test(children: [behavior]); await game.pump( - parent, + [parent], + bloc: bloc, gameBloc: gameBloc, ); - await parent.ensureAdd(behavior); - streamController.add(SpaceshipRampState(hits: 10)); + streamController.add(state.copyWith(hits: 10)); final scores = game.descendants().whereType(); await game.ready(); @@ -103,22 +110,23 @@ void main() { "when hits are not multiple of 10 times doesn't add any ScoringBehavior", (game) async { final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); final streamController = StreamController(); whenListen( bloc, streamController.stream, - initialState: SpaceshipRampState.initial(), + initialState: state, ); - final behavior = RampBonusBehavior(points: shotPoints); - final parent = SpaceshipRamp.test(bloc: bloc); + final behavior = RampBonusBehavior(points: shotPoints); + final parent = SpaceshipRamp.test(children: [behavior]); await game.pump( - parent, + [parent], + bloc: bloc, gameBloc: gameBloc, ); - await parent.ensureAdd(behavior); - streamController.add(SpaceshipRampState(hits: 1)); + streamController.add(state.copyWith(hits: 9)); final scores = game.descendants().whereType(); await game.ready(); @@ -126,38 +134,5 @@ void main() { expect(scores.length, 0); }, ); - - flameTester.test( - 'closes subscription when removed', - (game) async { - final bloc = _MockSpaceshipRampCubit(); - whenListen( - bloc, - const Stream.empty(), - initialState: SpaceshipRampState.initial(), - ); - when(bloc.close).thenAnswer((_) async {}); - - final subscription = _MockStreamSubscription(); - when(subscription.cancel).thenAnswer((_) async {}); - - final behavior = RampBonusBehavior.test( - points: shotPoints, - subscription: subscription, - ); - final parent = SpaceshipRamp.test(bloc: bloc); - - await game.pump( - parent, - gameBloc: gameBloc, - ); - await parent.ensureAdd(behavior); - - parent.remove(behavior); - await game.ready(); - - verify(subscription.cancel).called(1); - }, - ); }); } diff --git a/test/game/components/android_acres/behaviors/ramp_multiplier_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_multiplier_behavior_test.dart new file mode 100644 index 00000000..5413c3d3 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_multiplier_behavior_test.dart @@ -0,0 +1,183 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + 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.score.fiveThousand.keyName, + ]); + } + + Future pump( + SpaceshipRamp child, { + required GameBloc gameBloc, + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], + children: [ + ZCanvasComponent(children: [child]), + ], + ), + ); + } +} + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _FakeGameEvent extends Fake implements GameEvent {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RampMultiplierBehavior', () { + late GameBloc gameBloc; + + setUp(() { + registerFallbackValue(_FakeGameEvent()); + gameBloc = _MockGameBloc(); + }); + + final flameTester = FlameTester(_TestGame.new); + + flameTester.test( + 'adds MultiplierIncreased ' + 'when hits are multiples of 5 times and multiplier is less than 6', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 5, + ), + ); + when(() => gameBloc.add(any())).thenAnswer((_) async {}); + + final behavior = RampMultiplierBehavior(); + final parent = SpaceshipRamp.test(children: [behavior]); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 5)); + await Future.delayed(Duration.zero); + + verify(() => gameBloc.add(const MultiplierIncreased())).called(1); + }, + ); + + flameTester.test( + "doesn't add MultiplierIncreased " + 'when hits are multiples of 5 times but multiplier is 6', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 6, + ), + ); + + final behavior = RampMultiplierBehavior(); + final parent = SpaceshipRamp.test(children: [behavior]); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 5)); + await Future.delayed(Duration.zero); + + verifyNever(() => gameBloc.add(const MultiplierIncreased())); + }, + ); + + flameTester.test( + "doesn't add MultiplierIncreased " + "when hits aren't multiples of 5 times", + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 5, + ), + ); + + final behavior = RampMultiplierBehavior(); + final parent = SpaceshipRamp.test(children: [behavior]); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 1)); + + await game.ready(); + + verifyNever(() => gameBloc.add(const MultiplierIncreased())); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart new file mode 100644 index 00000000..29e9f452 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart @@ -0,0 +1,309 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + 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, + ]); + } + + Future pump( + SpaceshipRamp child, { + required GameBloc gameBloc, + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], + children: [ + ZCanvasComponent(children: [child]), + ], + ), + ); + } +} + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +class _FakeGameEvent extends Fake implements GameEvent {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RampProgressBehavior', () { + late GameBloc gameBloc; + + setUp(() { + registerFallbackValue(_FakeGameEvent()); + gameBloc = _MockGameBloc(); + }); + + final flameTester = FlameTester(_TestGame.new); + + flameTester.test( + 'adds onProgressed ' + 'when hits and multiplier are less than 6', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 1, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 5)); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(1); + }, + ); + + flameTester.test( + 'adds onProgressed ' + 'when hits and multiplier are 6 but arrow is not fully lit', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 6, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(hits: 5)); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(1); + }, + ); + + flameTester.test( + "doesn't add onProgressed " + 'when hits and multiplier are 6 and arrow is fully lit', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 6, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add( + state.copyWith( + hits: 5, + lightState: ArrowLightState.active5, + ), + ); + await Future.delayed(Duration.zero); + + verifyNever(bloc.onProgressed); + }, + ); + + flameTester.test( + 'adds onProgressed to dim arrow ' + 'when arrow is fully lit after hit and multiplier is less than 6', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 5, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add( + state.copyWith( + hits: 5, + lightState: ArrowLightState.active5, + ), + ); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(2); + }, + ); + + flameTester.test( + "doesn't add onProgressed to dim arrow " + 'when arrow is not fully lit after hit', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 5, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add( + state.copyWith( + hits: 4, + lightState: ArrowLightState.active4, + ), + ); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(1); + }, + ); + + flameTester.test( + "doesn't add onProgressed to dim arrow " + 'when multiplier is 6 after hit', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: state, + ); + when(() => gameBloc.state).thenReturn( + GameState.initial().copyWith( + multiplier: 6, + ), + ); + + final behavior = RampProgressBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add( + state.copyWith(hits: 4), + ); + await Future.delayed(Duration.zero); + + verify(bloc.onProgressed).called(1); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_reset_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_reset_behavior_test.dart new file mode 100644 index 00000000..d141c62a --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_reset_behavior_test.dart @@ -0,0 +1,135 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + 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.score.fiveThousand.keyName, + ]); + } + + Future pump( + SpaceshipRamp child, { + required GameBloc gameBloc, + required SpaceshipRampCubit bloc, + }) async { + await ensureAdd( + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], + children: [ + ZCanvasComponent(children: [child]), + ], + ), + ); + } +} + +class _MockGameBloc extends Mock implements GameBloc {} + +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('RampResetBehavior', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + }); + + final flameTester = FlameTester(_TestGame.new); + + flameTester.test( + 'calls onReset when round lost', + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = GameState.initial(); + final streamController = StreamController(); + whenListen( + gameBloc, + streamController.stream, + initialState: state, + ); + final behavior = RampResetBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController.add(state.copyWith(rounds: state.rounds - 1)); + await Future.delayed(Duration.zero); + + verify(bloc.onReset).called(1); + }, + ); + + flameTester.test( + "doesn't call onReset when round stays the same", + (game) async { + final bloc = _MockSpaceshipRampCubit(); + final state = GameState.initial(); + final streamController = StreamController(); + whenListen( + gameBloc, + streamController.stream, + initialState: state, + ); + final behavior = RampResetBehavior(); + final parent = SpaceshipRamp.test( + children: [behavior], + ); + + await game.pump( + parent, + gameBloc: gameBloc, + bloc: bloc, + ); + + streamController + .add(state.copyWith(roundScore: state.roundScore + 100)); + await Future.delayed(Duration.zero); + + verifyNever(bloc.onReset); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart index ae072ea4..d5a5ecd6 100644 --- a/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart +++ b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; @@ -36,14 +37,22 @@ class _TestGame extends Forge2DGame { } Future pump( - SpaceshipRamp child, { + List children, { + required SpaceshipRampCubit bloc, required GameBloc gameBloc, }) async { await ensureAdd( - FlameBlocProvider.value( - value: gameBloc, + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: bloc, + ), + ], children: [ - ZCanvasComponent(children: [child]), + ZCanvasComponent(children: children), ], ), ); @@ -54,120 +63,47 @@ class _MockGameBloc extends Mock implements GameBloc {} class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} -class _MockStreamSubscription extends Mock - implements StreamSubscription {} - void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('RampShotBehavior', () { - const shotPoints = Points.fiveThousand; + late GameBloc gameBloc; - late GameBloc gameBloc; + setUp(() { + gameBloc = _MockGameBloc(); + }); - setUp(() { - gameBloc = _MockGameBloc(); - }); + group('RampShotBehavior', () { + const shotPoints = Points.fiveThousand; - final flameBlocTester = FlameTester(_TestGame.new); + final flameTester = FlameTester(_TestGame.new); - flameBlocTester.test( - 'when hits are not multiple of 10 times ' - 'increases multiplier and adds a ScoringBehavior', + flameTester.test( + 'adds a ScoringBehavior when hit', (game) async { final bloc = _MockSpaceshipRampCubit(); + final state = SpaceshipRampState.initial(); final streamController = StreamController(); whenListen( bloc, streamController.stream, - initialState: SpaceshipRampState.initial(), + initialState: state, ); - final behavior = RampShotBehavior(points: shotPoints); - final parent = SpaceshipRamp.test(bloc: bloc); + final behavior = RampShotBehavior(points: shotPoints); + final parent = SpaceshipRamp.test(children: [behavior]); await game.pump( - parent, + [parent], + bloc: bloc, gameBloc: gameBloc, ); - await parent.ensureAdd(behavior); - streamController.add(SpaceshipRampState(hits: 1)); + streamController.add(state.copyWith(hits: state.hits + 1)); final scores = game.descendants().whereType(); await game.ready(); - verify(() => gameBloc.add(MultiplierIncreased())).called(1); expect(scores.length, 1); }, ); - - flameBlocTester.test( - 'when hits multiple of 10 times ' - "doesn't increase multiplier, neither ScoringBehavior", - (game) async { - final bloc = _MockSpaceshipRampCubit(); - final streamController = StreamController(); - whenListen( - bloc, - streamController.stream, - initialState: SpaceshipRampState(hits: 9), - ); - final behavior = RampShotBehavior( - points: shotPoints, - ); - final parent = SpaceshipRamp.test( - bloc: bloc, - ); - - await game.pump( - parent, - gameBloc: gameBloc, - ); - await parent.ensureAdd(behavior); - - streamController.add(SpaceshipRampState(hits: 10)); - - final scores = game.children.whereType(); - await game.ready(); - - verifyNever(() => gameBloc.add(MultiplierIncreased())); - expect(scores.length, 0); - }, - ); - - flameBlocTester.test( - 'closes subscription when removed', - (game) async { - final bloc = _MockSpaceshipRampCubit(); - whenListen( - bloc, - const Stream.empty(), - initialState: SpaceshipRampState.initial(), - ); - when(bloc.close).thenAnswer((_) async {}); - - final subscription = _MockStreamSubscription(); - when(subscription.cancel).thenAnswer((_) async {}); - - final behavior = RampShotBehavior.test( - points: shotPoints, - subscription: subscription, - ); - final parent = SpaceshipRamp.test( - bloc: bloc, - ); - - await game.pump( - parent, - gameBloc: gameBloc, - ); - await parent.ensureAdd(behavior); - - parent.remove(behavior); - await game.ready(); - - verify(subscription.cancel).called(1); - }, - ); }); }