refactor: implemented `Plunger` behaviors (#434)

* feat: defined Plunger behaviors

* refactor: removed ComponentController

* refactor: implementing plunger behaviors

* feat: tested plunger behaviors

* feat: applied Plunger behaviours depending on platfotm

* refactor: fixed typos

* test: updated tap

* refactor: removed key_testers

* refactor: PR typos

* test: added strength assertions

* test: updated goldens

* refactor: renamed methods

* refactor: fixed typo

* refactor: removed dead file
pull/439/head
Alejandro Santiago 2 years ago committed by GitHub
parent 11c076c386
commit 461471b01f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart';
export 'bottom_group.dart';
export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart';
export 'drain/drain.dart';
export 'flutter_forest/flutter_forest.dart';

@ -1,76 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template controlled_plunger}
/// A [Plunger] with a [PlungerController] attached.
/// {@endtemplate}
class ControlledPlunger extends Plunger with Controls<PlungerController> {
/// {@macro controlled_plunger}
ControlledPlunger({required double compressionDistance})
: super(compressionDistance: compressionDistance) {
controller = PlungerController(this);
}
@override
void release() {
super.release();
add(PlungerNoiseBehavior());
}
}
/// A behavior attached to the plunger when it launches the ball which plays the
/// related sound effects.
class PlungerNoiseBehavior extends Component {
@override
Future<void> onLoad() async {
await super.onLoad();
readProvider<PinballAudioPlayer>().play(PinballAudio.launcher);
}
@override
void update(double dt) {
super.update(dt);
removeFromParent();
}
}
/// {@template plunger_controller}
/// A [ComponentController] that controls a [Plunger]s movement.
/// {@endtemplate}
class PlungerController extends ComponentController<Plunger>
with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro plunger_controller}
PlungerController(Plunger plunger) : super(plunger);
/// The [LogicalKeyboardKey]s that will control the [Flipper].
///
/// [onKeyEvent] method listens to when one of these keys is pressed.
static const List<LogicalKeyboardKey> _keys = [
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyS,
];
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (bloc.state.status.isGameOver) return true;
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
component.pull();
} else if (event is RawKeyUpEvent) {
component.release();
}
return false;
}
}

@ -5,6 +5,7 @@ import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:platform_helper/platform_helper.dart';
/// Listens to the [GameBloc] and updates the game accordingly.
class GameBlocStatusListener extends Component
@ -24,7 +25,11 @@ class GameBlocStatusListener extends Component
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_addFlipperKeyControls);
.forEach(_addFlipperBehaviors);
gameRef
.descendants()
.whereType<Plunger>()
.forEach(_addPlungerBehaviors);
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
@ -40,18 +45,51 @@ class GameBlocStatusListener extends Component
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_removeFlipperKeyControls);
.forEach(_removeFlipperBehaviors);
gameRef
.descendants()
.whereType<Plunger>()
.forEach(_removePlungerBehaviors);
break;
}
}
void _addFlipperKeyControls(Flipper flipper) {
flipper
..add(FlipperKeyControllingBehavior())
..moveDown();
void _addPlungerBehaviors(Plunger plunger) {
final platformHelper = readProvider<PlatformHelper>();
const pullingStrength = 7.0;
final provider =
plunger.firstChild<FlameBlocProvider<PlungerCubit, PlungerState>>()!;
if (platformHelper.isMobile) {
provider.add(
PlungerAutoPullingBehavior(strength: pullingStrength),
);
} else {
provider.addAll(
[
PlungerKeyControllingBehavior(),
PlungerPullingBehavior(strength: pullingStrength),
],
);
}
}
void _removePlungerBehaviors(Plunger plunger) {
plunger
.descendants()
.whereType<PlungerPullingBehavior>()
.forEach(plunger.remove);
plunger
.descendants()
.whereType<PlungerKeyControllingBehavior>()
.forEach(plunger.remove);
}
void _removeFlipperKeyControls(Flipper flipper) => flipper
void _addFlipperBehaviors(Flipper flipper) => flipper
..add(FlipperKeyControllingBehavior())
..moveDown();
void _removeFlipperBehaviors(Flipper flipper) => flipper
.descendants()
.whereType<FlipperKeyControllingBehavior>()
.forEach(flipper.remove);

@ -1,5 +1,4 @@
import 'package:flame/components.dart';
import 'package:pinball/game/components/components.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template launcher}
@ -13,8 +12,7 @@ class Launcher extends Component {
children: [
LaunchRamp(),
Flapper(),
ControlledPlunger(compressionDistance: 9.2)
..initialPosition = Vector2(41, 43.7),
Plunger()..initialPosition = Vector2(41, 43.7),
RocketSpriteComponent()..position = Vector2(42.8, 62.3),
],
);

@ -156,9 +156,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] =

