mirror of https://github.com/flutter/pinball.git
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 filepull/439/head
parent
11c076c386
commit
461471b01f
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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…
Reference in new issue