feat: tested plunger behaviors

pull/434/head
alestiago 3 years ago
parent 552bf040c4
commit 73e9dc5fbc

@ -1,5 +1,4 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template launcher} /// {@template launcher}
@ -13,10 +12,7 @@ class Launcher extends Component {
children: [ children: [
LaunchRamp(), LaunchRamp(),
Flapper(), Flapper(),
FlameBlocProvider<PlungerCubit, PlungerState>( Plunger()..initialPosition = Vector2(41, 43.7),
create: PlungerCubit.new,
children: [Plunger()..initialPosition = Vector2(41, 43.7)],
),
RocketSpriteComponent()..position = Vector2(42.8, 62.3), RocketSpriteComponent()..position = Vector2(42.8, 62.3),
], ],
); );

@ -9,19 +9,11 @@ import 'package:pinball_flame/pinball_flame.dart';
/// It is attached when the plunger is released. /// It is attached when the plunger is released.
class PlungerNoiseBehavior extends Component class PlungerNoiseBehavior extends Component
with FlameBlocListenable<PlungerCubit, PlungerState> { with FlameBlocListenable<PlungerCubit, PlungerState> {
late final PinballAudioPlayer _audioPlayer;
@override @override
void onNewState(PlungerState state) { void onNewState(PlungerState state) {
super.onNewState(state); super.onNewState(state);
if (state.isReleasing) { if (state.isReleasing) {
_audioPlayer.play(PinballAudio.launcher); readProvider<PinballAudioPlayer>().play(PinballAudio.launcher);
} }
} }
@override
Future<void> onLoad() async {
await super.onLoad();
_audioPlayer = readProvider<PinballAudioPlayer>();
}
} }