@ -7,7 +7,9 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@endtemplate}
class BumpingBehavior extends ContactBehavior {
/// {@macro bumping_behavior}
BumpingBehavior({required double strength}) : _strength = strength;
BumpingBehavior({required double strength})
: assert(strength >= 0, "Strength can't be negative."),
_strength = strength;
/// Determines how strong the bump is.
final double _strength;

@ -27,7 +27,7 @@ export 'launch_ramp.dart';
export 'layer_sensor/layer_sensor.dart';
export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart';
export 'plunger/plunger.dart';
export 'rocket.dart';
export 'score_component/score_component.dart';
export 'signpost/signpost.dart';

@ -14,7 +14,21 @@ class FlipperKeyControllingBehavior extends Component
@override
Future<void> onLoad() async {
await super.onLoad();
_keys = parent.side.flipperKeys;
switch (parent.side) {
case BoardSide.left:
_keys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
];
break;
case BoardSide.right:
_keys = [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
];
break;
}
}
@override
@ -33,20 +47,3 @@ class FlipperKeyControllingBehavior extends Component
return false;
}
}
extension on BoardSide {
List<LogicalKeyboardKey> get flipperKeys {
switch (this) {
case BoardSide.left:
return [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
];
case BoardSide.right:
return [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
];
}
}
}

@ -1,251 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template plunger}
/// [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].
/// {@endtemplate}
class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger}
Plunger({
required this.compressionDistance,
}) : super(
renderBody: false,
children: [_PlungerSpriteAnimationGroupComponent()],
) {
zIndex = ZIndexes.plunger;
layer = Layer.launcher;
}
/// Creates a [Plunger] without any children.
///
/// This can be used for testing [Plunger]'s behaviors in isolation.
@visibleForTesting
Plunger.test({required this.compressionDistance});
/// Distance the plunger can lower.
final double compressionDistance;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final leftShapeVertices = [
Vector2(0, 0),
Vector2(-1.8, 0),
Vector2(-1.8, -2.2),
Vector2(0, -0.3),
]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle))
.toList();
final leftTriangleShape = PolygonShape()..set(leftShapeVertices);
final leftTriangleFixtureDef = FixtureDef(leftTriangleShape)..density = 80;
fixturesDef.add(leftTriangleFixtureDef);
final rightShapeVertices = [
Vector2(0, 0),
Vector2(1.8, 0),
Vector2(1.8, -2.2),
Vector2(0, -0.3),
]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle))
.toList();
final rightTriangleShape = PolygonShape()..set(rightShapeVertices);
final rightTriangleFixtureDef = FixtureDef(rightTriangleShape)
..density = 80;
fixturesDef.add(rightTriangleFixtureDef);
return fixturesDef;
}
@override
Body createBody() {
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
type: BodyType.dynamic,
gravityScale: Vector2.zero(),
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
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() {
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);
}
/// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical
/// motion.
Future<void> _anchorToJoint() async {
final anchor = PlungerAnchor(plunger: this);
await add(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: this,
anchor: anchor,
);
world.createJoint(
PrismaticJoint(jointDef)..setLimits(-compressionDistance, 0),
);
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
}
}
/// 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 {
_PlungerSpriteAnimationGroupComponent()
: super(
anchor: Anchor.center,
position: Vector2(1.87, 14.9),
);
void pull() {
if (current != _PlungerAnimationState.pull) {
animation?.reset();
}
current = _PlungerAnimationState.pull;
}
void release() {
if (current != _PlungerAnimationState.release) {
animation?.reset();
}
current = _PlungerAnimationState.release;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.plunger.plunger.keyName,
);
const amountPerRow = 20;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
final pullAnimation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn ~/ 2,
amountPerRow: amountPerRow ~/ 2,
stepTime: 1 / 24,
textureSize: textureSize,
texturePosition: Vector2.zero(),
loop: false,
),
);
animations = {
_PlungerAnimationState.release: pullAnimation.reversed(),
_PlungerAnimationState.pull: pullAnimation,
};
current = _PlungerAnimationState.release;
}
}
/// {@template plunger_anchor}
/// [JointAnchor] positioned below a [Plunger].
/// {@endtemplate}
class PlungerAnchor extends JointAnchor {
/// {@macro plunger_anchor}
PlungerAnchor({
required Plunger plunger,
}) {
initialPosition = Vector2(
0,
plunger.compressionDistance,
);
}
}
/// {@template plunger_anchor_prismatic_joint_def}
/// [PrismaticJointDef] between a [Plunger] and an [JointAnchor] with motion on
/// the vertical axis.
///
/// The [Plunger] is constrained vertically between its starting position and
/// the [JointAnchor]. The [JointAnchor] must be below the [Plunger].
/// {@endtemplate}
class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
/// {@macro plunger_anchor_prismatic_joint_def}
PlungerAnchorPrismaticJointDef({
required Plunger plunger,
required PlungerAnchor anchor,
}) {
initialize(
plunger.body,
anchor.body,
plunger.body.position + anchor.body.position,
Vector2(16, BoardDimensions.bounds.height),
);
enableLimit = true;
lowerTranslation = double.negativeInfinity;
enableMotor = true;
motorSpeed = 1000;
maxMotorForce = motorSpeed;
collideConnected = true;
}
}

