feat: defined Plunger behaviors

pull/434/head
alestiago 3 years ago
parent fdb9075738
commit b087d0e418

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

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

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

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

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

@ -0,0 +1,3 @@
export 'plunger_jointing_behavior.dart';
export 'plunger_key_controlling_behavior.dart';
export 'plunger_noise_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:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Allows controlling the [Plunger]'s movement with keyboard input.
class PlungerKeyControllingBehavior extends Component
with KeyboardHandler, ParentIsA<Plunger> {
/// 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 (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
parent.pull();
} else if (event is RawKeyUpEvent) {
parent.release();
}
return false;
}
}

@ -0,0 +1,20 @@
import 'package:flame/components.dart';
import 'package:pinball_audio/pinball_audio.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 {
@override
Future<void> onLoad() async {
await super.onLoad();
readProvider<PinballAudioPlayer>().play(PinballAudio.launcher);
}
@override
void update(double dt) {
super.update(dt);
removeFromParent();
}
}

@ -2,6 +2,7 @@ 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_components/src/components/plunger/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template plunger}
@ -12,11 +13,14 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@endtemplate}
class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger}
Plunger({
required this.compressionDistance,
}) : super(
Plunger()
: super(
renderBody: false,
children: [_PlungerSpriteAnimationGroupComponent()],
children: [
_PlungerSpriteAnimationGroupComponent(),
PlungerJointingBehavior(compressionDistance: 9.2),
PlungerKeyControllingBehavior(),
],
) {
zIndex = ZIndexes.plunger;
layer = Layer.launcher;
@ -26,53 +30,47 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
///
/// 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;
Plunger.test();
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();
]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle));
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();
]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle));
final rightTriangleShape = PolygonShape()..set(rightShapeVertices);
final rightTriangleFixtureDef = FixtureDef(rightTriangleShape)
..density = 80;
fixturesDef.add(rightTriangleFixtureDef);
return fixturesDef;
return [
FixtureDef(
leftTriangleShape,
density: 80,
),
FixtureDef(
rightTriangleShape,
density: 80,
),
];
}
@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;
}
@ -97,6 +95,7 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition].
void release() {
add(PlungerNoiseBehavior());
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
_pullingDownTime = 0;
@ -118,28 +117,6 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
}
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].
@ -206,46 +183,3 @@ class _PlungerSpriteAnimationGroupComponent
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;
}
}

@ -20,6 +20,8 @@ dependencies:
geometry:
path: ../geometry
intl: ^0.17.0
pinball_audio:
path: ../pinball_audio
pinball_flame:
path: ../pinball_flame
pinball_theme:
@ -27,6 +29,7 @@ dependencies:
pinball_ui:
path: ../pinball_ui
dev_dependencies:
bloc_test: ^9.0.3
flame_test: ^1.3.0

@ -26,8 +26,7 @@ class PlungerGame extends BallGame with KeyboardEvents, Traceable {
final center = screenToWorld(camera.viewport.canvasSize! / 2);
await add(
plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(center.x - 8.8, center.y),
plunger = Plunger()..initialPosition = Vector2(center.x - 8.8, center.y),
);
await traceAllBodies();
}

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

@ -5,7 +5,7 @@ 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';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -15,21 +15,23 @@ void main() {
const compressionDistance = 0.0;
test('can be instantiated', () {
expect(
Plunger(compressionDistance: compressionDistance),
isA<Plunger>(),
);
expect(
Plunger.test(compressionDistance: compressionDistance),
isA<Plunger>(),
);
expect(Plunger(), isA<Plunger>());
expect(Plunger.test(), isA<Plunger>());
});
flameTester.test(
'loads correctly',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(game.children, contains(plunger));
},
);
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.ensureAdd(Plunger(compressionDistance: compressionDistance));
await game.ensureAdd(Plunger());
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 4.1;
},
@ -53,80 +55,32 @@ void main() {
},
);
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);
test('is dynamic', () {
final body = Plunger().createBody();
expect(body.bodyType, equals(BodyType.dynamic));
});
expect(plunger.body.gravityScale, equals(Vector2.zero()));
},
);
test('ignores gravity', () {
final body = Plunger().createBody();
expect(body.gravityScale, equals(Vector2.zero()));
});
});
group('fixture', () {
flameTester.test(
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));
() async {
final body = Plunger().createBody();
expect(body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
test(
'has density',
(game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
final fixture = plunger.body.fixtures[0];
() {
final body = Plunger().createBody();
final fixture = body.fixtures[0];
expect(fixture.density, greaterThan(0));
},
);
@ -136,9 +90,7 @@ void main() {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
plunger = Plunger();
});
flameTester.testGameWidget(
@ -167,9 +119,7 @@ void main() {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
plunger = Plunger();
});
flameTester.test(
@ -200,9 +150,7 @@ void main() {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
plunger = Plunger();
});
flameTester.test(

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

@ -77,7 +77,7 @@ void main() {
ball,
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
Plunger.test(),
]);
const dinoThemeState = CharacterThemeState(theme.DinoTheme());

Loading…
Cancel
Save