@ -2,20 +2,27 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class PlungerPullingBehavior extends Component class PlungerPullingBehavior extends Component
with ParentIsA<Plunger>, FlameBlocReader<PlungerCubit, PlungerState> { with FlameBlocReader<PlungerCubit, PlungerState> {
PlungerPullingBehavior({ PlungerPullingBehavior({
required double strength, required double strength,
}) : _strength = strength; }) : _strength = strength;
final double _strength; final double _strength;
late final Plunger _plunger;
@override
Future<void> onLoad() async {
await super.onLoad();
_plunger = parent!.parent! as Plunger;
}
@override @override
void update(double dt) { void update(double dt) {
if (bloc.state.isPulling) { 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) { void update(double dt) {
super.update(dt); super.update(dt);
final joint = parent.body.joints.whereType<PrismaticJoint>().single; final joint = _plunger.body.joints.whereType<PrismaticJoint>().single;
final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit(); final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit();
if (reachedBottom) { if (reachedBottom) {
bloc.released(); bloc.released();

@ -1,23 +1,30 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class PlungerReleasingBehavior extends Component class PlungerReleasingBehavior extends Component
with ParentIsA<Plunger>, FlameBlocListenable<PlungerCubit, PlungerState> { with FlameBlocListenable<PlungerCubit, PlungerState> {
PlungerReleasingBehavior({ PlungerReleasingBehavior({
required double strength, required double strength,
}) : _strength = strength; }) : _strength = strength;
final double _strength; // 11 final double _strength;
late final Plunger _plunger;
@override
Future<void> onLoad() async {
await super.onLoad();
_plunger = parent!.parent! as Plunger;
}
@override @override
void onNewState(PlungerState state) { void onNewState(PlungerState state) {
super.onNewState(state); super.onNewState(state);
if (state.isReleasing) { if (state.isReleasing) {
final velocity = final velocity = (_plunger.initialPosition.y - _plunger.body.position.y) *
(parent.initialPosition.y - parent.body.position.y) * _strength; _strength.abs();
parent.body.linearVelocity = Vector2(0, velocity); _plunger.body.linearVelocity = Vector2(0, velocity);
} }
} }
} }

@ -12,7 +12,7 @@ export 'cubit/plunger_cubit.dart';
/// [Plunger] serves as a spring, that shoots the ball on the right side of the /// [Plunger] serves as a spring, that shoots the ball on the right side of the
/// play field. /// play field.
/// ///
/// [Plunger] ignores gravity so the player controls its downward [pull]. /// [Plunger] ignores gravity so the player controls its downward movement.
/// {@endtemplate} /// {@endtemplate}
class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger} /// {@macro plunger}
@ -20,10 +20,15 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
: super( : super(
renderBody: false, renderBody: false,
children: [ children: [
_PlungerSpriteAnimationGroupComponent(), FlameBlocProvider<PlungerCubit, PlungerState>(
create: PlungerCubit.new,
children: [
_PlungerSpriteAnimationGroupComponent(),
PlungerPullingBehavior(strength: 7),
PlungerReleasingBehavior(strength: 11),
],
),
PlungerJointingBehavior(compressionDistance: 9.2), PlungerJointingBehavior(compressionDistance: 9.2),
PlungerAutoPullingBehavior(strength: 7),
PlungerReleasingBehavior(strength: 11)
], ],
) { ) {
zIndex = ZIndexes.plunger; zIndex = ZIndexes.plunger;

@ -1,11 +1,10 @@
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.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 = ''' static const description = '''
Shows how Plunger is rendered. 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. - Tap anywhere on the screen to spawn a ball into the game.
'''; ''';
static const _downKeys = [
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
];
late Plunger plunger;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2); final center = screenToWorld(camera.viewport.canvasSize! / 2);
await add( final plunger = Plunger()
plunger = Plunger()..initialPosition = Vector2(center.x - 8.8, center.y), ..initialPosition = Vector2(center.x - 8.8, center.y);
); await add(plunger);
await traceAllBodies(); await plunger.add(PlungerKeyControllingBehavior());
}
@override await traceAllBodies();
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> 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;
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

@ -25,14 +25,6 @@ void main() {
expect(parent.children, contains(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 { flameTester.test('creates a joint', (game) async {
final behavior = PlungerJointingBehavior(compressionDistance: 0); final behavior = PlungerJointingBehavior(compressionDistance: 0);
final parent = Plunger.test(); final parent = Plunger.test();
@ -41,141 +33,4 @@ void main() {
expect(parent.body.joints, isNotEmpty); 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);
// },
// );
// });
// }
} }

@ -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_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -6,6 +9,22 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
PlungerKeyControllingBehavior child, {
PlungerCubit? plugerBloc,
}) async {
final plunger = Plunger.test();
await ensureAdd(plunger);
return plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plugerBloc ?? _MockPlungerCubit(),
children: [child],
),
);
}
}
class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
@ -20,9 +39,11 @@ class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
} }
} }
class _MockPlungerCubit extends Mock implements PlungerCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new); final flameTester = FlameTester(_TestGame.new);
group('PlungerKeyControllingBehavior', () { group('PlungerKeyControllingBehavior', () {
test('can be instantiated', () { test('can be instantiated', () {
@ -33,39 +54,141 @@ void main() {
}); });
flameTester.test('can be loaded', (game) async { flameTester.test('can be loaded', (game) async {
final parent = Plunger.test();
final behavior = PlungerKeyControllingBehavior(); final behavior = PlungerKeyControllingBehavior();
await game.ensureAdd(parent); await game.pump(behavior);
await parent.ensureAdd(behavior); expect(game.descendants(), contains(behavior));
expect(parent.children, contains(behavior));
}); });
group('onKeyEvent', () { group('onKeyEvent', () {
late Plunger plunger; late PlungerCubit plungerBloc;
setUp(() { setUp(() {
plunger = Plunger.test(); plungerBloc = _MockPlungerCubit();
}); });
flameTester.test( group('pulls when', () {
'pulls when down arrow is pressed', flameTester.test(
(game) async { 'down arrow is pressed',
final plunger = Plunger.test(); (game) async {
await game.ensureAdd(plunger); final behavior = PlungerKeyControllingBehavior();
final behavior = PlungerKeyControllingBehavior(); await game.pump(
await plunger.ensureAdd(behavior); behavior,
plugerBloc: plungerBloc,
);
final event = _MockRawKeyDownEvent(); final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn( when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowDown,
); );
behavior.onKeyEvent(event, {}); behavior.onKeyEvent(event, {});
// expect(plunger.body.linearVelocity.y, isPositive); verify(() => plungerBloc.pulled()).called(1);
// expect(plunger.body.linearVelocity.x, isZero); },
}, );
);
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);
},
);
});
}); });
}); });
} }