@ -0,0 +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';

@ -0,0 +1,54 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class PlungerJointingBehavior extends Component with ParentIsA<Plunger> {
PlungerJointingBehavior({required double compressionDistance})
: _compressionDistance = compressionDistance;
final double _compressionDistance;
@override
Future<void> onLoad() async {
await super.onLoad();
final anchor = JointAnchor()
..initialPosition = Vector2(0, _compressionDistance);
await add(anchor);
final jointDef = _PlungerAnchorPrismaticJointDef(
plunger: parent,
anchor: anchor,
);
parent.world.createJoint(
PrismaticJoint(jointDef)..setLimits(-_compressionDistance, 0),
);
}
}
/// [PrismaticJointDef] between a [Plunger] and an [JointAnchor] with motion on
/// the vertical axis.
///
/// The [Plunger] is constrained vertically between its starting position and
/// the [JointAnchor]. The [JointAnchor] must be below the [Plunger].
class _PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
/// {@macro plunger_anchor_prismatic_joint_def}
_PlungerAnchorPrismaticJointDef({
required Plunger plunger,
required BodyComponent anchor,
}) {
initialize(
plunger.body,
anchor.body,
plunger.body.position + anchor.body.position,
Vector2(16, BoardDimensions.bounds.height),
);
enableLimit = true;
lowerTranslation = double.negativeInfinity;
enableMotor = true;
motorSpeed = 1000;
maxMotorForce = motorSpeed;
collideConnected = true;
}
}

@ -0,0 +1,33 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
/// Allows controlling the [Plunger]'s movement with keyboard input.
class PlungerKeyControllingBehavior extends Component
with KeyboardHandler, FlameBlocReader<PlungerCubit, PlungerState> {
/// The [LogicalKeyboardKey]s that will control the [Plunger].
///
/// [onKeyEvent] method listens to when one of these keys is pressed.
static const List<LogicalKeyboardKey> _keys = [
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyS,
];
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
bloc.pulled();
} else if (event is RawKeyUpEvent) {
bloc.released();
}
return false;
}
}

@ -0,0 +1,19 @@
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
with FlameBlocListenable<PlungerCubit, PlungerState> {
@override
void onNewState(PlungerState state) {
super.onNewState(state);
if (state.isReleasing) {
readProvider<PinballAudioPlayer>().play(PinballAudio.launcher);
}
}
}

