From 552bf040c40f83b731bb2c260795428e1484aee0 Mon Sep 17 00:00:00 2001 From: alestiago Date: Mon, 9 May 2022 00:30:44 +0100 Subject: [PATCH] refactor: implementing plunger behaviors --- .../components/game_bloc_status_listener.dart | 22 ++- lib/game/components/launcher.dart | 6 +- lib/game/pinball_game.dart | 12 +- .../plunger/behaviors/behaviors.dart | 2 + .../plunger_key_controlling_behavior.dart | 8 +- .../behaviors/plunger_noise_behavior.dart | 21 +- .../behaviors/plunger_pulling_behavior.dart | 38 ++++ .../behaviors/plunger_releasing_behavior.dart | 23 +++ .../plunger/cubit/plunger_cubit.dart | 15 ++ .../plunger/cubit/plunger_state.dart | 12 ++ .../lib/src/components/plunger/plunger.dart | 89 ++------- .../pinball_components/sandbox/pubspec.lock | 84 ++++++++ .../flipper_jointing_behavior_test.dart | 5 +- ...flipper_key_controlling_behavior_test.dart | 47 ++++- .../plunger_jointing_behavior_test.dart | 181 +++++++++++++++++ ...plunger_key_controlling_behavior_test.dart | 68 ++++++- .../plunger_noise_behavior_test.dart | 74 +++++++ .../plunger_pulling_behavior_test.dart | 22 +++ .../plunger_releasing_behavior_test.dart | 5 + .../src/components/plunger/plunger_test.dart | 186 +----------------- .../lib/src/shapes/arc_shape.dart | 1 + .../components/controlled_plunger_test.dart | 184 ----------------- .../game_bloc_status_listener_test.dart | 27 +++ 23 files changed, 676 insertions(+), 456 deletions(-) create mode 100644 packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/plunger/behaviors/plunger_releasing_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart create mode 100644 packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart create mode 100644 packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart create mode 100644 packages/pinball_components/test/src/components/plunger/behaviors/plunger_releasing_behavior_test.dart create mode 100644 packages/pinball_flame/lib/src/shapes/arc_shape.dart diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index 1a5a06df..3e2d6581 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -25,6 +25,10 @@ class GameBlocStatusListener extends Component .descendants() .whereType() .forEach(_addFlipperKeyControls); + gameRef + .descendants() + .whereType() + .forEach(_addPlungerKeyControls); gameRef.overlays.remove(PinballGame.playButtonOverlay); break; @@ -41,15 +45,23 @@ class GameBlocStatusListener extends Component .descendants() .whereType() .forEach(_removeFlipperKeyControls); + gameRef + .descendants() + .whereType() + .forEach(_removePlungerKeyControls); break; } } - void _addFlipperKeyControls(Flipper flipper) { - flipper - ..add(FlipperKeyControllingBehavior()) - ..moveDown(); - } + void _addPlungerKeyControls(Plunger plunger) => + plunger.add(PlungerKeyControllingBehavior()); + + void _removePlungerKeyControls(Plunger plunger) => + plunger.remove(PlungerKeyControllingBehavior()); + + void _addFlipperKeyControls(Flipper flipper) => flipper + ..add(FlipperKeyControllingBehavior()) + ..moveDown(); void _removeFlipperKeyControls(Flipper flipper) => flipper .descendants() diff --git a/lib/game/components/launcher.dart b/lib/game/components/launcher.dart index 99b44a80..3a609838 100644 --- a/lib/game/components/launcher.dart +++ b/lib/game/components/launcher.dart @@ -1,4 +1,5 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@template launcher} @@ -12,7 +13,10 @@ class Launcher extends Component { children: [ LaunchRamp(), Flapper(), - Plunger()..initialPosition = Vector2(41, 43.7), + FlameBlocProvider( + create: PlungerCubit.new, + children: [Plunger()..initialPosition = Vector2(41, 43.7)], + ), RocketSpriteComponent()..position = Vector2(42.8, 62.3), ], ); diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 89250986..9ff48987 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -143,9 +143,15 @@ class PinballGame extends PinballForge2DGame final rocket = descendants().whereType().first; final bounds = rocket.topLeftPosition & rocket.size; - // NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. - if (bounds.contains(info.eventPosition.game.toOffset())) { - descendants().whereType().single.pullFor(2); + // NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 + // we need to check it at the highest level manually. + final tappedRocket = bounds.contains(info.eventPosition.game.toOffset()); + if (tappedRocket) { + descendants() + .whereType>() + .first + .bloc + .pulled(); } else { final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; focusedBoardSide[pointerId] = diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/behaviors.dart index 2ad36e5b..0c772a0e 100644 --- a/packages/pinball_components/lib/src/components/plunger/behaviors/behaviors.dart +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/behaviors.dart @@ -1,3 +1,5 @@ export 'plunger_jointing_behavior.dart'; export 'plunger_key_controlling_behavior.dart'; export 'plunger_noise_behavior.dart'; +export 'plunger_pulling_behavior.dart'; +export 'plunger_releasing_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_key_controlling_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_key_controlling_behavior.dart index 821bc004..fdbd40f2 100644 --- a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_key_controlling_behavior.dart +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_key_controlling_behavior.dart @@ -1,11 +1,11 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; /// Allows controlling the [Plunger]'s movement with keyboard input. class PlungerKeyControllingBehavior extends Component - with KeyboardHandler, ParentIsA { + with KeyboardHandler, FlameBlocReader { /// The [LogicalKeyboardKey]s that will control the [Flipper]. /// /// [onKeyEvent] method listens to when one of these keys is pressed. @@ -23,9 +23,9 @@ class PlungerKeyControllingBehavior extends Component if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { - parent.pull(); + bloc.pulled(); } else if (event is RawKeyUpEvent) { - parent.release(); + bloc.released(); } return false; diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_noise_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_noise_behavior.dart index 163ae196..8f7bb84a 100644 --- a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_noise_behavior.dart +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_noise_behavior.dart @@ -1,20 +1,27 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// Plays the [PinballAudio.launcher] sound. /// /// It is attached when the plunger is released. -class PlungerNoiseBehavior extends Component { +class PlungerNoiseBehavior extends Component + with FlameBlocListenable { + late final PinballAudioPlayer _audioPlayer; + @override - Future onLoad() async { - await super.onLoad(); - readProvider().play(PinballAudio.launcher); + void onNewState(PlungerState state) { + super.onNewState(state); + if (state.isReleasing) { + _audioPlayer.play(PinballAudio.launcher); + } } @override - void update(double dt) { - super.update(dt); - removeFromParent(); + Future onLoad() async { + await super.onLoad(); + _audioPlayer = readProvider(); } } diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart new file mode 100644 index 00000000..454653a5 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart @@ -0,0 +1,38 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class PlungerPullingBehavior extends Component + with ParentIsA, FlameBlocReader { + PlungerPullingBehavior({ + required double strength, + }) : _strength = strength; + + final double _strength; + + @override + void update(double dt) { + if (bloc.state.isPulling) { + parent.body.linearVelocity = Vector2(0, _strength); + } + } +} + +class PlungerAutoPullingBehavior extends PlungerPullingBehavior { + PlungerAutoPullingBehavior({ + required double strength, + }) : super(strength: strength); + + @override + void update(double dt) { + super.update(dt); + + final joint = parent.body.joints.whereType().single; + final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit(); + if (reachedBottom) { + bloc.released(); + } + } +} diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_releasing_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_releasing_behavior.dart new file mode 100644 index 00000000..bb017732 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_releasing_behavior.dart @@ -0,0 +1,23 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class PlungerReleasingBehavior extends Component + with ParentIsA, FlameBlocListenable { + PlungerReleasingBehavior({ + required double strength, + }) : _strength = strength; + + final double _strength; // 11 + + @override + void onNewState(PlungerState state) { + super.onNewState(state); + if (state.isReleasing) { + final velocity = + (parent.initialPosition.y - parent.body.position.y) * _strength; + parent.body.linearVelocity = Vector2(0, velocity); + } + } +} diff --git a/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart new file mode 100644 index 00000000..601257d2 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart @@ -0,0 +1,15 @@ +import 'package:bloc/bloc.dart'; + +part 'plunger_state.dart'; + +class PlungerCubit extends Cubit { + PlungerCubit() : super(PlungerState.releasing); + + void pulled() { + emit(PlungerState.pulling); + } + + void released() { + emit(PlungerState.releasing); + } +} diff --git a/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart new file mode 100644 index 00000000..8b82ef96 --- /dev/null +++ b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart @@ -0,0 +1,12 @@ +part of 'plunger_cubit.dart'; + +enum PlungerState { + pulling, + + releasing, +} + +extension PlungerStateX on PlungerState { + bool get isPulling => this == PlungerState.pulling; + bool get isReleasing => this == PlungerState.releasing; +} diff --git a/packages/pinball_components/lib/src/components/plunger/plunger.dart b/packages/pinball_components/lib/src/components/plunger/plunger.dart index 17ea6c07..c15265c6 100644 --- a/packages/pinball_components/lib/src/components/plunger/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger/plunger.dart @@ -1,10 +1,13 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_components/src/components/plunger/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'behaviors/behaviors.dart'; +export 'cubit/plunger_cubit.dart'; + /// {@template plunger} /// [Plunger] serves as a spring, that shoots the ball on the right side of the /// play field. @@ -19,7 +22,8 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { children: [ _PlungerSpriteAnimationGroupComponent(), PlungerJointingBehavior(compressionDistance: 9.2), - PlungerKeyControllingBehavior(), + PlungerAutoPullingBehavior(strength: 7), + PlungerReleasingBehavior(strength: 11) ], ) { zIndex = ZIndexes.plunger; @@ -73,83 +77,27 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { return body; } - - var _pullingDownTime = 0.0; - - /// Pulls the plunger down for the given amount of [seconds]. - // ignore: use_setters_to_change_properties - void pullFor(double seconds) { - _pullingDownTime = seconds; - } - - /// Set a constant downward velocity on the [Plunger]. - void pull() { - final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!; - - body.linearVelocity = Vector2(0, 7); - sprite.pull(); - } - - /// Set an upward velocity on the [Plunger]. - /// - /// The velocity's magnitude depends on how far the [Plunger] has been pulled - /// from its original [initialPosition]. - void release() { - add(PlungerNoiseBehavior()); - final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!; - - _pullingDownTime = 0; - final velocity = (initialPosition.y - body.position.y) * 11; - body.linearVelocity = Vector2(0, velocity); - sprite.release(); - } - - @override - void update(double dt) { - // Ensure that we only pull or release when the time is greater than zero. - if (_pullingDownTime > 0) { - _pullingDownTime -= PinballForge2DGame.clampDt(dt); - if (_pullingDownTime <= 0) { - release(); - } else { - pull(); - } - } - super.update(dt); - } -} - -/// Animation states associated with a [Plunger]. -enum _PlungerAnimationState { - /// Pull state. - pull, - - /// Release state. - release, } -/// Animations for pulling and releasing [Plunger]. class _PlungerSpriteAnimationGroupComponent - extends SpriteAnimationGroupComponent<_PlungerAnimationState> - with HasGameRef { + extends SpriteAnimationGroupComponent + with HasGameRef, FlameBlocListenable { _PlungerSpriteAnimationGroupComponent() : super( anchor: Anchor.center, position: Vector2(1.87, 14.9), ); - void pull() { - if (current != _PlungerAnimationState.pull) { + @override + void onNewState(PlungerState state) { + super.onNewState(state); + final startedReleasing = state.isReleasing && !current!.isReleasing; + final startedPulling = state.isPulling && !current!.isPulling; + if (startedReleasing || startedPulling) { animation?.reset(); } - current = _PlungerAnimationState.pull; - } - void release() { - if (current != _PlungerAnimationState.release) { - animation?.reset(); - } - current = _PlungerAnimationState.release; + current = state; } @override @@ -177,9 +125,10 @@ class _PlungerSpriteAnimationGroupComponent ), ); animations = { - _PlungerAnimationState.release: pullAnimation.reversed(), - _PlungerAnimationState.pull: pullAnimation, + PlungerState.releasing: pullAnimation.reversed(), + PlungerState.pulling: pullAnimation, }; - current = _PlungerAnimationState.release; + + current = readBloc().state; } } diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index b5ac88b7..9fcb0f89 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + audioplayers: + dependency: transitive + description: + name: audioplayers + url: "https://pub.dartlang.org" + source: hosted + version: "0.20.1" bloc: dependency: transitive description: @@ -57,6 +64,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" dashbook: dependency: "direct main" description: @@ -106,6 +120,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" + flame_audio: + dependency: transitive + description: + name: flame_audio + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" flame_bloc: dependency: transitive description: @@ -179,6 +200,20 @@ packages: relative: true source: path version: "1.0.0+1" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" intl: dependency: transitive description: @@ -249,6 +284,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" path_provider_linux: dependency: transitive description: @@ -256,6 +312,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" path_provider_platform_interface: dependency: transitive description: @@ -270,6 +333,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + pinball_audio: + dependency: transitive + description: + path: "../../pinball_audio" + relative: true + source: path + version: "1.0.0+1" pinball_components: dependency: "direct main" description: @@ -415,6 +485,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -492,6 +569,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" vector_math: dependency: transitive description: 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 index 3d6c3b83..70c93439 100644 --- 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 @@ -19,19 +19,18 @@ void main() { }); flameTester.test('can be loaded', (game) async { - final behavior = FlipperJointingBehavior(); final parent = Flipper.test(side: BoardSide.left); + final behavior = FlipperJointingBehavior(); 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); + final behavior = FlipperJointingBehavior(); 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 index 11af6187..e82f2a11 100644 --- 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 @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -8,7 +9,49 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; -import '../../../../helpers/helpers.dart'; +// testRawKeyDownEvents(downKeys, (event) { +// flameTester.test( +// 'moves down ' +// 'when ${event.logicalKey.keyLabel} is pressed', +// (game) async { +// await game.pump(plunger); +// controller.onKeyEvent(event, {}); + +// expect(plunger.body.linearVelocity.y, isPositive); +// expect(plunger.body.linearVelocity.x, isZero); +// }, +// ); +// }); + +// testRawKeyUpEvents(downKeys, (event) { +// flameTester.test( +// 'moves up ' +// 'when ${event.logicalKey.keyLabel} is released ' +// 'and plunger is below its starting position', +// (game) async { +// await game.pump(plunger); +// plunger.body.setTransform(Vector2(0, 1), 0); +// controller.onKeyEvent(event, {}); + +// expect(plunger.body.linearVelocity.y, isNegative); +// expect(plunger.body.linearVelocity.x, isZero); +// }, +// ); +// }); + +// testRawKeyUpEvents(downKeys, (event) { +// flameTester.test( +// 'does not move when ${event.logicalKey.keyLabel} is released ' +// 'and plunger is in its starting position', +// (game) async { +// await game.pump(plunger); +// controller.onKeyEvent(event, {}); + +// expect(plunger.body.linearVelocity.y, isZero); +// expect(plunger.body.linearVelocity.x, isZero); +// }, +// ); +// }); class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { @override @@ -27,7 +70,7 @@ class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FlipperKeyControllingBehavior', () { - final flameTester = FlameTester(TestGame.new); + final flameTester = FlameTester(Forge2DGame.new); group( 'onKeyEvent', diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_jointing_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_jointing_behavior_test.dart index e69de29b..674aad3a 100644 --- a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_jointing_behavior_test.dart +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_jointing_behavior_test.dart @@ -0,0 +1,181 @@ +// 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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(Forge2DGame.new); + + group('PlungerJointingBehavior', () { + test('can be instantiated', () { + expect( + PlungerJointingBehavior(compressionDistance: 0), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final parent = Plunger.test(); + final behavior = PlungerJointingBehavior(compressionDistance: 0); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + expect(parent.children, contains(behavior)); + }); + + flameTester.test('can be loaded', (game) async { + final parent = Plunger.test(); + final behavior = PlungerJointingBehavior(compressionDistance: 0); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + expect(parent.children, contains(behavior)); + }); + + flameTester.test('creates a joint', (game) async { + final behavior = PlungerJointingBehavior(compressionDistance: 0); + final parent = Plunger.test(); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + expect(parent.body.joints, isNotEmpty); + }); + }); + +// group('PlungerAnchorPrismaticJointDef', () { +// const compressionDistance = 10.0; +// late Plunger plunger; + +// setUp(() { +// plunger = Plunger( +// compressionDistance: compressionDistance, +// ); +// anchor = PlungerAnchor(plunger: plunger); +// }); + +// group('initializes with', () { +// flameTester.test( +// 'plunger body as bodyA', +// (game) async { +// await game.ensureAdd(plunger); +// await game.ensureAdd(anchor); + +// final jointDef = PlungerAnchorPrismaticJointDef( +// plunger: plunger, +// anchor: anchor, +// ); + +// expect(jointDef.bodyA, equals(plunger.body)); +// }, +// ); + +// flameTester.test( +// 'anchor body as bodyB', +// (game) async { +// await game.ensureAdd(plunger); +// await game.ensureAdd(anchor); + +// final jointDef = PlungerAnchorPrismaticJointDef( +// plunger: plunger, +// anchor: anchor, +// ); +// game.world.createJoint(PrismaticJoint(jointDef)); + +// expect(jointDef.bodyB, equals(anchor.body)); +// }, +// ); + +// flameTester.test( +// 'limits enabled', +// (game) async { +// await game.ensureAdd(plunger); +// await game.ensureAdd(anchor); + +// final jointDef = PlungerAnchorPrismaticJointDef( +// plunger: plunger, +// anchor: anchor, +// ); +// game.world.createJoint(PrismaticJoint(jointDef)); + +// expect(jointDef.enableLimit, isTrue); +// }, +// ); + +// flameTester.test( +// 'lower translation limit as negative infinity', +// (game) async { +// await game.ensureAdd(plunger); +// await game.ensureAdd(anchor); + +// final jointDef = PlungerAnchorPrismaticJointDef( +// plunger: plunger, +// anchor: anchor, +// ); +// game.world.createJoint(PrismaticJoint(jointDef)); + +// expect(jointDef.lowerTranslation, equals(double.negativeInfinity)); +// }, +// ); + +// flameTester.test( +// 'connected body collision enabled', +// (game) async { +// await game.ensureAdd(plunger); +// await game.ensureAdd(anchor); + +// final jointDef = PlungerAnchorPrismaticJointDef( +// plunger: plunger, +// anchor: anchor, +// ); +// game.world.createJoint(PrismaticJoint(jointDef)); + +// expect(jointDef.collideConnected, isTrue); +// }, +// ); +// }); + +// flameTester.testGameWidget( +// 'plunger cannot go below anchor', +// setUp: (game, tester) async { +// await game.ensureAdd(plunger); +// await game.ensureAdd(anchor); + +// // Giving anchor a shape for the plunger to collide with. +// anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1)); + +// final jointDef = PlungerAnchorPrismaticJointDef( +// plunger: plunger, +// anchor: anchor, +// ); +// game.world.createJoint(PrismaticJoint(jointDef)); + +// await tester.pump(const Duration(seconds: 1)); +// }, +// verify: (game, tester) async { +// expect(plunger.body.position.y < anchor.body.position.y, isTrue); +// }, +// ); + +// flameTester.testGameWidget( +// 'plunger cannot excessively exceed starting position', +// setUp: (game, tester) async { +// await game.ensureAdd(plunger); +// await game.ensureAdd(anchor); + +// final jointDef = PlungerAnchorPrismaticJointDef( +// plunger: plunger, +// anchor: anchor, +// ); +// game.world.createJoint(PrismaticJoint(jointDef)); + +// plunger.body.setTransform(Vector2(0, -1), 0); + +// await tester.pump(const Duration(seconds: 1)); +// }, +// verify: (game, tester) async { +// expect(plunger.body.position.y < 1, isTrue); +// }, +// ); +// }); +// } +} diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_key_controlling_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_key_controlling_behavior_test.dart index 773ee35c..5cd03719 100644 --- a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_key_controlling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_key_controlling_behavior_test.dart @@ -1,5 +1,71 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/foundation.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'; + +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() { - group('PlungerKeyControllingBehavior', () {}); + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(Forge2DGame.new); + + group('PlungerKeyControllingBehavior', () { + test('can be instantiated', () { + expect( + PlungerKeyControllingBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final parent = Plunger.test(); + final behavior = PlungerKeyControllingBehavior(); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + expect(parent.children, contains(behavior)); + }); + + group('onKeyEvent', () { + late Plunger plunger; + + setUp(() { + plunger = Plunger.test(); + }); + + flameTester.test( + 'pulls when down arrow is pressed', + (game) async { + final plunger = Plunger.test(); + await game.ensureAdd(plunger); + final behavior = PlungerKeyControllingBehavior(); + await plunger.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowDown, + ); + + behavior.onKeyEvent(event, {}); + + // expect(plunger.body.linearVelocity.y, isPositive); + // expect(plunger.body.linearVelocity.x, isZero); + }, + ); + }); + }); } diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_noise_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_noise_behavior_test.dart index e69de29b..5b067c3d 100644 --- a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_noise_behavior_test.dart +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_noise_behavior_test.dart @@ -0,0 +1,74 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + Component child, { + PinballAudioPlayer? pinballAudioPlayer, + }) { + return ensureAdd( + FlameProvider.value( + pinballAudioPlayer ?? _MockPinballAudioPlayer(), + children: [ + FlameBlocProvider.value( + value: PlungerCubit(), + children: [child], + ), + ], + ), + ); + } +} + +class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + + group('PlungerNoiseBehavior', () { + late PinballAudioPlayer audioPlayer; + + setUp(() { + audioPlayer = _MockPinballAudioPlayer(); + }); + + test('can be instantiated', () { + expect( + PlungerNoiseBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final parent = Component(); + final behavior = PlungerNoiseBehavior(); + await game.pump(parent); + await parent.ensureAdd(behavior); + expect(parent.children, contains(behavior)); + }); + + flameTester.test('plays the correct sound on when released', (game) async { + final parent = Component(); + final behavior = PlungerNoiseBehavior(); + await game.pump( + parent, + pinballAudioPlayer: audioPlayer, + ); + await parent.ensureAdd(behavior); + + behavior.onNewState(PlungerState.releasing); + + verify(() => audioPlayer.play(PinballAudio.launcher)).called(1); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart new file mode 100644 index 00000000..2989024c --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('PlungerPullingBehavior', () { + test('can be instantiated', () { + expect( + PlungerPullingBehavior(strength: 0), + isA(), + ); + }); + }); + + group('PlungerAutoPullingBehavior', () { + test('can be instantiated', () { + expect( + PlungerAutoPullingBehavior(strength: 0), + isA(), + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_releasing_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_releasing_behavior_test.dart new file mode 100644 index 00000000..ec0dea98 --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_releasing_behavior_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PlungerReleasingBehavior', () {}); +} diff --git a/packages/pinball_components/test/src/components/plunger/plunger_test.dart b/packages/pinball_components/test/src/components/plunger/plunger_test.dart index 84701654..7b43a210 100644 --- a/packages/pinball_components/test/src/components/plunger/plunger_test.dart +++ b/packages/pinball_components/test/src/components/plunger/plunger_test.dart @@ -12,8 +12,6 @@ void main() { final flameTester = FlameTester(TestGame.new); group('Plunger', () { - const compressionDistance = 0.0; - test('can be instantiated', () { expect(Plunger(), isA()); expect(Plunger.test(), isA()); @@ -68,22 +66,16 @@ void main() { }); group('fixture', () { - test( - 'exists', - () async { - final body = Plunger().createBody(); - expect(body.fixtures[0], isA()); - }, - ); + test('exists', () async { + final body = Plunger().createBody(); + expect(body.fixtures[0], isA()); + }); - test( - 'has density', - () { - final body = Plunger().createBody(); - final fixture = body.fixtures[0]; - expect(fixture.density, greaterThan(0)); - }, - ); + test('has density', () { + final body = Plunger().createBody(); + final fixture = body.fixtures[0]; + expect(fixture.density, greaterThan(0)); + }); }); group('pullFor', () { @@ -178,162 +170,4 @@ void main() { }); }); - group('PlungerAnchor', () { - const compressionDistance = 10.0; - - flameTester.test( - 'position is a compression distance below the Plunger', - (game) async { - final plunger = Plunger( - compressionDistance: compressionDistance, - ); - await game.ensureAdd(plunger); - - final plungerAnchor = PlungerAnchor(plunger: plunger); - await game.ensureAdd(plungerAnchor); - - expect( - plungerAnchor.body.position.y, - equals(plunger.body.position.y + compressionDistance), - ); - }, - ); - }); - - group('PlungerAnchorPrismaticJointDef', () { - const compressionDistance = 10.0; - late Plunger plunger; - late PlungerAnchor anchor; - - setUp(() { - plunger = Plunger( - compressionDistance: compressionDistance, - ); - anchor = PlungerAnchor(plunger: plunger); - }); - - group('initializes with', () { - flameTester.test( - 'plunger body as bodyA', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - - expect(jointDef.bodyA, equals(plunger.body)); - }, - ); - - flameTester.test( - 'anchor body as bodyB', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - expect(jointDef.bodyB, equals(anchor.body)); - }, - ); - - flameTester.test( - 'limits enabled', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - expect(jointDef.enableLimit, isTrue); - }, - ); - - flameTester.test( - 'lower translation limit as negative infinity', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - expect(jointDef.lowerTranslation, equals(double.negativeInfinity)); - }, - ); - - flameTester.test( - 'connected body collision enabled', - (game) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - expect(jointDef.collideConnected, isTrue); - }, - ); - }); - - flameTester.testGameWidget( - 'plunger cannot go below anchor', - setUp: (game, tester) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - // Giving anchor a shape for the plunger to collide with. - anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1)); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - await tester.pump(const Duration(seconds: 1)); - }, - verify: (game, tester) async { - expect(plunger.body.position.y < anchor.body.position.y, isTrue); - }, - ); - - flameTester.testGameWidget( - 'plunger cannot excessively exceed starting position', - setUp: (game, tester) async { - await game.ensureAdd(plunger); - await game.ensureAdd(anchor); - - final jointDef = PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ); - game.world.createJoint(PrismaticJoint(jointDef)); - - plunger.body.setTransform(Vector2(0, -1), 0); - - await tester.pump(const Duration(seconds: 1)); - }, - verify: (game, tester) async { - expect(plunger.body.position.y < 1, isTrue); - }, - ); - }); -} + diff --git a/packages/pinball_flame/lib/src/shapes/arc_shape.dart b/packages/pinball_flame/lib/src/shapes/arc_shape.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packages/pinball_flame/lib/src/shapes/arc_shape.dart @@ -0,0 +1 @@ + diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index 68bde767..8b137891 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -1,185 +1 @@ -// ignore_for_file: cascade_invocations -import 'dart:collection'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/components.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_audio/pinball_audio.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -import '../../helpers/helpers.dart'; - -class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents { - @override - Future onLoad() async { - images.prefix = ''; - await images.load(Assets.images.plunger.plunger.keyName); - } - - Future pump( - Plunger child, { - GameBloc? gameBloc, - PinballAudioPlayer? pinballAudioPlayer, - }) { - return ensureAdd( - FlameBlocProvider.value( - value: gameBloc ?? GameBloc() - ..add(const GameStarted()), - children: [ - FlameProvider.value( - pinballAudioPlayer ?? _MockPinballAudioPlayer(), - children: [child], - ) - ], - ), - ); - } -} - -class _MockGameBloc extends Mock implements GameBloc {} - -class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(_TestGame.new); - - group('PlungerController', () { - late GameBloc gameBloc; - - final flameBlocTester = FlameTester(_TestGame.new); - - late Plunger plunger; - late PlungerController controller; - - setUp(() { - gameBloc = _MockGameBloc(); - plunger = ControlledPlunger(compressionDistance: 10); - controller = PlungerController(plunger); - plunger.add(controller); - }); - - group('onKeyEvent', () { - final downKeys = UnmodifiableListView([ - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.space, - LogicalKeyboardKey.keyS, - ]); - - testRawKeyDownEvents(downKeys, (event) { - flameTester.test( - 'moves down ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - await game.pump(plunger); - controller.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isPositive); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(downKeys, (event) { - flameTester.test( - 'moves up ' - 'when ${event.logicalKey.keyLabel} is released ' - 'and plunger is below its starting position', - (game) async { - await game.pump(plunger); - plunger.body.setTransform(Vector2(0, 1), 0); - controller.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isNegative); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(downKeys, (event) { - flameTester.test( - 'does not move when ${event.logicalKey.keyLabel} is released ' - 'and plunger is in its starting position', - (game) async { - await game.pump(plunger); - controller.onKeyEvent(event, {}); - - expect(plunger.body.linearVelocity.y, isZero); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyDownEvents(downKeys, (event) { - flameBlocTester.testGameWidget( - 'does nothing when is game over', - setUp: (game, tester) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.gameOver, - ), - ); - - await game.pump(plunger, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - }, - verify: (game, tester) async { - expect(plunger.body.linearVelocity.y, isZero); - expect(plunger.body.linearVelocity.x, isZero); - }, - ); - }); - }); - - flameTester.test( - 'adds the PlungerNoiseBehavior plunger is released', - (game) async { - await game.pump(plunger); - plunger.body.setTransform(Vector2(0, 1), 0); - plunger.release(); - - await game.ready(); - final count = - game.descendants().whereType().length; - expect(count, equals(1)); - }, - ); - }); - - group('PlungerNoiseBehavior', () { - late PinballAudioPlayer audioPlayer; - - setUp(() { - audioPlayer = _MockPinballAudioPlayer(); - }); - - flameTester.test('plays the correct sound on load', (game) async { - final parent = ControlledPlunger(compressionDistance: 10); - await game.pump(parent, pinballAudioPlayer: audioPlayer); - await parent.ensureAdd(PlungerNoiseBehavior()); - verify(() => audioPlayer.play(PinballAudio.launcher)).called(1); - }); - - test('is removed on the first update', () { - final parent = Component(); - final behavior = PlungerNoiseBehavior(); - parent.add(behavior); - parent.update(0); // Run a tick to ensure it is added - - behavior.update(0); // Run its own update where the removal happens - - expect(behavior.shouldRemove, isTrue); - }); - }); -} diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart index 3151e70b..33e40296 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -188,6 +188,33 @@ void main() { }, ); + flameTester.test( + 'removes PlungerKeyControllingBehavior from Plunger', + (game) async { + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox( + leaderboardRepository: repository, + entries: const [], + ); + final plunger = Plunger.test(); + final behavior = PlungerKeyControllingBehavior(); + + await game.pump([component, backbox, plunger]); + await plunger.ensureAdd(behavior); + + expect(state.status, GameStatus.gameOver); + + component.onNewState(state); + await game.ready(); + + expect( + plunger.children.whereType(), + isEmpty, + ); + }, + ); + flameTester.test( 'plays the game over voice over', (game) async {