diff --git a/lib/game/components/launcher.dart b/lib/game/components/launcher.dart index 3a609838..99b44a80 100644 --- a/lib/game/components/launcher.dart +++ b/lib/game/components/launcher.dart @@ -1,5 +1,4 @@ import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; /// {@template launcher} @@ -13,10 +12,7 @@ class Launcher extends Component { children: [ LaunchRamp(), Flapper(), - FlameBlocProvider( - create: PlungerCubit.new, - children: [Plunger()..initialPosition = Vector2(41, 43.7)], - ), + Plunger()..initialPosition = Vector2(41, 43.7), RocketSpriteComponent()..position = Vector2(42.8, 62.3), ], ); 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 8f7bb84a..96cb9bd2 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 @@ -9,19 +9,11 @@ import 'package:pinball_flame/pinball_flame.dart'; /// It is attached when the plunger is released. class PlungerNoiseBehavior extends Component with FlameBlocListenable { - late final PinballAudioPlayer _audioPlayer; - @override void onNewState(PlungerState state) { super.onNewState(state); if (state.isReleasing) { - _audioPlayer.play(PinballAudio.launcher); + readProvider().play(PinballAudio.launcher); } } - - @override - 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 index 454653a5..c7188aeb 100644 --- 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 @@ -2,20 +2,27 @@ 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 { + with FlameBlocReader { PlungerPullingBehavior({ required double strength, }) : _strength = strength; final double _strength; + late final Plunger _plunger; + + @override + Future onLoad() async { + await super.onLoad(); + _plunger = parent!.parent! as Plunger; + } + @override void update(double dt) { if (bloc.state.isPulling) { - parent.body.linearVelocity = Vector2(0, _strength); + _plunger.body.linearVelocity = Vector2(0, _strength.abs()); } } } @@ -29,7 +36,7 @@ class PlungerAutoPullingBehavior extends PlungerPullingBehavior { void update(double dt) { super.update(dt); - final joint = parent.body.joints.whereType().single; + final joint = _plunger.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 index bb017732..ac02984f 100644 --- 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 @@ -1,23 +1,30 @@ 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 { + with FlameBlocListenable { PlungerReleasingBehavior({ required double strength, }) : _strength = strength; - final double _strength; // 11 + final double _strength; + + late final Plunger _plunger; + + @override + Future onLoad() async { + await super.onLoad(); + _plunger = parent!.parent! as Plunger; + } @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); + final velocity = (_plunger.initialPosition.y - _plunger.body.position.y) * + _strength.abs(); + _plunger.body.linearVelocity = Vector2(0, velocity); } } } diff --git a/packages/pinball_components/lib/src/components/plunger/plunger.dart b/packages/pinball_components/lib/src/components/plunger/plunger.dart index c15265c6..63c81cff 100644 --- a/packages/pinball_components/lib/src/components/plunger/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger/plunger.dart @@ -12,7 +12,7 @@ export 'cubit/plunger_cubit.dart'; /// [Plunger] serves as a spring, that shoots the ball on the right side of the /// play field. /// -/// [Plunger] ignores gravity so the player controls its downward [pull]. +/// [Plunger] ignores gravity so the player controls its downward movement. /// {@endtemplate} class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { /// {@macro plunger} @@ -20,10 +20,15 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { : super( renderBody: false, children: [ - _PlungerSpriteAnimationGroupComponent(), + FlameBlocProvider( + create: PlungerCubit.new, + children: [ + _PlungerSpriteAnimationGroupComponent(), + PlungerPullingBehavior(strength: 7), + PlungerReleasingBehavior(strength: 11), + ], + ), PlungerJointingBehavior(compressionDistance: 9.2), - PlungerAutoPullingBehavior(strength: 7), - PlungerReleasingBehavior(strength: 11) ], ) { zIndex = ZIndexes.plunger; diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart index cef7b9eb..50af919f 100644 --- a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -1,11 +1,10 @@ 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/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class PlungerGame extends BallGame with KeyboardEvents, Traceable { +class PlungerGame extends BallGame + with HasKeyboardHandlerComponents, Traceable { static const description = ''' Shows how Plunger is rendered. @@ -13,38 +12,16 @@ class PlungerGame extends BallGame with KeyboardEvents, Traceable { - Tap anywhere on the screen to spawn a ball into the game. '''; - static const _downKeys = [ - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.space, - ]; - - late Plunger plunger; - @override Future onLoad() async { await super.onLoad(); final center = screenToWorld(camera.viewport.canvasSize! / 2); - await add( - plunger = Plunger()..initialPosition = Vector2(center.x - 8.8, center.y), - ); - await traceAllBodies(); - } + final plunger = Plunger() + ..initialPosition = Vector2(center.x - 8.8, center.y); + await add(plunger); + await plunger.add(PlungerKeyControllingBehavior()); - @override - KeyEventResult onKeyEvent( - RawKeyEvent event, - Set keysPressed, - ) { - final movedPlungerDown = _downKeys.contains(event.logicalKey); - if (movedPlungerDown) { - if (event is RawKeyDownEvent) { - plunger.pull(); - } else if (event is RawKeyUpEvent) { - plunger.release(); - } - return KeyEventResult.handled; - } - return KeyEventResult.ignored; + await traceAllBodies(); } } diff --git a/packages/pinball_components/test/src/components/golden/plunger/pull.png b/packages/pinball_components/test/src/components/golden/plunger/pull.png index 0ec27a4e..45e104ba 100644 Binary files a/packages/pinball_components/test/src/components/golden/plunger/pull.png and b/packages/pinball_components/test/src/components/golden/plunger/pull.png differ diff --git a/packages/pinball_components/test/src/components/golden/plunger/release.png b/packages/pinball_components/test/src/components/golden/plunger/release.png index 61f7a4d9..d7ce313a 100644 Binary files a/packages/pinball_components/test/src/components/golden/plunger/release.png and b/packages/pinball_components/test/src/components/golden/plunger/release.png differ 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 674aad3a..940ea625 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 @@ -25,14 +25,6 @@ void main() { 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(); @@ -41,141 +33,4 @@ void main() { 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 5cd03719..d415bf7d 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,3 +1,6 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/foundation.dart'; @@ -6,6 +9,22 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; +class _TestGame extends Forge2DGame { + Future pump( + PlungerKeyControllingBehavior child, { + PlungerCubit? plugerBloc, + }) async { + final plunger = Plunger.test(); + await ensureAdd(plunger); + return plunger.ensureAdd( + FlameBlocProvider.value( + value: plugerBloc ?? _MockPlungerCubit(), + children: [child], + ), + ); + } +} + class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { @@ -20,9 +39,11 @@ class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { } } +class _MockPlungerCubit extends Mock implements PlungerCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(Forge2DGame.new); + final flameTester = FlameTester(_TestGame.new); group('PlungerKeyControllingBehavior', () { test('can be instantiated', () { @@ -33,39 +54,141 @@ void main() { }); 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)); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); }); group('onKeyEvent', () { - late Plunger plunger; + late PlungerCubit plungerBloc; setUp(() { - plunger = Plunger.test(); + plungerBloc = _MockPlungerCubit(); }); - 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); + group('pulls when', () { + flameTester.test( + 'down arrow is pressed', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); - final event = _MockRawKeyDownEvent(); - when(() => event.logicalKey).thenReturn( - LogicalKeyboardKey.arrowDown, - ); + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowDown, + ); - behavior.onKeyEvent(event, {}); + behavior.onKeyEvent(event, {}); - // expect(plunger.body.linearVelocity.y, isPositive); - // expect(plunger.body.linearVelocity.x, isZero); - }, - ); + verify(() => plungerBloc.pulled()).called(1); + }, + ); + + flameTester.test( + '"s" is pressed', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyS, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.pulled()).called(1); + }, + ); + + flameTester.test( + 'space is pressed', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.space, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.pulled()).called(1); + }, + ); + }); + + group('releases when', () { + flameTester.test( + 'down arrow is released', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowDown, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.released()).called(1); + }, + ); + + flameTester.test( + '"s" is released', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyS, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.released()).called(1); + }, + ); + + flameTester.test( + 'space is released', + (game) async { + final behavior = PlungerKeyControllingBehavior(); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.space, + ); + + behavior.onKeyEvent(event, {}); + + verify(() => plungerBloc.released()).called(1); + }, + ); + }); }); }); } 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 5b067c3d..a5e11ad0 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 @@ -1,5 +1,8 @@ // ignore_for_file: cascade_invocations +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; @@ -14,13 +17,16 @@ class _TestGame extends Forge2DGame { Future pump( Component child, { PinballAudioPlayer? pinballAudioPlayer, - }) { - return ensureAdd( + PlungerCubit? plungerBloc, + }) async { + final parent = Component(); + await ensureAdd(parent); + return parent.ensureAdd( FlameProvider.value( pinballAudioPlayer ?? _MockPinballAudioPlayer(), children: [ FlameBlocProvider.value( - value: PlungerCubit(), + value: plungerBloc ?? PlungerCubit(), children: [child], ), ], @@ -31,6 +37,8 @@ class _TestGame extends Forge2DGame { class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} +class _MockPlungerCubit extends Mock implements PlungerCubit {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(_TestGame.new); @@ -50,25 +58,34 @@ void main() { }); 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)); + await game.pump(behavior); + expect(game.descendants(), 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); + flameTester.test( + 'plays the correct sound when released', + (game) async { + final plungerBloc = _MockPlungerCubit(); + final streamController = StreamController(); + whenListen( + plungerBloc, + streamController.stream, + initialState: PlungerState.pulling, + ); - behavior.onNewState(PlungerState.releasing); + final behavior = PlungerNoiseBehavior(); + await game.pump( + behavior, + pinballAudioPlayer: audioPlayer, + plungerBloc: plungerBloc, + ); - verify(() => audioPlayer.play(PinballAudio.launcher)).called(1); - }); + streamController.add(PlungerState.releasing); + await Future.delayed(Duration.zero); + + 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 index 2989024c..2fa55233 100644 --- 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 @@ -1,7 +1,39 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; +class _TestGame extends Forge2DGame { + Future pump( + PlungerPullingBehavior behavior, { + PlungerCubit? plugerBloc, + }) async { + final plunger = Plunger.test(); + await ensureAdd(plunger); + return plunger.ensureAdd( + FlameBlocProvider.value( + value: plugerBloc ?? _MockPlungerCubit(), + children: [behavior], + ), + ); + } +} + +class _MockPlungerCubit extends Mock implements PlungerCubit {} + +class _MockPrismaticJoint extends Mock implements PrismaticJoint {} + void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + group('PlungerPullingBehavior', () { test('can be instantiated', () { expect( @@ -9,6 +41,38 @@ void main() { isA(), ); }); + + flameTester.test('can be loaded', (game) async { + final behavior = PlungerPullingBehavior(strength: 0); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); + }); + + flameTester.test( + 'applies vertical linear velocity when pulled', + (game) async { + final plungerBloc = _MockPlungerCubit(); + whenListen( + plungerBloc, + Stream.value(PlungerState.pulling), + initialState: PlungerState.pulling, + ); + + const strength = 2.0; + final behavior = PlungerPullingBehavior( + strength: strength, + ); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); + game.update(0); + + final plunger = behavior.ancestors().whereType().single; + expect(plunger.body.linearVelocity.x, equals(0)); + expect(plunger.body.linearVelocity.y, equals(strength)); + }, + ); }); group('PlungerAutoPullingBehavior', () { @@ -18,5 +82,72 @@ void main() { isA(), ); }); + + flameTester.test('can be loaded', (game) async { + final behavior = PlungerAutoPullingBehavior(strength: 0); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); + }); + + flameTester.test( + "pulls while joint hasn't reached limit", + (game) async { + final plungerBloc = _MockPlungerCubit(); + whenListen( + plungerBloc, + Stream.value(PlungerState.pulling), + initialState: PlungerState.pulling, + ); + + const strength = 2.0; + final behavior = PlungerAutoPullingBehavior( + strength: strength, + ); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); + final plunger = behavior.ancestors().whereType().single; + final joint = _MockPrismaticJoint(); + when(joint.getJointTranslation).thenReturn(2); + when(joint.getLowerLimit).thenReturn(0); + plunger.body.joints.add(joint); + + game.update(0); + + expect(plunger.body.linearVelocity.x, equals(0)); + expect(plunger.body.linearVelocity.y, equals(strength)); + }, + ); + + flameTester.test( + 'releases when joint reaches limit', + (game) async { + final plungerBloc = _MockPlungerCubit(); + whenListen( + plungerBloc, + Stream.value(PlungerState.pulling), + initialState: PlungerState.pulling, + ); + + const strength = 2.0; + final behavior = PlungerAutoPullingBehavior( + strength: strength, + ); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); + final plunger = behavior.ancestors().whereType().single; + final joint = _MockPrismaticJoint(); + when(joint.getJointTranslation).thenReturn(0); + when(joint.getLowerLimit).thenReturn(0); + plunger.body.joints.add(joint); + + game.update(0); + + verify(plungerBloc.released).called(1); + }, + ); }); } 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 index ec0dea98..71f3132a 100644 --- 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 @@ -1,5 +1,72 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/forge2d_game.dart'; +import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + PlungerReleasingBehavior behavior, { + PlungerCubit? plugerBloc, + }) async { + final plunger = Plunger.test(); + await ensureAdd(plunger); + return plunger.ensureAdd( + FlameBlocProvider.value( + value: plugerBloc ?? PlungerCubit(), + children: [behavior], + ), + ); + } +} + +class _MockPlungerCubit extends Mock implements PlungerCubit {} void main() { - group('PlungerReleasingBehavior', () {}); + group('PlungerReleasingBehavior', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(_TestGame.new); + + test('can be instantiated', () { + expect( + PlungerReleasingBehavior(strength: 0), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final behavior = PlungerReleasingBehavior(strength: 0); + await game.pump(behavior); + expect(game.descendants(), contains(behavior)); + }); + + flameTester.test('applies vertical linear velocity', (game) async { + final plungerBloc = _MockPlungerCubit(); + final streamController = StreamController(); + whenListen( + plungerBloc, + streamController.stream, + initialState: PlungerState.pulling, + ); + + final behavior = PlungerReleasingBehavior(strength: 2); + await game.pump( + behavior, + plugerBloc: plungerBloc, + ); + + streamController.add(PlungerState.releasing); + await Future.delayed(Duration.zero); + + final plunger = behavior.ancestors().whereType().single; + expect(plunger.body.linearVelocity.x, equals(0)); + expect(plunger.body.linearVelocity.y, isNot(greaterThan(0))); + }); + }); } 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 7b43a210..f4b5c446 100644 --- a/packages/pinball_components/test/src/components/plunger/plunger_test.dart +++ b/packages/pinball_components/test/src/components/plunger/plunger_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -14,7 +15,6 @@ void main() { group('Plunger', () { test('can be instantiated', () { expect(Plunger(), isA()); - expect(Plunger.test(), isA()); }); flameTester.test( @@ -26,148 +26,78 @@ void main() { }, ); - flameTester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - await game.ensureAdd(Plunger()); - game.camera.followVector2(Vector2.zero()); - game.camera.zoom = 4.1; - }, - verify: (game, tester) async { - final plunger = game.descendants().whereType().first; - plunger.pull(); - game.update(1); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/plunger/pull.png'), - ); - - plunger.release(); - game.update(1); - await tester.pump(); - await expectLater( - find.byGame(), - matchesGoldenFile('golden/plunger/release.png'), - ); - }, - ); - - group('body', () { - test('is dynamic', () { - final body = Plunger().createBody(); - expect(body.bodyType, equals(BodyType.dynamic)); - }); - - test('ignores gravity', () { - final body = Plunger().createBody(); - expect(body.gravityScale, equals(Vector2.zero())); - }); - }); - - group('fixture', () { - 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)); - }); - }); - - group('pullFor', () { - late Plunger plunger; - - setUp(() { - plunger = Plunger(); - }); - + group('renders correctly', () { + const goldenPath = '../golden/plunger/'; flameTester.testGameWidget( - 'moves downwards for given period when pullFor is called', + 'pulling', setUp: (game, tester) async { - await game.ensureAdd(plunger); + await game.ensureAdd(Plunger()); + game.camera.followVector2(Vector2.zero()); + game.camera.zoom = 4.1; }, verify: (game, tester) async { - plunger.pullFor(2); - game.update(0); - - expect(plunger.body.linearVelocity.y, isPositive); - - // Call game update at 120 FPS, so that the plunger will act as if it - // was pulled for 2 seconds. - for (var i = 0.0; i < 2; i += 1 / 120) { - game.update(1 / 20); - } - - expect(plunger.body.linearVelocity.y, isZero); + final plunger = game.descendants().whereType().first; + final bloc = plunger + .descendants() + .whereType>() + .single + .bloc; + bloc.pulled(); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenPath}pull.png'), + ); }, ); - }); - - group('pull', () { - late Plunger plunger; - - setUp(() { - plunger = Plunger(); - }); - flameTester.test( - 'moves downwards when pull is called', - (game) async { - await game.ensureAdd(plunger); - plunger.pull(); - - expect(plunger.body.linearVelocity.y, isPositive); - expect(plunger.body.linearVelocity.x, isZero); + flameTester.testGameWidget( + 'releasing', + setUp: (game, tester) async { + await game.ensureAdd(Plunger()); + game.camera.followVector2(Vector2.zero()); + game.camera.zoom = 4.1; }, - ); - - flameTester.test( - 'moves downwards when pull is called ' - 'and plunger is below its starting position', (game) async { - await game.ensureAdd(plunger); - plunger.pull(); - plunger.release(); - plunger.pull(); - - expect(plunger.body.linearVelocity.y, isPositive); - expect(plunger.body.linearVelocity.x, isZero); - }); - }); - - group('release', () { - late Plunger plunger; - - setUp(() { - plunger = Plunger(); - }); - - flameTester.test( - 'moves upwards when release is called ' - 'and plunger is below its starting position', (game) async { - await game.ensureAdd(plunger); - plunger.body.setTransform(Vector2(0, 1), 0); - plunger.release(); - - expect(plunger.body.linearVelocity.y, isNegative); - expect(plunger.body.linearVelocity.x, isZero); - }); - - flameTester.test( - 'does not move when release is called ' - 'and plunger is in its starting position', - (game) async { - await game.ensureAdd(plunger); - plunger.release(); - - expect(plunger.body.linearVelocity.y, isZero); - expect(plunger.body.linearVelocity.x, isZero); + verify: (game, tester) async { + final plunger = game.descendants().whereType().first; + final bloc = plunger + .descendants() + .whereType>() + .single + .bloc; + bloc.released(); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenPath}release.png'), + ); }, ); }); - }); - + // group('body', () { + // test('is dynamic', () { + // final body = Plunger.test().createBody(); + // expect(body.bodyType, equals(BodyType.dynamic)); + // }); + + // test('ignores gravity', () { + // final body = Plunger().createBody(); + // expect(body.gravityScale, equals(Vector2.zero())); + // }); + // }); + + // group('fixture', () { + // 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)); + // }); + // }); + }); +}