@ -0,0 +1,46 @@
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';
class PlungerPullingBehavior extends Component
with FlameBlocReader<PlungerCubit, PlungerState> {
PlungerPullingBehavior({
required double strength,
}) : assert(strength >= 0, "Strength can't be negative."),
_strength = strength;
final double _strength;
late final Plunger _plunger;
@override
Future<void> onLoad() async {
await super.onLoad();
_plunger = parent!.parent! as Plunger;
}
@override
void update(double dt) {
if (bloc.state.isPulling) {
_plunger.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 = _plunger.body.joints.whereType<PrismaticJoint>().single;
final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit();
if (reachedBottom) {
bloc.released();
}
}
}

@ -0,0 +1,31 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_components/pinball_components.dart';
class PlungerReleasingBehavior extends Component
with FlameBlocListenable<PlungerCubit, PlungerState> {
PlungerReleasingBehavior({
required double strength,
}) : assert(strength >= 0, "Strength can't be negative."),
_strength = strength;
final double _strength;
late final Plunger _plunger;
@override
Future<void> onLoad() async {
await super.onLoad();
_plunger = parent!.parent! as Plunger;
}
@override
void onNewState(PlungerState state) {
super.onNewState(state);
if (state.isReleasing) {
final velocity =
(_plunger.initialPosition.y - _plunger.body.position.y) * _strength;
_plunger.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;
}

@ -0,0 +1,139 @@
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_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.
///
/// [Plunger] ignores gravity so the player controls its downward movement.
/// {@endtemplate}
class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger}
Plunger()
: super(
renderBody: false,
children: [
FlameBlocProvider<PlungerCubit, PlungerState>(
create: PlungerCubit.new,
children: [
_PlungerSpriteAnimationGroupComponent(),
PlungerReleasingBehavior(strength: 11),
PlungerNoiseBehavior(),
],
),
PlungerJointingBehavior(compressionDistance: 9.2),
],
) {
zIndex = ZIndexes.plunger;
layer = Layer.launcher;
}
/// Creates a [Plunger] without any children.
///
/// This can be used for testing [Plunger]'s behaviors in isolation.
@visibleForTesting
Plunger.test();
List<FixtureDef> _createFixtureDefs() {
final leftShapeVertices = [
Vector2(0, 0),
Vector2(-1.8, 0),
Vector2(-1.8, -2.2),
Vector2(0, -0.3),
]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle));
final leftTriangleShape = PolygonShape()..set(leftShapeVertices);
final rightShapeVertices = [
Vector2(0, 0),
Vector2(1.8, 0),
Vector2(1.8, -2.2),
Vector2(0, -0.3),
]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle));
final rightTriangleShape = PolygonShape()..set(rightShapeVertices);
return [
FixtureDef(
leftTriangleShape,
density: 80,
),
FixtureDef(
rightTriangleShape,
density: 80,
),
];
}
@override
Body createBody() {
final bodyDef = BodyDef(
position: initialPosition,
type: BodyType.dynamic,
gravityScale: Vector2.zero(),
);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
}
class _PlungerSpriteAnimationGroupComponent
extends SpriteAnimationGroupComponent<PlungerState>
with HasGameRef, FlameBlocListenable<PlungerCubit, PlungerState> {
_PlungerSpriteAnimationGroupComponent()
: super(
anchor: Anchor.center,
position: Vector2(1.87, 14.9),
);
@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 = state;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.plunger.plunger.keyName,
);
const amountPerRow = 20;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
final pullAnimation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn ~/ 2,
amountPerRow: amountPerRow ~/ 2,
stepTime: 1 / 24,
textureSize: textureSize,
texturePosition: Vector2.zero(),
loop: false,
),
);
animations = {
PlungerState.releasing: pullAnimation.reversed(),
PlungerState.pulling: pullAnimation,
};
current = readBloc<PlungerCubit, PlungerState>().state;
}
}

@ -18,6 +18,8 @@ dependencies:
flutter:
sdk: flutter
intl: ^0.17.0
pinball_audio:
path: ../pinball_audio
pinball_flame:
path: ../pinball_flame
pinball_theme:

@ -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,39 +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<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
await add(
plunger = Plunger(compressionDistance: 29)
..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<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;
await traceAllBodies();
}
}

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

@ -24,6 +24,20 @@ void main() {
final flameTester = FlameTester(TestGame.new);
group('BumpingBehavior', () {
test('can be instantiated', () {
expect(
BumpingBehavior(strength: 0),
isA<BumpingBehavior>(),
);
});
test('throws assertion error when strength is negative ', () {
expect(
() => BumpingBehavior(strength: -1),
throwsAssertionError,
);
});
flameTester.test('can be added', (game) async {
final behavior = BumpingBehavior(strength: 0);
final component = _TestBodyComponent();

@ -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,8 +9,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../../../helpers/helpers.dart';
class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
@ -27,7 +26,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',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

@ -0,0 +1,36 @@
// 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('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);
});
});
}