@ -1,5 +1,8 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
@ -14,13 +17,16 @@ class _TestGame extends Forge2DGame {
Future<void> pump( Future<void> pump(
Component child, { Component child, {
PinballAudioPlayer? pinballAudioPlayer, PinballAudioPlayer? pinballAudioPlayer,
}) { PlungerCubit? plungerBloc,
return ensureAdd( }) async {
final parent = Component();
await ensureAdd(parent);
return parent.ensureAdd(
FlameProvider<PinballAudioPlayer>.value( FlameProvider<PinballAudioPlayer>.value(
pinballAudioPlayer ?? _MockPinballAudioPlayer(), pinballAudioPlayer ?? _MockPinballAudioPlayer(),
children: [ children: [
FlameBlocProvider<PlungerCubit, PlungerState>.value( FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: PlungerCubit(), value: plungerBloc ?? PlungerCubit(),
children: [child], children: [child],
), ),
], ],
@ -31,6 +37,8 @@ class _TestGame extends Forge2DGame {
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new); final flameTester = FlameTester(_TestGame.new);
@ -50,25 +58,34 @@ void main() {
}); });
flameTester.test('can be loaded', (game) async { flameTester.test('can be loaded', (game) async {
final parent = Component();
final behavior = PlungerNoiseBehavior(); final behavior = PlungerNoiseBehavior();
await game.pump(parent); await game.pump(behavior);
await parent.ensureAdd(behavior); expect(game.descendants(), contains(behavior));
expect(parent.children, contains(behavior));
}); });
flameTester.test('plays the correct sound on when released', (game) async { flameTester.test(
final parent = Component(); 'plays the correct sound when released',
final behavior = PlungerNoiseBehavior(); (game) async {
await game.pump( final plungerBloc = _MockPlungerCubit();
parent, final streamController = StreamController<PlungerState>();
pinballAudioPlayer: audioPlayer, whenListen<PlungerState>(
); plungerBloc,
await parent.ensureAdd(behavior); 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<void>.delayed(Duration.zero);
verify(() => audioPlayer.play(PinballAudio.launcher)).called(1);
},
);
}); });
} }

@ -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:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
PlungerPullingBehavior behavior, {
PlungerCubit? plugerBloc,
}) async {
final plunger = Plunger.test();
await ensureAdd(plunger);
return plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plugerBloc ?? _MockPlungerCubit(),
children: [behavior],
),
);
}
}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
class _MockPrismaticJoint extends Mock implements PrismaticJoint {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('PlungerPullingBehavior', () { group('PlungerPullingBehavior', () {
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
@ -9,6 +41,38 @@ void main() {
isA<PlungerPullingBehavior>(), isA<PlungerPullingBehavior>(),
); );
}); });
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<PlungerState>(
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<Plunger>().single;
expect(plunger.body.linearVelocity.x, equals(0));
expect(plunger.body.linearVelocity.y, equals(strength));
},
);
}); });
group('PlungerAutoPullingBehavior', () { group('PlungerAutoPullingBehavior', () {
@ -18,5 +82,72 @@ void main() {
isA<PlungerAutoPullingBehavior>(), isA<PlungerAutoPullingBehavior>(),
); );
}); });
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<PlungerState>(
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<Plunger>().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<PlungerState>(
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<Plunger>().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);
},
);
}); });
} }

