From 3b5af49756ed3245b2b1564ecd28ace3f6779c49 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Fri, 6 May 2022 17:24:00 -0500 Subject: [PATCH] refactor: `SparkyComputer` behaviors and removed `ControlledBall` (#365) * refactor: sparky behaviors and remove controlled ball * fix: test typo * fix: PR suggestions --- .../behaviors/ball_spawning_behavior.dart | 6 +- lib/game/components/components.dart | 3 +- lib/game/components/controlled_ball.dart | 66 -------- .../flutter_forest_bonus_behavior.dart | 9 +- .../sparky_scorch/behaviors/behaviors.dart | 1 + .../sparky_computer_bonus_behavior.dart | 28 ++++ .../{ => sparky_scorch}/sparky_scorch.dart | 59 ++------ lib/game/pinball_game.dart | 7 +- .../lib/src/components/components.dart | 2 +- .../sparky_computer/behaviors/behaviors.dart | 1 + ...computer_sensor_ball_contact_behavior.dart | 35 +++++ .../cubit/sparky_computer_cubit.dart | 17 +++ .../cubit/sparky_computer_state.dart | 8 + .../sparky_computer.dart | 67 +++++++-- ...ter_sensor_ball_contact_behavior_test.dart | 141 ++++++++++++++++++ .../cubit/sparky_computer_cubit_test.dart | 24 +++ .../sparky_computer/sparky_computer_test.dart | 93 ++++++++++++ .../src/components/sparky_computer_test.dart | 45 ------ .../game/components/controlled_ball_test.dart | 71 --------- .../sparky_computer_bonus_behavior_test.dart | 86 +++++++++++ .../sparky_scorch_test.dart | 86 +++++------ test/game/pinball_game_test.dart | 11 +- 22 files changed, 559 insertions(+), 307 deletions(-) delete mode 100644 lib/game/components/controlled_ball.dart create mode 100644 lib/game/components/sparky_scorch/behaviors/behaviors.dart create mode 100644 lib/game/components/sparky_scorch/behaviors/sparky_computer_bonus_behavior.dart rename lib/game/components/{ => sparky_scorch}/sparky_scorch.dart (50%) create mode 100644 packages/pinball_components/lib/src/components/sparky_computer/behaviors/behaviors.dart create mode 100644 packages/pinball_components/lib/src/components/sparky_computer/behaviors/sparky_computer_sensor_ball_contact_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/sparky_computer/cubit/sparky_computer_cubit.dart create mode 100644 packages/pinball_components/lib/src/components/sparky_computer/cubit/sparky_computer_state.dart rename packages/pinball_components/lib/src/components/{ => sparky_computer}/sparky_computer.dart (62%) create mode 100644 packages/pinball_components/test/src/components/sparky_computer/behaviors/sparky_computer_sensor_ball_contact_behavior_test.dart create mode 100644 packages/pinball_components/test/src/components/sparky_computer/cubit/sparky_computer_cubit_test.dart create mode 100644 packages/pinball_components/test/src/components/sparky_computer/sparky_computer_test.dart delete mode 100644 packages/pinball_components/test/src/components/sparky_computer_test.dart delete mode 100644 test/game/components/controlled_ball_test.dart create mode 100644 test/game/components/sparky_scorch/behaviors/sparky_computer_bonus_behavior_test.dart rename test/game/components/{ => sparky_scorch}/sparky_scorch_test.dart (58%) diff --git a/lib/game/behaviors/ball_spawning_behavior.dart b/lib/game/behaviors/ball_spawning_behavior.dart index c074fe52..75656d8f 100644 --- a/lib/game/behaviors/ball_spawning_behavior.dart +++ b/lib/game/behaviors/ball_spawning_behavior.dart @@ -24,11 +24,13 @@ class BallSpawningBehavior extends Component final plunger = gameRef.descendants().whereType().single; final canvas = gameRef.descendants().whereType().single; final characterTheme = readProvider(); - final ball = ControlledBall.launch(characterTheme: characterTheme) + final ball = Ball(assetPath: characterTheme.ball.keyName) ..initialPosition = Vector2( plunger.body.position.x, plunger.body.position.y - Ball.size.y, - ); + ) + ..layer = Layer.launcher + ..zIndex = ZIndexes.ballOnLaunchRamp; canvas.add(ball); } diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index b96b6a65..08dc5cb0 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,7 +1,6 @@ export 'android_acres/android_acres.dart'; export 'backbox/backbox.dart'; export 'bottom_group.dart'; -export 'controlled_ball.dart'; export 'controlled_flipper.dart'; export 'controlled_plunger.dart'; export 'dino_desert/dino_desert.dart'; @@ -12,4 +11,4 @@ export 'google_word/google_word.dart'; export 'launcher.dart'; export 'multiballs/multiballs.dart'; export 'multipliers/multipliers.dart'; -export 'sparky_scorch.dart'; +export 'sparky_scorch/sparky_scorch.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart deleted file mode 100644 index 241465dd..00000000 --- a/lib/game/components/controlled_ball.dart +++ /dev/null @@ -1,66 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -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'; -import 'package:pinball_theme/pinball_theme.dart'; - -/// {@template controlled_ball} -/// A [Ball] with a [BallController] attached. -/// -/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is -/// not over, a new [Ball] will be spawned. -/// {@endtemplate} -class ControlledBall extends Ball with Controls { - /// A [Ball] that launches from the [Plunger]. - ControlledBall.launch({ - required CharacterTheme characterTheme, - }) : super(assetPath: characterTheme.ball.keyName) { - controller = BallController(this); - layer = Layer.launcher; - zIndex = ZIndexes.ballOnLaunchRamp; - } - - /// {@macro controlled_ball} - ControlledBall.bonus({ - required CharacterTheme characterTheme, - }) : super(assetPath: characterTheme.ball.keyName) { - controller = BallController(this); - zIndex = ZIndexes.ballOnBoard; - } - - /// [Ball] used in [DebugPinballGame]. - ControlledBall.debug() : super() { - controller = BallController(this); - zIndex = ZIndexes.ballOnBoard; - } -} - -/// {@template ball_controller} -/// Controller attached to a [Ball] that handles its game related logic. -/// {@endtemplate} -class BallController extends ComponentController - with FlameBlocReader { - /// {@macro ball_controller} - BallController(Ball ball) : super(ball); - - /// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge - /// sequence runs, then boosts the ball out of the computer. - Future turboCharge() async { - bloc.add(const SparkyTurboChargeActivated()); - - component.stop(); - // TODO(alestiago): Refactor this hard coded duration once the following is - // merged: - // https://github.com/flame-engine/flame/pull/1564 - await Future.delayed( - const Duration(milliseconds: 2583), - ); - component.resume(); - await component.add( - BallTurboChargingBehavior(impulse: Vector2(40, 110)), - ); - } -} diff --git a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart index a4931f90..55902eb7 100644 --- a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -41,10 +41,13 @@ class FlutterForestBonusBehavior extends Component if (signpost.bloc.isFullyProgressed()) { bloc.add(const BonusActivated(GameBonus.dashNest)); + final characterTheme = readProvider(); canvas.add( - ControlledBall.bonus( - characterTheme: readProvider(), - )..initialPosition = Vector2(29.2, -24.5), + Ball( + assetPath: characterTheme.ball.keyName, + ) + ..initialPosition = Vector2(29.2, -24.5) + ..zIndex = ZIndexes.ballOnBoard, ); animatronic.playing = true; signpost.bloc.onProgressed(); diff --git a/lib/game/components/sparky_scorch/behaviors/behaviors.dart b/lib/game/components/sparky_scorch/behaviors/behaviors.dart new file mode 100644 index 00000000..3281bb69 --- /dev/null +++ b/lib/game/components/sparky_scorch/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'sparky_computer_bonus_behavior.dart'; diff --git a/lib/game/components/sparky_scorch/behaviors/sparky_computer_bonus_behavior.dart b/lib/game/components/sparky_scorch/behaviors/sparky_computer_bonus_behavior.dart new file mode 100644 index 00000000..15deab29 --- /dev/null +++ b/lib/game/components/sparky_scorch/behaviors/sparky_computer_bonus_behavior.dart @@ -0,0 +1,28 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Adds a [GameBonus.sparkyTurboCharge] when a [Ball] enters the +/// [SparkyComputer]. +class SparkyComputerBonusBehavior extends Component + with ParentIsA, FlameBlocReader { + @override + void onMount() { + super.onMount(); + final sparkyComputer = parent.firstChild()!; + final animatronic = parent.firstChild()!; + + // TODO(alestiago): Refactor subscription management once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1538 + sparkyComputer.bloc.stream.listen((state) async { + final listenWhen = state == SparkyComputerState.withBall; + if (!listenWhen) return; + + bloc.add(const BonusActivated(GameBonus.sparkyTurboCharge)); + animatronic.playing = true; + }); + } +} diff --git a/lib/game/components/sparky_scorch.dart b/lib/game/components/sparky_scorch/sparky_scorch.dart similarity index 50% rename from lib/game/components/sparky_scorch.dart rename to lib/game/components/sparky_scorch/sparky_scorch.dart index b820e89d..da624361 100644 --- a/lib/game/components/sparky_scorch.dart +++ b/lib/game/components/sparky_scorch/sparky_scorch.dart @@ -1,9 +1,9 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; -import 'package:pinball/game/components/components.dart'; +import 'package:pinball/game/components/sparky_scorch/behaviors/behaviors.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template sparky_scorch} @@ -33,51 +33,20 @@ class SparkyScorch extends Component { BumperNoiseBehavior(), ], )..initialPosition = Vector2(-3.3, -52.55), - SparkyComputerSensor()..initialPosition = Vector2(-13.2, -49.9), SparkyAnimatronic()..position = Vector2(-14, -58.2), - SparkyComputer(), - ], - ); -} - -/// {@template sparky_computer_sensor} -/// Small sensor body used to detect when a ball has entered the -/// [SparkyComputer]. -/// {@endtemplate} -class SparkyComputerSensor extends BodyComponent - with InitialPosition, ContactCallbacks { - /// {@macro sparky_computer_sensor} - SparkyComputerSensor() - : super( - renderBody: false, - children: [ - ScoringContactBehavior(points: Points.twentyThousand), + SparkyComputer( + children: [ + ScoringContactBehavior(points: Points.twoHundredThousand) + ..applyTo(['turbo_charge_sensor']), + ], + ), + SparkyComputerBonusBehavior(), ], ); - @override - Body createBody() { - final shape = PolygonShape() - ..setAsBox( - 1, - 0.1, - Vector2.zero(), - -0.18, - ); - final fixtureDef = FixtureDef(shape, isSensor: true); - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is! ControlledBall) return; - - other.controller.turboCharge(); - gameRef.firstChild()?.playing = true; - } + /// Creates [SparkyScorch] without any children. + /// + /// This can be used for testing [SparkyScorch]'s behaviors in isolation. + @visibleForTesting + SparkyScorch.test(); } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 8f1628be..6818f566 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -191,8 +191,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { if (info.raw.kind == PointerDeviceKind.mouse) { final canvas = descendants().whereType().single; - final ball = ControlledBall.debug() - ..initialPosition = info.eventPosition.game; + final ball = Ball()..initialPosition = info.eventPosition.game; canvas.add(ball); } } @@ -219,7 +218,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { void _turboChargeBall(Vector2 line) { final canvas = descendants().whereType().single; - final ball = ControlledBall.debug()..initialPosition = lineStart!; + final ball = Ball()..initialPosition = lineStart!; final impulse = line * -1 * 10; ball.add(BallTurboChargingBehavior(impulse: impulse)); canvas.add(ball); @@ -265,7 +264,7 @@ class _DebugInformation extends Component with HasGameRef { void render(Canvas canvas) { final debugText = [ 'FPS: ${gameRef.fps().toStringAsFixed(1)}', - 'BALLS: ${gameRef.descendants().whereType().length}', + 'BALLS: ${gameRef.descendants().whereType().length}', ].join(' | '); final height = _debugTextPaint.measureTextHeight(debugText); diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 55fe6bb5..54345772 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -36,5 +36,5 @@ export 'spaceship_rail.dart'; export 'spaceship_ramp/spaceship_ramp.dart'; export 'sparky_animatronic.dart'; export 'sparky_bumper/sparky_bumper.dart'; -export 'sparky_computer.dart'; +export 'sparky_computer/sparky_computer.dart'; export 'z_indexes.dart'; diff --git a/packages/pinball_components/lib/src/components/sparky_computer/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/sparky_computer/behaviors/behaviors.dart new file mode 100644 index 00000000..7befc568 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_computer/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'sparky_computer_sensor_ball_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/sparky_computer/behaviors/sparky_computer_sensor_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/sparky_computer/behaviors/sparky_computer_sensor_ball_contact_behavior.dart new file mode 100644 index 00000000..8e83f61f --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_computer/behaviors/sparky_computer_sensor_ball_contact_behavior.dart @@ -0,0 +1,35 @@ +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 sparky_computer_sensor_ball_contact_behavior} +/// When a [Ball] enters the [SparkyComputer] it is stopped for a period of time +/// before a [BallTurboChargingBehavior] is applied to it. +/// {@endtemplate} +class SparkyComputerSensorBallContactBehavior + extends ContactBehavior { + @override + Future beginContact(Object other, Contact contact) async { + super.beginContact(other, contact); + if (other is! Ball) return; + + other.stop(); + parent.bloc.onBallEntered(); + await parent.add( + TimerComponent( + period: 1.5, + removeOnFinish: true, + onTick: () async { + other.resume(); + await other.add( + BallTurboChargingBehavior( + impulse: Vector2(40, 110), + ), + ); + parent.bloc.onBallTurboCharged(); + }, + ), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_computer/cubit/sparky_computer_cubit.dart b/packages/pinball_components/lib/src/components/sparky_computer/cubit/sparky_computer_cubit.dart new file mode 100644 index 00000000..e86defcd --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_computer/cubit/sparky_computer_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'sparky_computer_state.dart'; + +class SparkyComputerCubit extends Cubit { + SparkyComputerCubit() : super(SparkyComputerState.withoutBall); + + void onBallEntered() { + emit(SparkyComputerState.withBall); + } + + void onBallTurboCharged() { + emit(SparkyComputerState.withoutBall); + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_computer/cubit/sparky_computer_state.dart b/packages/pinball_components/lib/src/components/sparky_computer/cubit/sparky_computer_state.dart new file mode 100644 index 00000000..372f1d15 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_computer/cubit/sparky_computer_state.dart @@ -0,0 +1,8 @@ +// ignore_for_file: public_member_api_docs + +part of 'sparky_computer_cubit.dart'; + +enum SparkyComputerState { + withoutBall, + withBall, +} diff --git a/packages/pinball_components/lib/src/components/sparky_computer.dart b/packages/pinball_components/lib/src/components/sparky_computer/sparky_computer.dart similarity index 62% rename from packages/pinball_components/lib/src/components/sparky_computer.dart rename to packages/pinball_components/lib/src/components/sparky_computer/sparky_computer.dart index 8e2fc905..9025d69d 100644 --- a/packages/pinball_components/lib/src/components/sparky_computer.dart +++ b/packages/pinball_components/lib/src/components/sparky_computer/sparky_computer.dart @@ -2,31 +2,52 @@ 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/sparky_computer/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'cubit/sparky_computer_cubit.dart'; + /// {@template sparky_computer} /// A computer owned by Sparky. /// {@endtemplate} -class SparkyComputer extends Component { +class SparkyComputer extends BodyComponent { /// {@macro sparky_computer} - SparkyComputer() - : super( + SparkyComputer({Iterable? children}) + : bloc = SparkyComputerCubit(), + super( + renderBody: false, children: [ - _ComputerBase(), + SparkyComputerSensorBallContactBehavior() + ..applyTo(['turbo_charge_sensor']), + _ComputerBaseSpriteComponent(), _ComputerTopSpriteComponent(), _ComputerGlowSpriteComponent(), + ...?children, ], ); -} -class _ComputerBase extends BodyComponent with InitialPosition, ZIndex { - _ComputerBase() - : super( - renderBody: false, - children: [_ComputerBaseSpriteComponent()], - ) { - zIndex = ZIndexes.computerBase; + /// Creates a [SparkyComputer] without any children. + /// + /// This can be used for testing [SparkyComputer]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + SparkyComputer.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 + // ignore: public_member_api_docs + final SparkyComputerCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); } List _createFixtureDefs() { @@ -45,30 +66,44 @@ class _ComputerBase extends BodyComponent with InitialPosition, ZIndex { topEdge.vertex2, Vector2(-9.4, -47.1), ); + final turboChargeSensor = PolygonShape() + ..setAsBox( + 1, + 0.1, + Vector2(-13.2, -49.9), + -0.18, + ); return [ FixtureDef(leftEdge), FixtureDef(topEdge), FixtureDef(rightEdge), + FixtureDef( + turboChargeSensor, + isSensor: true, + userData: 'turbo_charge_sensor', + ), ]; } @override Body createBody() { - final bodyDef = BodyDef(position: initialPosition); - final body = world.createBody(bodyDef); + final body = world.createBody(BodyDef()); _createFixtureDefs().forEach(body.createFixture); return body; } } -class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef { +class _ComputerBaseSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { _ComputerBaseSpriteComponent() : super( anchor: Anchor.center, position: Vector2(-12.44, -48.15), - ); + ) { + zIndex = ZIndexes.computerBase; + } @override Future onLoad() async { diff --git a/packages/pinball_components/test/src/components/sparky_computer/behaviors/sparky_computer_sensor_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/sparky_computer/behaviors/sparky_computer_sensor_ball_contact_behavior_test.dart new file mode 100644 index 00000000..d90cc2c9 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_computer/behaviors/sparky_computer_sensor_ball_contact_behavior_test.dart @@ -0,0 +1,141 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/sparky_computer/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockSparkyComputerCubit extends Mock implements SparkyComputerCubit {} + +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'SparkyComputerSensorBallContactBehavior', + () { + test('can be instantiated', () { + expect( + SparkyComputerSensorBallContactBehavior(), + isA(), + ); + }); + + group('beginContact', () { + flameTester.test( + 'stops a ball', + (game) async { + final behavior = SparkyComputerSensorBallContactBehavior(); + final bloc = _MockSparkyComputerCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SparkyComputerState.withoutBall, + ); + + final sparkyComputer = SparkyComputer.test( + bloc: bloc, + ); + await sparkyComputer.add(behavior); + await game.ensureAdd(sparkyComputer); + + final ball = _MockBall(); + await behavior.beginContact(ball, _MockContact()); + + verify(ball.stop).called(1); + }, + ); + + flameTester.test( + 'emits onBallEntered when contacts with a ball', + (game) async { + final behavior = SparkyComputerSensorBallContactBehavior(); + final bloc = _MockSparkyComputerCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SparkyComputerState.withoutBall, + ); + + final sparkyComputer = SparkyComputer.test( + bloc: bloc, + ); + await sparkyComputer.add(behavior); + await game.ensureAdd(sparkyComputer); + + await behavior.beginContact(_MockBall(), _MockContact()); + + verify(sparkyComputer.bloc.onBallEntered).called(1); + }, + ); + + flameTester.test( + 'adds TimerComponent when contacts with a ball', + (game) async { + final behavior = SparkyComputerSensorBallContactBehavior(); + final bloc = _MockSparkyComputerCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SparkyComputerState.withoutBall, + ); + + final sparkyComputer = SparkyComputer.test( + bloc: bloc, + ); + await sparkyComputer.add(behavior); + await game.ensureAdd(sparkyComputer); + + await behavior.beginContact(_MockBall(), _MockContact()); + await game.ready(); + + expect( + sparkyComputer.firstChild(), + isA(), + ); + }, + ); + + flameTester.test( + 'TimerComponent resumes ball and calls onBallTurboCharged onTick', + (game) async { + final behavior = SparkyComputerSensorBallContactBehavior(); + final bloc = _MockSparkyComputerCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SparkyComputerState.withoutBall, + ); + + final sparkyComputer = SparkyComputer.test( + bloc: bloc, + ); + await sparkyComputer.add(behavior); + await game.ensureAdd(sparkyComputer); + + final ball = _MockBall(); + await behavior.beginContact(ball, _MockContact()); + await game.ready(); + game.update( + sparkyComputer.firstChild()!.timer.limit, + ); + await game.ready(); + + verify(ball.resume).called(1); + verify(sparkyComputer.bloc.onBallTurboCharged).called(1); + }, + ); + }); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/sparky_computer/cubit/sparky_computer_cubit_test.dart b/packages/pinball_components/test/src/components/sparky_computer/cubit/sparky_computer_cubit_test.dart new file mode 100644 index 00000000..b08b412f --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_computer/cubit/sparky_computer_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( + 'SparkyComputerCubit', + () { + blocTest( + 'onBallEntered emits withBall', + build: SparkyComputerCubit.new, + act: (bloc) => bloc.onBallEntered(), + expect: () => [SparkyComputerState.withBall], + ); + + blocTest( + 'onBallTurboCharged emits withoutBall', + build: SparkyComputerCubit.new, + act: (bloc) => bloc.onBallTurboCharged(), + expect: () => [SparkyComputerState.withoutBall], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/sparky_computer/sparky_computer_test.dart b/packages/pinball_components/test/src/components/sparky_computer/sparky_computer_test.dart new file mode 100644 index 00000000..ffb14fd8 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_computer/sparky_computer_test.dart @@ -0,0 +1,93 @@ +// 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/sparky_computer/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +class _MockSparkyComputerCubit extends Mock implements SparkyComputerCubit {} + +void main() { + group('SparkyComputer', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.glow.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('loads correctly', (game) async { + final component = SparkyComputer(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.ensureAdd(SparkyComputer()); + await tester.pump(); + + game.camera + ..followVector2(Vector2(0, -20)) + ..zoom = 7; + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/sparky-computer.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 = _MockSparkyComputerCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SparkyComputerState.withoutBall, + ); + when(bloc.close).thenAnswer((_) async {}); + final sparkyComputer = SparkyComputer.test(bloc: bloc); + + await game.ensureAdd(sparkyComputer); + game.remove(sparkyComputer); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final sparkyComputer = SparkyComputer( + children: [component], + ); + await game.ensureAdd(sparkyComputer); + expect(sparkyComputer.children, contains(component)); + }); + + flameTester.test('a SparkyComputerSensorBallContactBehavior', + (game) async { + final sparkyComputer = SparkyComputer(); + await game.ensureAdd(sparkyComputer); + expect( + sparkyComputer.children + .whereType() + .single, + isNotNull, + ); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/sparky_computer_test.dart b/packages/pinball_components/test/src/components/sparky_computer_test.dart deleted file mode 100644 index ffba79b6..00000000 --- a/packages/pinball_components/test/src/components/sparky_computer_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('SparkyComputer', () { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.sparky.computer.base.keyName, - Assets.images.sparky.computer.top.keyName, - Assets.images.sparky.computer.glow.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); - - flameTester.test('loads correctly', (game) async { - final component = SparkyComputer(); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); - }); - - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - await game.images.loadAll(assets); - await game.ensureAdd(SparkyComputer()); - await tester.pump(); - - game.camera - ..followVector2(Vector2(0, -20)) - ..zoom = 7; - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('golden/sparky-computer.png'), - ); - }, - ); - }); -} diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart deleted file mode 100644 index 95451515..00000000 --- a/test/game/components/controlled_ball_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -// ignore_for_file: cascade_invocations - -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/game.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_theme/pinball_theme.dart' as theme; - -class _TestGame extends Forge2DGame { - @override - Future onLoad() async { - images.prefix = ''; - await images.load(theme.Assets.images.dash.ball.keyName); - } - - Future pump(Ball child, {required GameBloc gameBloc}) async { - await ensureAdd( - FlameBlocProvider.value( - value: gameBloc, - children: [child], - ), - ); - } -} - -class _MockGameBloc extends Mock implements GameBloc {} - -class _MockBall extends Mock implements Ball {} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('BallController', () { - late Ball ball; - late GameBloc gameBloc; - - setUp(() { - ball = Ball(); - gameBloc = _MockGameBloc(); - }); - - final flameBlocTester = FlameTester(_TestGame.new); - - test('can be instantiated', () { - expect( - BallController(_MockBall()), - isA(), - ); - }); - - flameBlocTester.testGameWidget( - 'turboCharge adds TurboChargeActivated', - setUp: (game, tester) async { - await game.onLoad(); - - final controller = BallController(ball); - await ball.add(controller); - await game.pump(ball, gameBloc: gameBloc); - - await controller.turboCharge(); - }, - verify: (game, tester) async { - verify(() => gameBloc.add(const SparkyTurboChargeActivated())) - .called(1); - }, - ); - }); -} diff --git a/test/game/components/sparky_scorch/behaviors/sparky_computer_bonus_behavior_test.dart b/test/game/components/sparky_scorch/behaviors/sparky_computer_bonus_behavior_test.dart new file mode 100644 index 00000000..fbfeef0b --- /dev/null +++ b/test/game/components/sparky_scorch/behaviors/sparky_computer_bonus_behavior_test.dart @@ -0,0 +1,86 @@ +// ignore_for_file: cascade_invocations + +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/sparky_scorch/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.glow.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, + ]); + } + + Future pump( + SparkyScorch child, { + required GameBloc gameBloc, + }) async { + // Not needed once https://github.com/flame-engine/flame/issues/1607 + // is fixed + await onLoad(); + await ensureAdd( + FlameBlocProvider.value( + value: gameBloc, + children: [child], + ), + ); + } +} + +class _MockGameBloc extends Mock implements GameBloc {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SparkyComputerBonusBehavior', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + }); + + final flameTester = FlameTester(_TestGame.new); + + flameTester.testGameWidget( + 'adds GameBonus.sparkyTurboCharge to the game and plays animatronic ' + 'when SparkyComputerState.withBall is emitted', + setUp: (game, tester) async { + final behavior = SparkyComputerBonusBehavior(); + final parent = SparkyScorch.test(); + final sparkyComputer = SparkyComputer(); + final animatronic = SparkyAnimatronic(); + + await parent.addAll([ + sparkyComputer, + animatronic, + ]); + await game.pump(parent, gameBloc: gameBloc); + await parent.ensureAdd(behavior); + + sparkyComputer.bloc.onBallEntered(); + await tester.pump(); + + verify( + () => gameBloc.add(const BonusActivated(GameBonus.sparkyTurboCharge)), + ).called(1); + expect(animatronic.playing, isTrue); + }, + ); + }); +} diff --git a/test/game/components/sparky_scorch_test.dart b/test/game/components/sparky_scorch/sparky_scorch_test.dart similarity index 58% rename from test/game/components/sparky_scorch_test.dart rename to test/game/components/sparky_scorch/sparky_scorch_test.dart index 92a3ab01..0cd7b806 100644 --- a/test/game/components/sparky_scorch_test.dart +++ b/test/game/components/sparky_scorch/sparky_scorch_test.dart @@ -1,10 +1,11 @@ // ignore_for_file: cascade_invocations +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/behaviors/behaviors.dart'; +import 'package:pinball/game/components/sparky_scorch/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -25,13 +26,16 @@ class _TestGame extends Forge2DGame { Assets.images.sparky.bumper.c.dimmed.keyName, ]); } -} - -class _MockControlledBall extends Mock implements ControlledBall {} - -class _MockBallController extends Mock implements BallController {} -class _MockContact extends Mock implements Contact {} + Future pump(SparkyScorch child) async { + await ensureAdd( + FlameBlocProvider.value( + value: GameBloc(), + children: [child], + ), + ); + } +} void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -41,15 +45,18 @@ void main() { group('SparkyScorch', () { flameTester.test('loads correctly', (game) async { final component = SparkyScorch(); - await game.ensureAdd(component); - expect(game.contains(component), isTrue); + await game.pump(component); + expect( + game.descendants().whereType().length, + equals(1), + ); }); group('loads', () { flameTester.test( 'a SparkyComputer', (game) async { - await game.ensureAdd(SparkyScorch()); + await game.pump(SparkyScorch()); expect( game.descendants().whereType().length, equals(1), @@ -60,7 +67,7 @@ void main() { flameTester.test( 'a SparkyAnimatronic', (game) async { - await game.ensureAdd(SparkyScorch()); + await game.pump(SparkyScorch()); expect( game.descendants().whereType().length, equals(1), @@ -71,7 +78,7 @@ void main() { flameTester.test( 'three SparkyBumper', (game) async { - await game.ensureAdd(SparkyScorch()); + await game.pump(SparkyScorch()); expect( game.descendants().whereType().length, equals(3), @@ -82,7 +89,7 @@ void main() { flameTester.test( 'three SparkyBumpers with BumperNoiseBehavior', (game) async { - await game.ensureAdd(SparkyScorch()); + await game.pump(SparkyScorch()); final bumpers = game.descendants().whereType(); for (final bumper in bumpers) { expect( @@ -93,41 +100,30 @@ void main() { }, ); }); - }); - - group('SparkyComputerSensor', () { - flameTester.test('calls turboCharge', (game) async { - final sensor = SparkyComputerSensor(); - final ball = _MockControlledBall(); - final controller = _MockBallController(); - when(() => ball.controller).thenReturn(controller); - when(controller.turboCharge).thenAnswer((_) async {}); - - await game.ensureAddAll([ - sensor, - SparkyAnimatronic(), - ]); - sensor.beginContact(ball, _MockContact()); - - verify(() => ball.controller.turboCharge()).called(1); - }); + group('adds', () { + flameTester.test( + 'ScoringContactBehavior to SparkyComputer', + (game) async { + await game.pump(SparkyScorch()); - flameTester.test('plays SparkyAnimatronic', (game) async { - final sensor = SparkyComputerSensor(); - final sparkyAnimatronic = SparkyAnimatronic(); - final ball = _MockControlledBall(); - final controller = _MockBallController(); - when(() => ball.controller).thenReturn(controller); - when(controller.turboCharge).thenAnswer((_) async {}); - await game.ensureAddAll([ - sensor, - sparkyAnimatronic, - ]); + final sparkyComputer = + game.descendants().whereType().single; + expect( + sparkyComputer.firstChild(), + isNotNull, + ); + }, + ); - expect(sparkyAnimatronic.playing, isFalse); - sensor.beginContact(ball, _MockContact()); - expect(sparkyAnimatronic.playing, isTrue); + flameTester.test('a SparkyComputerBonusBehavior', (game) async { + final sparkyScorch = SparkyScorch(); + await game.pump(sparkyScorch); + expect( + sparkyScorch.children.whereType().single, + isNotNull, + ); + }); }); }); } diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index b983b0b8..4130ca77 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -409,14 +409,12 @@ void main() { when(() => tapUpEvent.raw).thenReturn(raw); await game.ready(); - final previousBalls = - game.descendants().whereType().toList(); + final previousBalls = game.descendants().whereType().toList(); game.onTapUp(0, tapUpEvent); await game.ready(); - final currentBalls = - game.descendants().whereType().toList(); + final currentBalls = game.descendants().whereType().toList(); expect( currentBalls.length, @@ -475,14 +473,13 @@ void main() { game.lineEnd = endPosition; await game.ready(); - final previousBalls = - game.descendants().whereType().toList(); + final previousBalls = game.descendants().whereType().toList(); game.onPanEnd(_MockDragEndInfo()); await game.ready(); expect( - game.descendants().whereType().length, + game.descendants().whereType().length, equals(previousBalls.length + 1), ); },