@ -0,0 +1,194 @@
// 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';
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 _TestGame extends Forge2DGame {
Future<void> pump(
PlungerKeyControllingBehavior child, {
PlungerCubit? plungerBloc,
}) async {
final plunger = Plunger.test();
await ensureAdd(plunger);
return plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plungerBloc ?? _MockPlungerCubit(),
children: [child],
),
);
}
}
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();
}
}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('PlungerKeyControllingBehavior', () {
test('can be instantiated', () {
expect(
PlungerKeyControllingBehavior(),
isA<PlungerKeyControllingBehavior>(),
);
});
flameTester.test('can be loaded', (game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(behavior);
expect(game.descendants(), contains(behavior));
});
group('onKeyEvent', () {
late PlungerCubit plungerBloc;
setUp(() {
plungerBloc = _MockPlungerCubit();
});
group('pulls when', () {
flameTester.test(
'down arrow is pressed',
(game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(
behavior,
plungerBloc: plungerBloc,
);
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.arrowDown,
);
behavior.onKeyEvent(event, {});
verify(() => plungerBloc.pulled()).called(1);
},
);
flameTester.test(
'"s" is pressed',
(game) async {
final behavior = PlungerKeyControllingBehavior();
await game.pump(
behavior,
plungerBloc: 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,
plungerBloc: 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,
plungerBloc: 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,
plungerBloc: 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,
plungerBloc: plungerBloc,
);
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(
LogicalKeyboardKey.space,
);
behavior.onKeyEvent(event, {});
verify(() => plungerBloc.released()).called(1);
},
);
});
});
});
}

@ -0,0 +1,91 @@
// 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';
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,
PlungerCubit? plungerBloc,
}) async {
final parent = Component();
await ensureAdd(parent);
return parent.ensureAdd(
FlameProvider<PinballAudioPlayer>.value(
pinballAudioPlayer ?? _MockPinballAudioPlayer(),
children: [
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plungerBloc ?? PlungerCubit(),
children: [child],
),
],
),
);
}
}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
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 behavior = PlungerNoiseBehavior();
await game.pump(behavior);
expect(game.descendants(), contains(behavior));
});
flameTester.test(
'plays the correct sound when released',
(game) async {
final plungerBloc = _MockPlungerCubit();
final streamController = StreamController<PlungerState>();
whenListen<PlungerState>(
plungerBloc,
streamController.stream,
initialState: PlungerState.pulling,
);
final behavior = PlungerNoiseBehavior();
await game.pump(
behavior,
pinballAudioPlayer: audioPlayer,
plungerBloc: plungerBloc,
);
streamController.add(PlungerState.releasing);
await Future<void>.delayed(Duration.zero);
verify(() => audioPlayer.play(PinballAudio.launcher)).called(1);
},
);
});
}

@ -0,0 +1,160 @@
// 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<void> pump(
PlungerPullingBehavior behavior, {
PlungerCubit? plungerBloc,
}) async {
final plunger = Plunger.test();
await ensureAdd(plunger);
return plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plungerBloc ?? _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(
PlungerPullingBehavior(strength: 0),
isA<PlungerPullingBehavior>(),
);
});
test('throws assertion error when strength is negative ', () {
expect(
() => PlungerPullingBehavior(strength: -1),
throwsAssertionError,
);
});
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,
plungerBloc: 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', () {
test('can be instantiated', () {
expect(
PlungerAutoPullingBehavior(strength: 0),
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,
plungerBloc: 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,
plungerBloc: 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);
},
);
});
}

@ -0,0 +1,79 @@
// 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<void> pump(
PlungerReleasingBehavior behavior, {
PlungerCubit? plungerBloc,
}) async {
final plunger = Plunger.test();
await ensureAdd(plunger);
return plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>.value(
value: plungerBloc ?? PlungerCubit(),
children: [behavior],
),
);
}
}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
void main() {
group('PlungerReleasingBehavior', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
test('can be instantiated', () {
expect(
PlungerReleasingBehavior(strength: 0),
isA<PlungerReleasingBehavior>(),
);
});
test('throws assertion error when strength is negative ', () {
expect(
() => PlungerReleasingBehavior(strength: -1),
throwsAssertionError,
);
});
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,
plungerBloc: 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)));
});
});
}

