From eb8cc4bb6fbb5ae5744236efa8b81997150dc6ec Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Sat, 7 May 2022 17:45:48 +0100 Subject: [PATCH 1/2] fix: improved entering SpaceshipRamp consitency (#385) --- .../spaceship_ramp/spaceship_ramp.dart | 31 +++++++++--- .../spaceship_ramp/spaceship_ramp_test.dart | 49 +++++++++++++++++++ .../src/behaviors/layer_contact_behavior.dart | 21 +++++--- .../behaviors/z_index_contact_behavior.dart | 21 +++++--- .../layer_contact_behavior_test.dart | 18 +++++++ .../z_index_contact_behavior_test.dart | 15 ++++++ 6 files changed, 131 insertions(+), 24 deletions(-) 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 0796be92..ea7ed2e7 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 @@ -42,7 +42,7 @@ class SpaceshipRamp extends Component { _SpaceshipRampBackground(), _SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5), _SpaceshipRampForegroundRailing(), - _SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), + SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), _SpaceshipRampBackgroundRailingSpriteComponent(), SpaceshipRampArrowSpriteComponent( current: bloc.state.hits, @@ -255,9 +255,14 @@ class _SpaceshipRampBoardOpening extends BodyComponent _SpaceshipRampBoardOpeningSpriteComponent(), LayerContactBehavior(layer: Layer.spaceshipEntranceRamp) ..applyTo(['inside']), - LayerContactBehavior(layer: Layer.board)..applyTo(['outside']), - ZIndexContactBehavior(zIndex: ZIndexes.ballOnBoard) - ..applyTo(['outside']), + LayerContactBehavior( + layer: Layer.board, + onBegin: false, + )..applyTo(['outside']), + ZIndexContactBehavior( + zIndex: ZIndexes.ballOnBoard, + onBegin: false, + )..applyTo(['outside']), ZIndexContactBehavior(zIndex: ZIndexes.ballOnSpaceshipRamp) ..applyTo(['middle', 'inside']), ], @@ -426,9 +431,19 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent } } -class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition { - _SpaceshipRampBase() : super(renderBody: false) { - layer = Layer.board; +@visibleForTesting +class SpaceshipRampBase extends BodyComponent + with InitialPosition, ContactCallbacks { + SpaceshipRampBase() : super(renderBody: false); + + @override + void preSolve(Object other, Contact contact, Manifold oldManifold) { + super.preSolve(other, contact, oldManifold); + if (other is! Layered) return; + // Although, the Layer should already be taking care of the contact + // filtering, this is to ensure the ball doesn't collide with the ramp base + // when the filtering is calculated on different time steps. + contact.setEnabled(other.layer == Layer.board); } @override @@ -441,7 +456,7 @@ class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition { Vector2(4.1, 1.5), ], ); - final bodyDef = BodyDef(position: initialPosition); + final bodyDef = BodyDef(position: initialPosition, userData: this); return world.createBody(bodyDef)..createFixtureFromShape(shape); } } 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 b74cfb88..7bd18aeb 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 @@ -2,6 +2,7 @@ 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'; @@ -12,6 +13,12 @@ import '../../../helpers/helpers.dart'; class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + +class _MockManifold extends Mock implements Manifold {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -275,4 +282,46 @@ void main() { }); }); }); + + group('SpaceshipRampBase', () { + test('can be instantiated', () { + expect(SpaceshipRampBase(), isA()); + }); + + flameTester.test('can be loaded', (game) async { + final component = SpaceshipRampBase(); + await game.ensureAdd(component); + expect(game.children, contains(component)); + }); + + flameTester.test( + 'postSolves disables contact when ball is not on Layer.board', + (game) async { + final ball = _MockBall(); + final contact = _MockContact(); + when(() => ball.layer).thenReturn(Layer.spaceshipEntranceRamp); + final component = SpaceshipRampBase(); + await game.ensureAdd(component); + + component.preSolve(ball, contact, _MockManifold()); + + verify(() => contact.setEnabled(false)).called(1); + }, + ); + + flameTester.test( + 'postSolves enables contact when ball is on Layer.board', + (game) async { + final ball = _MockBall(); + final contact = _MockContact(); + when(() => ball.layer).thenReturn(Layer.board); + final component = SpaceshipRampBase(); + await game.ensureAdd(component); + + component.preSolve(ball, contact, _MockManifold()); + + verify(() => contact.setEnabled(true)).called(1); + }, + ); + }); } diff --git a/packages/pinball_flame/lib/src/behaviors/layer_contact_behavior.dart b/packages/pinball_flame/lib/src/behaviors/layer_contact_behavior.dart index a73b94a5..ef475bc9 100644 --- a/packages/pinball_flame/lib/src/behaviors/layer_contact_behavior.dart +++ b/packages/pinball_flame/lib/src/behaviors/layer_contact_behavior.dart @@ -6,15 +6,20 @@ import 'package:pinball_flame/pinball_flame.dart'; /// {@endtemplate} class LayerContactBehavior extends ContactBehavior { /// {@macro layer_contact_behavior} - LayerContactBehavior({required Layer layer}) : _layer = layer; - - final Layer _layer; + LayerContactBehavior({ + required Layer layer, + bool onBegin = true, + }) { + if (onBegin) { + onBeginContact = (other, _) => _changeLayer(other, layer); + } else { + onEndContact = (other, _) => _changeLayer(other, layer); + } + } - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); + void _changeLayer(Object other, Layer layer) { if (other is! Layered) return; - if (other.layer == _layer) return; - other.layer = _layer; + if (other.layer == layer) return; + other.layer = layer; } } diff --git a/packages/pinball_flame/lib/src/behaviors/z_index_contact_behavior.dart b/packages/pinball_flame/lib/src/behaviors/z_index_contact_behavior.dart index ea9bfcad..c763e2cf 100644 --- a/packages/pinball_flame/lib/src/behaviors/z_index_contact_behavior.dart +++ b/packages/pinball_flame/lib/src/behaviors/z_index_contact_behavior.dart @@ -6,15 +6,20 @@ import 'package:pinball_flame/pinball_flame.dart'; /// {@endtemplate} class ZIndexContactBehavior extends ContactBehavior { /// {@macro layer_contact_behavior} - ZIndexContactBehavior({required int zIndex}) : _zIndex = zIndex; - - final int _zIndex; + ZIndexContactBehavior({ + required int zIndex, + bool onBegin = true, + }) { + if (onBegin) { + onBeginContact = (other, _) => _changeZIndex(other, zIndex); + } else { + onEndContact = (other, _) => _changeZIndex(other, zIndex); + } + } - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); + void _changeZIndex(Object other, int zIndex) { if (other is! ZIndex) return; - if (other.zIndex == _zIndex) return; - other.zIndex = _zIndex; + if (other.zIndex == zIndex) return; + other.zIndex = zIndex; } } diff --git a/packages/pinball_flame/test/src/behaviors/layer_contact_behavior_test.dart b/packages/pinball_flame/test/src/behaviors/layer_contact_behavior_test.dart index 49040977..d4b7ba18 100644 --- a/packages/pinball_flame/test/src/behaviors/layer_contact_behavior_test.dart +++ b/packages/pinball_flame/test/src/behaviors/layer_contact_behavior_test.dart @@ -56,5 +56,23 @@ void main() { expect(component.layer, newLayer); }); + + flameTester.test('endContact changes layer', (game) async { + const oldLayer = Layer.all; + const newLayer = Layer.board; + final behavior = LayerContactBehavior( + layer: newLayer, + onBegin: false, + ); + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + final component = _TestLayeredBodyComponent(layer: oldLayer); + + behavior.endContact(component, _MockContact()); + + expect(component.layer, newLayer); + }); }); } diff --git a/packages/pinball_flame/test/src/behaviors/z_index_contact_behavior_test.dart b/packages/pinball_flame/test/src/behaviors/z_index_contact_behavior_test.dart index ad09004c..292a51fc 100644 --- a/packages/pinball_flame/test/src/behaviors/z_index_contact_behavior_test.dart +++ b/packages/pinball_flame/test/src/behaviors/z_index_contact_behavior_test.dart @@ -56,5 +56,20 @@ void main() { expect(component.zIndex, newIndex); }); + + flameTester.test('endContact changes zIndex', (game) async { + const oldIndex = 0; + const newIndex = 1; + final behavior = ZIndexContactBehavior(zIndex: newIndex, onBegin: false); + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + final component = _TestZIndexBodyComponent(zIndex: oldIndex); + + behavior.endContact(component, _MockContact()); + + expect(component.zIndex, newIndex); + }); }); } From 0b22b712b0695b42625f983a444f397082279c43 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Sat, 7 May 2022 18:25:29 +0100 Subject: [PATCH 2/2] refactor: removed `ControlledFlipper` (#343) * feat: implemented Flippers behaviors * test: tested Flipper behaviors * refactor: fixed typo * refactor: removed unecessary import * feat: removed and added behavior depending on game status * test: renamed test * test: changed golden path * refactor: removed TODOs --- lib/game/components/bottom_group.dart | 3 +- lib/game/components/components.dart | 1 - .../components/game_bloc_status_listener.dart | 19 + .../lib/src/components/components.dart | 2 +- .../flipper/behaviors/behaviors.dart | 2 + .../behaviors/flipper_jointing_behavior.dart | 103 +++++ .../flipper_key_controlling_behavior.dart | 40 +- .../src/components/{ => flipper}/flipper.dart | 130 +------ .../stories/bottom_group/flipper_game.dart | 40 -- .../flipper_jointing_behavior_test.dart | 38 ++ ...flipper_key_controlling_behavior_test.dart | 357 ++++++++++++++++++ .../{ => flipper}/flipper_test.dart | 4 +- .../components/controlled_flipper_test.dart | 260 ------------- .../game_bloc_status_listener_test.dart | 187 ++++++--- 14 files changed, 700 insertions(+), 486 deletions(-) create mode 100644 packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart create mode 100644 packages/pinball_components/lib/src/components/flipper/behaviors/flipper_jointing_behavior.dart rename lib/game/components/controlled_flipper.dart => packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart (50%) rename packages/pinball_components/lib/src/components/{ => flipper}/flipper.dart (56%) create mode 100644 packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart create mode 100644 packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart rename packages/pinball_components/test/src/components/{ => flipper}/flipper_test.dart (97%) delete mode 100644 test/game/components/controlled_flipper_test.dart diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart index 4c6b2822..bc644f96 100644 --- a/lib/game/components/bottom_group.dart +++ b/lib/game/components/bottom_group.dart @@ -1,6 +1,5 @@ import 'package:flame/components.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'; @@ -40,7 +39,7 @@ class _BottomGroupSide extends Component { final direction = _side.direction; final centerXAdjustment = _side.isLeft ? -0.45 : -6.8; - final flipper = ControlledFlipper( + final flipper = Flipper( side: _side, )..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6); final baseboard = Baseboard(side: _side) diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 08dc5cb0..324f379a 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_flipper.dart'; export 'controlled_plunger.dart'; export 'dino_desert/dino_desert.dart'; export 'drain/drain.dart'; diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index 81da96d5..84627eb0 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -3,6 +3,7 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// Listens to the [GameBloc] and updates the game accordingly. @@ -20,6 +21,11 @@ class GameBlocStatusListener extends Component break; case GameStatus.playing: readProvider().play(PinballAudio.backgroundMusic); + gameRef + .descendants() + .whereType() + .forEach(_addFlipperKeyControls); + gameRef.overlays.remove(PinballGame.playButtonOverlay); break; case GameStatus.gameOver: @@ -30,7 +36,20 @@ class GameBlocStatusListener extends Component .state .characterTheme, ); + + gameRef + .descendants() + .whereType() + .forEach(_removeFlipperKeyControls); break; } } + + void _addFlipperKeyControls(Flipper flipper) => + flipper.add(FlipperKeyControllingBehavior()); + + void _removeFlipperKeyControls(Flipper flipper) => flipper + .descendants() + .whereType() + .forEach(flipper.remove); } diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 7fda6272..2f420181 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -15,7 +15,7 @@ export 'dino_walls.dart'; export 'error_component.dart'; export 'fire_effect.dart'; export 'flapper/flapper.dart'; -export 'flipper.dart'; +export 'flipper/flipper.dart'; export 'google_letter/google_letter.dart'; export 'initial_position.dart'; export 'joint_anchor.dart'; diff --git a/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart new file mode 100644 index 00000000..ef3630e7 --- /dev/null +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'flipper_jointing_behavior.dart'; +export 'flipper_key_controlling_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_jointing_behavior.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_jointing_behavior.dart new file mode 100644 index 00000000..c0574f6c --- /dev/null +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_jointing_behavior.dart @@ -0,0 +1,103 @@ +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'; + +/// Joints the [Flipper] to allow pivoting around one end. +class FlipperJointingBehavior extends Component + with ParentIsA, HasGameRef { + late final RevoluteJoint _joint; + + @override + Future onLoad() async { + await super.onLoad(); + + final anchor = _FlipperAnchor(flipper: parent); + await add(anchor); + + final jointDef = _FlipperAnchorRevoluteJointDef( + flipper: parent, + anchor: anchor, + ); + _joint = _FlipperJoint(jointDef); + parent.world.createJoint(_joint); + } + + @override + void onMount() { + gameRef.ready().whenComplete( + () => parent.body.joints.whereType<_FlipperJoint>().first.unlock(), + ); + } +} + +/// {@template flipper_anchor} +/// [JointAnchor] positioned at the end of a [Flipper]. +/// +/// The end of a [Flipper] depends on its [Flipper.side]. +/// {@endtemplate} +class _FlipperAnchor extends JointAnchor { + /// {@macro flipper_anchor} + _FlipperAnchor({ + required Flipper flipper, + }) { + initialPosition = Vector2( + (Flipper.size.x * flipper.side.direction) / 2 - + (1.65 * flipper.side.direction), + -0.15, + ); + } +} + +/// {@template flipper_anchor_revolute_joint_def} +/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve a potivoting +/// motion. +/// {@endtemplate} +class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef { + /// {@macro flipper_anchor_revolute_joint_def} + _FlipperAnchorRevoluteJointDef({ + required Flipper flipper, + required _FlipperAnchor anchor, + }) : side = flipper.side { + enableLimit = true; + initialize( + flipper.body, + anchor.body, + flipper.body.position + anchor.body.position, + ); + } + + final BoardSide side; +} + +/// {@template flipper_joint} +/// [RevoluteJoint] that controls the pivoting motion of a [Flipper]. +/// {@endtemplate} +class _FlipperJoint extends RevoluteJoint { + /// {@macro flipper_joint} + _FlipperJoint(_FlipperAnchorRevoluteJointDef def) + : side = def.side, + super(def) { + lock(); + } + + /// Half the angle of the arc motion. + static const _halfSweepingAngle = 0.611; + + final BoardSide side; + + /// Locks the [Flipper] to its resting position. + /// + /// The joint is locked when initialized in order to force the [Flipper] + /// at its resting position. + void lock() { + final angle = _halfSweepingAngle * side.direction; + setLimits(angle, angle); + } + + /// Unlocks the [Flipper] from its resting position. + void unlock() { + const angle = _halfSweepingAngle; + setLimits(-angle, angle); + } +} diff --git a/lib/game/components/controlled_flipper.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart similarity index 50% rename from lib/game/components/controlled_flipper.dart rename to packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart index 1d5502c6..95566e75 100644 --- a/lib/game/components/controlled_flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart @@ -1,49 +1,33 @@ import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/services.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -/// {@template controlled_flipper} -/// A [Flipper] with a [FlipperController] attached. -/// {@endtemplate} -class ControlledFlipper extends Flipper with Controls { - /// {@macro controlled_flipper} - ControlledFlipper({ - required BoardSide side, - }) : super(side: side) { - controller = FlipperController(this); - } -} - -/// {@template flipper_controller} -/// A [ComponentController] that controls a [Flipper]s movement. -/// {@endtemplate} -class FlipperController extends ComponentController - with KeyboardHandler, FlameBlocReader { - /// {@macro flipper_controller} - FlipperController(Flipper flipper) - : _keys = flipper.side.flipperKeys, - super(flipper); - +/// Allows controlling the [Flipper]'s movement with keyboard input. +class FlipperKeyControllingBehavior extends Component + with KeyboardHandler, ParentIsA { /// The [LogicalKeyboardKey]s that will control the [Flipper]. /// /// [onKeyEvent] method listens to when one of these keys is pressed. - final List _keys; + late final List _keys; + + @override + Future onLoad() async { + await super.onLoad(); + _keys = parent.side.flipperKeys; + } @override bool onKeyEvent( RawKeyEvent event, Set keysPressed, ) { - if (!bloc.state.status.isPlaying) return true; if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { - component.moveUp(); + parent.moveUp(); } else if (event is RawKeyUpEvent) { - component.moveDown(); + parent.moveDown(); } return false; diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper/flipper.dart similarity index 56% rename from packages/pinball_components/lib/src/components/flipper.dart rename to packages/pinball_components/lib/src/components/flipper/flipper.dart index ca033440..280c157f 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper/flipper.dart @@ -2,8 +2,11 @@ import 'dart:async'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/foundation.dart'; import 'package:pinball_components/pinball_components.dart'; +export 'behaviors/behaviors.dart'; + /// {@template flipper} /// A bat, typically found in pairs at the bottom of the board. /// @@ -15,9 +18,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { required this.side, }) : super( renderBody: false, - children: [_FlipperSpriteComponent(side: side)], + children: [ + _FlipperSpriteComponent(side: side), + FlipperJointingBehavior(), + ], ); + /// Creates a [Flipper] without any children. + /// + /// This can be used for testing [Flipper]'s behaviors in isolation. + @visibleForTesting + Flipper.test({required this.side}); + /// The size of the [Flipper]. static final size = Vector2(13.5, 4.3); @@ -44,19 +56,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { body.linearVelocity = Vector2(0, -_speed); } - /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. - Future _anchorToJoint() async { - final anchor = _FlipperAnchor(flipper: this); - await add(anchor); - - final jointDef = _FlipperAnchorRevoluteJointDef( - flipper: this, - anchor: anchor, - ); - final joint = _FlipperJoint(jointDef); - world.createJoint(joint); - } - List _createFixtureDefs() { final direction = side.direction; @@ -73,7 +72,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { assetShadow, 0, ); - final bigCircleFixtureDef = FixtureDef(bigCircleShape); final smallCircleShape = CircleShape()..radius = size.y * 0.23; smallCircleShape.position.setValues( @@ -82,7 +80,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { assetShadow, 0, ); - final smallCircleFixtureDef = FixtureDef(smallCircleShape); final trapeziumVertices = side.isLeft ? [ @@ -98,26 +95,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { Vector2(smallCircleShape.position.x, -smallCircleShape.radius), ]; final trapezium = PolygonShape()..set(trapeziumVertices); - final trapeziumFixtureDef = FixtureDef( - trapezium, - density: 50, - friction: .1, - ); return [ - bigCircleFixtureDef, - smallCircleFixtureDef, - trapeziumFixtureDef, + FixtureDef(bigCircleShape), + FixtureDef(smallCircleShape), + FixtureDef( + trapezium, + density: 50, + friction: .1, + ), ]; } - @override - Future onLoad() async { - await super.onLoad(); - - await _anchorToJoint(); - } - @override Body createBody() { final bodyDef = BodyDef( @@ -131,15 +120,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { return body; } - - @override - void onMount() { - super.onMount(); - - gameRef.ready().whenComplete( - () => body.joints.whereType<_FlipperJoint>().first.unlock(), - ); - } } class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { @@ -163,73 +143,3 @@ class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { size = sprite.originalSize / 10; } } - -/// {@template flipper_anchor} -/// [JointAnchor] positioned at the end of a [Flipper]. -/// -/// The end of a [Flipper] depends on its [Flipper.side]. -/// {@endtemplate} -class _FlipperAnchor extends JointAnchor { - /// {@macro flipper_anchor} - _FlipperAnchor({ - required Flipper flipper, - }) { - initialPosition = Vector2( - (Flipper.size.x * flipper.side.direction) / 2 - - (1.65 * flipper.side.direction), - -0.15, - ); - } -} - -/// {@template flipper_anchor_revolute_joint_def} -/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion. -/// {@endtemplate} -class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef { - /// {@macro flipper_anchor_revolute_joint_def} - _FlipperAnchorRevoluteJointDef({ - required Flipper flipper, - required _FlipperAnchor anchor, - }) : side = flipper.side { - enableLimit = true; - initialize( - flipper.body, - anchor.body, - flipper.body.position + anchor.body.position, - ); - } - - final BoardSide side; -} - -/// {@template flipper_joint} -/// [RevoluteJoint] that controls the arc motion of a [Flipper]. -/// {@endtemplate} -class _FlipperJoint extends RevoluteJoint { - /// {@macro flipper_joint} - _FlipperJoint(_FlipperAnchorRevoluteJointDef def) - : side = def.side, - super(def) { - lock(); - } - - /// Half the angle of the arc motion. - static const _halfSweepingAngle = 0.611; - - final BoardSide side; - - /// Locks the [Flipper] to its resting position. - /// - /// The joint is locked when initialized in order to force the [Flipper] - /// at its resting position. - void lock() { - final angle = _halfSweepingAngle * side.direction; - setLimits(angle, angle); - } - - /// Unlocks the [Flipper] from its resting position. - void unlock() { - const angle = _halfSweepingAngle; - setLimits(-angle, angle); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart index 789fa8b4..bdb23141 100644 --- a/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart @@ -1,6 +1,4 @@ import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; @@ -23,16 +21,6 @@ class FlipperGame extends BallGame with KeyboardEvents { - Press right arrow key or "D" to move the right flipper. '''; - static const _leftFlipperKeys = [ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.keyA, - ]; - - static const _rightFlipperKeys = [ - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.keyD, - ]; - late Flipper leftFlipper; late Flipper rightFlipper; @@ -50,32 +38,4 @@ class FlipperGame extends BallGame with KeyboardEvents { await traceAllBodies(); } - - @override - KeyEventResult onKeyEvent( - RawKeyEvent event, - Set keysPressed, - ) { - final movedLeftFlipper = _leftFlipperKeys.contains(event.logicalKey); - if (movedLeftFlipper) { - if (event is RawKeyDownEvent) { - leftFlipper.moveUp(); - } else if (event is RawKeyUpEvent) { - leftFlipper.moveDown(); - } - } - - final movedRightFlipper = _rightFlipperKeys.contains(event.logicalKey); - if (movedRightFlipper) { - if (event is RawKeyDownEvent) { - rightFlipper.moveUp(); - } else if (event is RawKeyUpEvent) { - rightFlipper.moveDown(); - } - } - - return movedLeftFlipper || movedRightFlipper - ? KeyEventResult.handled - : KeyEventResult.ignored; - } } diff --git a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart new file mode 100644 index 00000000..3d6c3b83 --- /dev/null +++ b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart @@ -0,0 +1,38 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/components/components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('FlipperJointingBehavior', () { + final flameTester = FlameTester(TestGame.new); + + test('can be instantiated', () { + expect( + FlipperJointingBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final behavior = FlipperJointingBehavior(); + final parent = Flipper.test(side: BoardSide.left); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + expect(parent.contains(behavior), isTrue); + }); + + flameTester.test('creates a joint', (game) async { + final behavior = FlipperJointingBehavior(); + final parent = Flipper.test(side: BoardSide.left); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + expect(parent.body.joints, isNotEmpty); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart new file mode 100644 index 00000000..11af6187 --- /dev/null +++ b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart @@ -0,0 +1,357 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('FlipperKeyControllingBehavior', () { + final flameTester = FlameTester(TestGame.new); + + group( + 'onKeyEvent', + () { + late Flipper rightFlipper; + late Flipper leftFlipper; + + setUp(() { + rightFlipper = Flipper.test(side: BoardSide.right); + leftFlipper = Flipper.test(side: BoardSide.left); + }); + + group('on right Flipper', () { + flameTester.test( + 'moves upwards when right arrow is pressed', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowRight, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isNegative); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves downwards when right arrow is released', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowRight, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isPositive); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves upwards when D is pressed', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyD, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isNegative); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves downwards when D is released', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyD, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isPositive); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + group("doesn't move when", () { + flameTester.test( + 'left arrow is pressed', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowLeft, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isZero); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'left arrow is released', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowLeft, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isZero); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'A is pressed', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyA, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isZero); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'A is released', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyA, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isZero); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + }); + }); + + group('on left Flipper', () { + flameTester.test( + 'moves upwards when left arrow is pressed', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowLeft, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isNegative); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves downwards when left arrow is released', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowLeft, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isPositive); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves upwards when A is pressed', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyA, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isNegative); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves downwards when A is released', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyA, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isPositive); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + group("doesn't move when", () { + flameTester.test( + 'right arrow is pressed', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowRight, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isZero); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'right arrow is released', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowRight, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isZero); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'D is pressed', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyD, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isZero); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'D is released', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyD, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isZero); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + }); + }); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/flipper_test.dart b/packages/pinball_components/test/src/components/flipper/flipper_test.dart similarity index 97% rename from packages/pinball_components/test/src/components/flipper_test.dart rename to packages/pinball_components/test/src/components/flipper/flipper_test.dart index 53b0e108..4569f3ec 100644 --- a/packages/pinball_components/test/src/components/flipper_test.dart +++ b/packages/pinball_components/test/src/components/flipper/flipper_test.dart @@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -36,7 +36,7 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/flipper.png'), + matchesGoldenFile('../golden/flipper.png'), ); }, ); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart deleted file mode 100644 index 00a69f9e..00000000 --- a/test/game/components/controlled_flipper_test.dart +++ /dev/null @@ -1,260 +0,0 @@ -import 'dart:collection'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/input.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/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents { - @override - Future onLoad() async { - images.prefix = ''; - await images.loadAll([ - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - ]); - } - - Future pump(Flipper flipper, {required GameBloc gameBloc}) { - return ensureAdd( - FlameBlocProvider.value( - value: gameBloc, - children: [flipper], - ), - ); - } -} - -class _MockGameBloc extends Mock implements GameBloc {} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(_TestGame.new); - - group('FlipperController', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = _MockGameBloc(); - }); - - group('onKeyEvent', () { - final leftKeys = UnmodifiableListView([ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.keyA, - ]); - final rightKeys = UnmodifiableListView([ - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.keyD, - ]); - - group('and Flipper is left', () { - late Flipper flipper; - late FlipperController controller; - - setUp(() { - flipper = Flipper(side: BoardSide.left); - controller = FlipperController(flipper); - flipper.add(controller); - }); - - testRawKeyDownEvents(leftKeys, (event) { - flameTester.test( - 'moves upwards ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isNegative); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyDownEvents(leftKeys, (event) { - flameTester.test( - 'does nothing when is game over', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.gameOver, - ), - ); - - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(leftKeys, (event) { - flameTester.test( - 'moves downwards ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isPositive); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(rightKeys, (event) { - flameTester.test( - 'does nothing ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - }); - - group('and Flipper is right', () { - late Flipper flipper; - late FlipperController controller; - - setUp(() { - flipper = Flipper(side: BoardSide.right); - controller = FlipperController(flipper); - flipper.add(controller); - }); - - testRawKeyDownEvents(rightKeys, (event) { - flameTester.test( - 'moves upwards ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isNegative); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(rightKeys, (event) { - flameTester.test( - 'moves downwards ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isPositive); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyDownEvents(rightKeys, (event) { - flameTester.test( - 'does nothing when is game over', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.gameOver, - ), - ); - - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(leftKeys, (event) { - flameTester.test( - 'does nothing ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - }); - }); - }); -} diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart index 767ddefa..36e496f3 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -8,16 +8,24 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; class _TestGame extends Forge2DGame { @override Future onLoad() async { images.prefix = ''; - await images.load(Assets.images.backbox.marquee.keyName); + await images.loadAll( + [ + const theme.DashTheme().leaderboardIcon.keyName, + Assets.images.backbox.marquee.keyName, + Assets.images.backbox.displayDivider.keyName, + ], + ); } Future pump( @@ -35,8 +43,15 @@ class _TestGame extends Forge2DGame { ), ], children: [ - FlameProvider.value( - pinballPlayer ?? _MockPinballPlayer(), + MultiFlameProvider( + providers: [ + FlameProvider.value( + pinballPlayer ?? _MockPinballPlayer(), + ), + FlameProvider.value( + _MockAppLocalizations(), + ), + ], children: children, ), ], @@ -50,6 +65,35 @@ class _MockPinballPlayer extends Mock implements PinballPlayer {} class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { } +class _MockAppLocalizations extends Mock implements AppLocalizations { + @override + String get score => ''; + + @override + String get name => ''; + + @override + String get rank => ''; + + @override + String get enterInitials => ''; + + @override + String get arrows => ''; + + @override + String get andPress => ''; + + @override + String get enterReturn => ''; + + @override + String get toSubmit => ''; + + @override + String get loading => ''; +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -92,54 +136,113 @@ void main() { }); group('onNewState', () { - flameTester.test( - 'changes the backbox display when the game is over', - (game) async { - final component = GameBlocStatusListener(); - final repository = _MockLeaderboardRepository(); - final backbox = Backbox(leaderboardRepository: repository); - final state = const GameState.initial() - ..copyWith( - status: GameStatus.gameOver, + group('on game over', () { + late GameState state; + + setUp(() { + state = const GameState.initial().copyWith( + status: GameStatus.gameOver, + ); + }); + + flameTester.test( + 'changes the backbox display', + (game) async { + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox(leaderboardRepository: repository); + + await game.pump([component, backbox]); + + expect(() => component.onNewState(state), returnsNormally); + }, + ); + + flameTester.test( + 'removes FlipperKeyControllingBehavior from Fipper', + (game) async { + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox(leaderboardRepository: repository); + final flipper = Flipper.test(side: BoardSide.left); + final behavior = FlipperKeyControllingBehavior(); + + await game.pump([component, backbox, flipper]); + await flipper.ensureAdd(behavior); + + expect(state.status, GameStatus.gameOver); + + component.onNewState(state); + await game.ready(); + + expect( + flipper.children.whereType(), + isEmpty, ); + }, + ); - await game.pump([component, backbox]); + flameTester.test( + 'plays the game over voice over', + (game) async { + final player = _MockPinballPlayer(); + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox(leaderboardRepository: repository); + await game.pump([component, backbox], pinballPlayer: player); - expect(() => component.onNewState(state), returnsNormally); - }, - ); + component.onNewState(state); + + verify(() => player.play(PinballAudio.gameOverVoiceOver)).called(1); + }, + ); + }); - flameTester.test( - 'plays the background music on start', - (game) async { - final player = _MockPinballPlayer(); - final component = GameBlocStatusListener(); - await game.pump([component], pinballPlayer: player); + group('on playing', () { + late GameState state; - component.onNewState( - const GameState.initial().copyWith(status: GameStatus.playing), + setUp(() { + state = const GameState.initial().copyWith( + status: GameStatus.playing, ); + }); - verify(() => player.play(PinballAudio.backgroundMusic)).called(1); - }, - ); + flameTester.test( + 'plays the background music on start', + (game) async { + final player = _MockPinballPlayer(); + final component = GameBlocStatusListener(); + await game.pump([component], pinballPlayer: player); - flameTester.test( - 'plays the game over voice over when it is game over', - (game) async { - final player = _MockPinballPlayer(); - final component = GameBlocStatusListener(); - final repository = _MockLeaderboardRepository(); - final backbox = Backbox(leaderboardRepository: repository); - await game.pump([component, backbox], pinballPlayer: player); - - component.onNewState( - const GameState.initial().copyWith(status: GameStatus.gameOver), - ); + expect(state.status, equals(GameStatus.playing)); + component.onNewState(state); - verify(() => player.play(PinballAudio.gameOverVoiceOver)).called(1); - }, - ); + verify(() => player.play(PinballAudio.backgroundMusic)).called(1); + }, + ); + + flameTester.test( + 'adds key controlling behavior to Fippers when the game is started', + (game) async { + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox(leaderboardRepository: repository); + final flipper = Flipper.test(side: BoardSide.left); + + await game.pump([component, backbox, flipper]); + + component.onNewState(state); + await game.ready(); + + expect( + flipper.children + .whereType() + .length, + equals(1), + ); + }, + ); + }); }); }); }