mirror of https://github.com/flutter/pinball.git
commit
72ce22ef9a
@ -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 'mock_flame_images.dart';
|
||||||
export 'pump_app.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