@ -0,0 +1,116 @@
// 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';
import 'package:pinball_components/pinball_components.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('Plunger', () {
test('can be instantiated', () {
expect(Plunger(), isA<Plunger>());
});
flameTester.test(
'loads correctly',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(game.children, contains(plunger));
},
);
group('adds', () {
flameTester.test(
'a PlungerReleasingBehavior',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(
game.descendants().whereType<PlungerReleasingBehavior>().length,
equals(1),
);
},
);
flameTester.test(
'a PlungerJointingBehavior',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(
game.descendants().whereType<PlungerJointingBehavior>().length,
equals(1),
);
},
);
flameTester.test(
'a PlungerNoiseBehavior',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(
game.descendants().whereType<PlungerNoiseBehavior>().length,
equals(1),
);
},
);
});
group('renders correctly', () {
const goldenPath = '../golden/plunger/';
flameTester.testGameWidget(
'pulling',
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;
final bloc = plunger
.descendants()
.whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
.single
.bloc;
bloc.pulled();
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenPath}pull.png'),
);
},
);
flameTester.testGameWidget(
'releasing',
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;
final bloc = plunger
.descendants()
.whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
.single
.bloc;
bloc.released();
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenPath}release.png'),
);
},
);
});
});
}

@ -1,391 +0,0 @@
// 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';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('Plunger', () {
const compressionDistance = 0.0;
test('can be instantiated', () {
expect(
Plunger(compressionDistance: compressionDistance),
isA<Plunger>(),
);
expect(
Plunger.test(compressionDistance: compressionDistance),
isA<Plunger>(),
);
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.ensureAdd(Plunger(compressionDistance: compressionDistance));
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'),
);
},
);
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(game.contains(plunger), isTrue);
},
);
group('body', () {
flameTester.test(
'is dynamic',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(plunger.body.bodyType, equals(BodyType.dynamic));
},
);
flameTester.test(
'ignores gravity',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(plunger.body.gravityScale, equals(Vector2.zero()));
},
);
});
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(plunger.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'shape is a polygon',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
final fixture = plunger.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.polygon));
},
);
flameTester.test(
'has density',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
final fixture = plunger.body.fixtures[0];
expect(fixture.density, greaterThan(0));
},
);
});
group('pullFor', () {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
});
flameTester.testGameWidget(
'moves downwards for given period when pullFor is called',
setUp: (game, tester) async {
await game.ensureAdd(plunger);
},
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);
},
);
});
group('pull', () {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
});
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.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(
compressionDistance: compressionDistance,
);
});
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('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);
},
);
});
}

@ -2,7 +2,6 @@ library pinball_flame;
export 'src/behaviors/behaviors.dart';
export 'src/canvas/canvas.dart';
export 'src/component_controller.dart';
export 'src/flame_provider.dart';
export 'src/keyboard_input_controller.dart';
export 'src/layer.dart';

@ -1,41 +0,0 @@
import 'package:flame/components.dart';
import 'package:flutter/foundation.dart';
/// {@template component_controller}
/// A [ComponentController] is a [Component] in charge of handling the logic
/// associated with another [Component].
/// {@endtemplate}
abstract class ComponentController<T extends Component> extends Component {
/// {@macro component_controller}
ComponentController(this.component);
/// The [Component] controlled by this [ComponentController].
final T component;
@override
Future<void> addToParent(Component parent) async {
assert(
parent == component,
'ComponentController should be child of $component.',
);
await super.addToParent(parent);
}
@override
Future<void> add(Component component) {
throw Exception('ComponentController cannot add other components.');
}
}
/// Mixin that attaches a single [ComponentController] to a [Component].
mixin Controls<T extends ComponentController> on Component {
/// The [ComponentController] attached to this [Component].
late T controller;
@override
@mustCallSuper
Future<void> onLoad() async {
await super.onLoad();
await add(controller);
}
}