@ -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:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
PlungerReleasingBehavior behavior, {
PlungerCubit? plugerBloc,
}) async {
final plunger = Plunger.test();
await ensureAdd(plunger);
return plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plugerBloc ?? PlungerCubit(),
children: [behavior],
),
);
}
}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
void main() { void main() {
group('PlungerReleasingBehavior', () {}); group('PlungerReleasingBehavior', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
test('can be instantiated', () {
expect(
PlungerReleasingBehavior(strength: 0),
isA<PlungerReleasingBehavior>(),
);
});
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<PlungerState>();
whenListen<PlungerState>(
plungerBloc,
streamController.stream,
initialState: PlungerState.pulling,
);
final behavior = PlungerReleasingBehavior(strength: 2);
await game.pump(
behavior,
plugerBloc: plungerBloc,
);
streamController.add(PlungerState.releasing);
await Future<void>.delayed(Duration.zero);
final plunger = behavior.ancestors().whereType<Plunger>().single;
expect(plunger.body.linearVelocity.x, equals(0));
expect(plunger.body.linearVelocity.y, isNot(greaterThan(0)));
});
});
} }

@ -1,5 +1,6 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -14,7 +15,6 @@ void main() {
group('Plunger', () { group('Plunger', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(Plunger(), isA<Plunger>()); expect(Plunger(), isA<Plunger>());
expect(Plunger.test(), isA<Plunger>());
}); });
flameTester.test( flameTester.test(
@ -26,148 +26,78 @@ void main() {
}, },
); );
flameTester.testGameWidget( group('renders correctly', () {
'renders correctly', const goldenPath = '../golden/plunger/';
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<Plunger>().first;
plunger.pull();
game.update(1);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/plunger/pull.png'),
);
plunger.release();
game.update(1);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
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<Fixture>());
});
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();
});
flameTester.testGameWidget( flameTester.testGameWidget(
'moves downwards for given period when pullFor is called', 'pulling',
setUp: (game, tester) async { 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 { verify: (game, tester) async {
plunger.pullFor(2); final plunger = game.descendants().whereType<Plunger>().first;
game.update(0); final bloc = plunger
.descendants()
expect(plunger.body.linearVelocity.y, isPositive); .whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
.single
// Call game update at 120 FPS, so that the plunger will act as if it .bloc;
// was pulled for 2 seconds. bloc.pulled();
for (var i = 0.0; i < 2; i += 1 / 120) { await tester.pump();
game.update(1 / 20); await expectLater(
} find.byGame<TestGame>(),
matchesGoldenFile('${goldenPath}pull.png'),
expect(plunger.body.linearVelocity.y, isZero); );
}, },
); );
});
group('pull', () {
late Plunger plunger;
setUp(() {
plunger = Plunger();
});
flameTester.test( flameTester.testGameWidget(
'moves downwards when pull is called', 'releasing',
(game) async { setUp: (game, tester) async {
await game.ensureAdd(plunger); await game.ensureAdd(Plunger());
plunger.pull(); game.camera.followVector2(Vector2.zero());
game.camera.zoom = 4.1;
expect(plunger.body.linearVelocity.y, isPositive);
expect(plunger.body.linearVelocity.x, isZero);
}, },
); verify: (game, tester) async {
final plunger = game.descendants().whereType<Plunger>().first;
flameTester.test( final bloc = plunger
'moves downwards when pull is called ' .descendants()
'and plunger is below its starting position', (game) async { .whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
await game.ensureAdd(plunger); .single
plunger.pull(); .bloc;
plunger.release(); bloc.released();
plunger.pull(); await tester.pump();
await expectLater(
expect(plunger.body.linearVelocity.y, isPositive); find.byGame<TestGame>(),
expect(plunger.body.linearVelocity.x, isZero); matchesGoldenFile('${goldenPath}release.png'),
}); );
});
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);
}, },
); );
}); });
});
// 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<Fixture>());
// });
// test('has density', () {
// final body = Plunger().createBody();
// final fixture = body.fixtures[0];
// expect(fixture.density, greaterThan(0));
// });
// });
});
}

Loading…
Cancel
Save