refactor: implementing plunger behaviors

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

@ -25,6 +25,10 @@ class GameBlocStatusListener extends Component
.descendants()
.whereType<Flipper>()
.forEach(_addFlipperKeyControls);
gameRef
.descendants()
.whereType<Plunger>()
.forEach(_addPlungerKeyControls);
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
@ -41,15 +45,23 @@ class GameBlocStatusListener extends Component
.descendants()
.whereType<Flipper>()
.forEach(_removeFlipperKeyControls);
gameRef
.descendants()
.whereType<Plunger>()
.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()

@ -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<PlungerCubit, PlungerState>(
create: PlungerCubit.new,
children: [Plunger()..initialPosition = Vector2(41, 43.7)],
),
RocketSpriteComponent()..position = Vector2(42.8, 62.3),
],
);

@ -143,9 +143,15 @@ class PinballGame extends PinballForge2DGame
final rocket = descendants().whereType<RocketSpriteComponent>().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<Plunger>().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<FlameBlocProvider<PlungerCubit, PlungerState>>()
.first
.bloc
.pulled();
} else {
final leftSide = info.eventPosition.widget.x < canvasSize.x / 2;
focusedBoardSide[pointerId] =

@ -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';

@ -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<Plunger> {
with KeyboardHandler, FlameBlocReader<PlungerCubit, PlungerState> {
/// 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;

@ -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<PlungerCubit, PlungerState> {
late final PinballAudioPlayer _audioPlayer;
@override
Future<void> onLoad() async {
await super.onLoad();
readProvider<PinballAudioPlayer>().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<void> onLoad() async {
await super.onLoad();
_audioPlayer = readProvider<PinballAudioPlayer>();
}
}

@ -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<Plunger>, FlameBlocReader<PlungerCubit, PlungerState> {
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<PrismaticJoint>().single;
final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit();
if (reachedBottom) {
bloc.released();
}
}
}

@ -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<Plunger>, FlameBlocListenable<PlungerCubit, PlungerState> {
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);
}
}
}

@ -0,0 +1,15 @@
import 'package:bloc/bloc.dart';
part 'plunger_state.dart';
class PlungerCubit extends Cubit<PlungerState> {
PlungerCubit() : super(PlungerState.releasing);
void pulled() {
emit(PlungerState.pulling);
}
void released() {
emit(PlungerState.releasing);
}
}

@ -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;
}

@ -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<PlungerState>
with HasGameRef, FlameBlocListenable<PlungerCubit, PlungerState> {
_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<PlungerCubit, PlungerState>().state;
}
}

@ -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:

@ -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);
});
});

@ -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',

@ -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<PlungerJointingBehavior>(),
);
});
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);
// },
// );
// });
// }
}

@ -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<PlungerKeyControllingBehavior>(),
);
});
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);
},
);
});
});
}

@ -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<void> pump(
Component child, {
PinballAudioPlayer? pinballAudioPlayer,
}) {
return ensureAdd(
FlameProvider<PinballAudioPlayer>.value(
pinballAudioPlayer ?? _MockPinballAudioPlayer(),
children: [
FlameBlocProvider<PlungerCubit, PlungerState>.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<PlungerNoiseBehavior>(),
);
});
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);
});
});
}

@ -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<PlungerPullingBehavior>(),
);
});
});
group('PlungerAutoPullingBehavior', () {
test('can be instantiated', () {
expect(
PlungerAutoPullingBehavior(strength: 0),
isA<PlungerAutoPullingBehavior>(),
);
});
});
}

@ -0,0 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
group('PlungerReleasingBehavior', () {});
}

@ -12,8 +12,6 @@ void main() {
final flameTester = FlameTester(TestGame.new);
group('Plunger', () {
const compressionDistance = 0.0;
test('can be instantiated', () {
expect(Plunger(), isA<Plunger>());
expect(Plunger.test(), isA<Plunger>());
@ -68,22 +66,16 @@ void main() {
});
group('fixture', () {
test(
'exists',
() async {
final body = Plunger().createBody();
expect(body.fixtures[0], isA<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));
},
);
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);
},
);
});
}

@ -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<void> onLoad() async {
images.prefix = '';
await images.load(Assets.images.plunger.plunger.keyName);
}
Future<void> pump(
Plunger child, {
GameBloc? gameBloc,
PinballAudioPlayer? pinballAudioPlayer,
}) {
return ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc ?? GameBloc()
..add(const GameStarted()),
children: [
FlameProvider<PinballAudioPlayer>.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<GameState>.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<PlungerNoiseBehavior>().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);
});
});
}

@ -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<PlungerKeyControllingBehavior>(),
isEmpty,
);
},
);
flameTester.test(
'plays the game over voice over',
(game) async {

Loading…
Cancel
Save