@ -1,96 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame/game.dart';
import 'package:flame/src/components/component.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_flame/pinball_flame.dart';
class TestComponentController extends ComponentController {
TestComponentController(Component component) : super(component);
}
class ControlledComponent extends Component
with Controls<TestComponentController> {
ControlledComponent() : super() {
controller = TestComponentController(this);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(FlameGame.new);
group('ComponentController', () {
flameTester.test(
'can be instantiated',
(game) async {
expect(
TestComponentController(Component()),
isA<ComponentController>(),
);
},
);
flameTester.test(
'throws AssertionError when not attached to controlled component',
(game) async {
final component = Component();
final controller = TestComponentController(component);
final anotherComponent = Component();
await expectLater(
() async => await anotherComponent.add(controller),
throwsAssertionError,
);
},
);
flameTester.test(
'throws Exception when adding a component',
(game) async {
final component = ControlledComponent();
final controller = TestComponentController(component);
await expectLater(
() async => controller.add(Component()),
throwsException,
);
},
);
flameTester.test(
'throws Exception when adding multiple components',
(game) async {
final component = ControlledComponent();
final controller = TestComponentController(component);
await expectLater(
() async => controller.addAll([
Component(),
Component(),
]),
throwsException,
);
},
);
});
group('Controls', () {
flameTester.test(
'can be instantiated',
(game) async {
expect(ControlledComponent(), isA<Component>());
},
);
flameTester.test('adds controller', (game) async {
final component = ControlledComponent();
await game.add(component);
await game.ready();
expect(component.contains(component.controller), isTrue);
});
});
}

@ -130,7 +130,7 @@ void main() {
await game.pump([
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
Plunger.test(),
]);
expect(game.descendants().whereType<Ball>(), isEmpty);

@ -79,8 +79,6 @@ void main() {
flameTester.test(
'onNewState calls onCharacterSelected on the arcade background bloc',
(game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenAnswer((_) => false);
final arcadeBackgroundBloc = _MockArcadeBackgroundCubit();
whenListen(
arcadeBackgroundBloc,
@ -95,10 +93,9 @@ void main() {
arcadeBackground,
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
Plunger.test(),
Ball.test(),
],
platformHelper: platformHelper,
);
const dinoThemeState = CharacterThemeState(theme.DinoTheme());
@ -130,7 +127,7 @@ void main() {
ball,
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
Plunger.test(),
ArcadeBackground.test(),
],
platformHelper: platformHelper,

@ -1,185 +0,0 @@
// 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);
});
});
}

@ -36,6 +36,7 @@ class _TestGame extends Forge2DGame with HasTappables {
Future<void> pump(
Iterable<Component> children, {
PinballAudioPlayer? pinballAudioPlayer,
PlatformHelper? platformHelper,
}) async {
return ensureAdd(
FlameMultiBlocProvider(
@ -57,7 +58,7 @@ class _TestGame extends Forge2DGame with HasTappables {
_MockAppLocalizations(),
),
FlameProvider<PlatformHelper>.value(
_MockPlatformHelper(),
platformHelper ?? PlatformHelper(),
),
],
children: children,
@ -75,10 +76,9 @@ class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockPlatformHelper extends Mock implements PlatformHelper {
@override
bool get isMobile => false;
}
class _MockPlatformHelper extends Mock implements PlatformHelper {}
class _MockPlungerCubit extends Mock implements PlungerCubit {}
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
@ -196,7 +196,6 @@ void main() {
await flipper.ensureAdd(behavior);
expect(state.status, GameStatus.gameOver);
component.onNewState(state);
await game.ready();
@ -207,6 +206,77 @@ void main() {
},
);
flameTester.test(
'removes PlungerKeyControllingBehavior from Plunger',
(game) async {
final component = GameBlocStatusListener();
final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox(
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [],
);
final plunger = Plunger.test();
await game.pump(
[component, backbox, plunger],
);
await plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>(
create: PlungerCubit.new,
children: [PlungerKeyControllingBehavior()],
),
);
expect(state.status, GameStatus.gameOver);
component.onNewState(state);
await game.ready();
expect(
plunger.children.whereType<PlungerKeyControllingBehavior>(),
isEmpty,
);
},
);
flameTester.test(
'removes PlungerPullingBehavior from Plunger',
(game) async {
final component = GameBlocStatusListener();
final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox(
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [],
);
final plunger = Plunger.test();
await game.pump(
[component, backbox, plunger],
);
await plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>(
create: PlungerCubit.new,
children: [
PlungerPullingBehavior(strength: 0),
PlungerAutoPullingBehavior(strength: 0)
],
),
);
expect(state.status, GameStatus.gameOver);
component.onNewState(state);
await game.ready();
expect(
plunger.children.whereType<PlungerPullingBehavior>(),
isEmpty,
);
},
);
flameTester.test(
'plays the game over voice over',
(game) async {
@ -263,7 +333,7 @@ void main() {
);
flameTester.test(
'adds key controlling behavior to Flippers when the game is started',
'adds FlipperKeyControllingBehavior to Flippers',
(game) async {
final component = GameBlocStatusListener();
final leaderboardRepository = _MockLeaderboardRepository();
@ -288,6 +358,120 @@ void main() {
);
},
);
flameTester.test(
'adds PlungerKeyControllingBehavior to Plunger when on desktop',
(game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenReturn(false);
final component = GameBlocStatusListener();
final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox(
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [],
);
final plunger = Plunger.test();
await game.pump(
[component, backbox, plunger],
platformHelper: platformHelper,
);
await plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>(
create: _MockPlungerCubit.new,
),
);
expect(state.status, GameStatus.playing);
component.onNewState(state);
await game.ready();
expect(
plunger
.descendants()
.whereType<PlungerKeyControllingBehavior>()
.length,
equals(1),
);
},
);
flameTester.test(
'adds PlungerPullingBehavior to Plunger when on desktop',
(game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenReturn(false);
final component = GameBlocStatusListener();
final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox(
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [],
);
final plunger = Plunger.test();
await game.pump(
[component, backbox, plunger],
platformHelper: platformHelper,
);
await plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>(
create: _MockPlungerCubit.new,
),
);
expect(state.status, GameStatus.playing);
component.onNewState(state);
await game.ready();
expect(
plunger.descendants().whereType<PlungerPullingBehavior>().length,
equals(1),
);
},
);
flameTester.test(
'adds PlungerAutoPullingBehavior to Plunger when on mobile',
(game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenReturn(true);
final component = GameBlocStatusListener();
final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox(
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [],
);
final plunger = Plunger.test();
await game.pump(
[component, backbox, plunger],
platformHelper: platformHelper,
);
await plunger.ensureAdd(
FlameBlocProvider<PlungerCubit, PlungerState>(
create: _MockPlungerCubit.new,
),
);
expect(state.status, GameStatus.playing);
component.onNewState(state);
await game.ready();
expect(
plunger
.descendants()
.whereType<PlungerAutoPullingBehavior>()
.length,
equals(1),
);
},
);
});
});
});

@ -5,6 +5,7 @@ import 'dart:ui';
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_test/flame_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -395,7 +396,7 @@ void main() {
});
group('plunger control', () {
flameTester.test('tap down moves plunger down', (game) async {
flameTester.test('tap down emits plunging', (game) async {
await game.ready();
final eventPosition = _MockEventPosition();
@ -408,13 +409,15 @@ void main() {
when(() => tapDownEvent.eventPosition).thenReturn(eventPosition);
when(() => tapDownEvent.raw).thenReturn(raw);
final plunger = game.descendants().whereType<Plunger>().first;
game.onTapDown(0, tapDownEvent);
game.update(1);
final plungerBloc = game
.descendants()
.whereType<FlameBlocProvider<PlungerCubit, PlungerState>>()
.single
.bloc;
expect(plunger.body.linearVelocity.y, isPositive);
expect(plungerBloc.state, PlungerState.pulling);
});
});
});

@ -1,3 +1,2 @@
export 'key_testers.dart';
export 'mock_flame_images.dart';
export 'pump_app.dart';

@ -1,50 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'package:mocktail/mocktail.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();
}
}
@isTest
void testRawKeyUpEvents(
List<LogicalKeyboardKey> keys,
Function(RawKeyUpEvent) test,
) {
for (final key in keys) {
test(_mockKeyUpEvent(key));
}
}
RawKeyUpEvent _mockKeyUpEvent(LogicalKeyboardKey key) {
final event = _MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(key);
return event;
}
@isTest
void testRawKeyDownEvents(
List<LogicalKeyboardKey> keys,
Function(RawKeyDownEvent) test,
) {
for (final key in keys) {
test(_mockKeyDownEvent(key));
}
}
RawKeyDownEvent _mockKeyDownEvent(LogicalKeyboardKey key) {
final event = _MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(key);
return event;
}
Loading…
Cancel
Save