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 'android_acres/android_acres.dart';
export 'backbox/backbox.dart'; export 'backbox/backbox.dart';
export 'bottom_group.dart'; export 'bottom_group.dart';
export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart'; export 'dino_desert/dino_desert.dart';
export 'drain/drain.dart'; export 'drain/drain.dart';
export 'flutter_forest/flutter_forest.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:flame/components.dart';
import 'package:pinball/game/components/components.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template launcher} /// {@template launcher}
@ -13,8 +12,7 @@ class Launcher extends Component {
children: [ children: [
LaunchRamp(), LaunchRamp(),
Flapper(), Flapper(),
ControlledPlunger(compressionDistance: 9.2) Plunger()..initialPosition = Vector2(41, 43.7),
..initialPosition = Vector2(41, 43.7),
RocketSpriteComponent()..position = Vector2(42.8, 62.3), RocketSpriteComponent()..position = Vector2(42.8, 62.3),
], ],
); );

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

@ -14,7 +14,21 @@ class FlipperKeyControllingBehavior extends Component
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); 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 @override
@ -33,20 +47,3 @@ class FlipperKeyControllingBehavior extends Component
return false; 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:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.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'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template plunger} /// {@template plunger}
@ -12,11 +13,14 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@endtemplate} /// {@endtemplate}
class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger} /// {@macro plunger}
Plunger({ Plunger()
required this.compressionDistance, : super(
}) : super(
renderBody: false, renderBody: false,
children: [_PlungerSpriteAnimationGroupComponent()], children: [
_PlungerSpriteAnimationGroupComponent(),
PlungerJointingBehavior(compressionDistance: 9.2),
PlungerKeyControllingBehavior(),
],
) { ) {
zIndex = ZIndexes.plunger; zIndex = ZIndexes.plunger;
layer = Layer.launcher; 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. /// This can be used for testing [Plunger]'s behaviors in isolation.
@visibleForTesting @visibleForTesting
Plunger.test({required this.compressionDistance}); Plunger.test();
/// Distance the plunger can lower.
final double compressionDistance;
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final leftShapeVertices = [ final leftShapeVertices = [
Vector2(0, 0), Vector2(0, 0),
Vector2(-1.8, 0), Vector2(-1.8, 0),
Vector2(-1.8, -2.2), Vector2(-1.8, -2.2),
Vector2(0, -0.3), Vector2(0, -0.3),
]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle)) ]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle));
.toList();
final leftTriangleShape = PolygonShape()..set(leftShapeVertices); final leftTriangleShape = PolygonShape()..set(leftShapeVertices);
final leftTriangleFixtureDef = FixtureDef(leftTriangleShape)..density = 80;
fixturesDef.add(leftTriangleFixtureDef);
final rightShapeVertices = [ final rightShapeVertices = [
Vector2(0, 0), Vector2(0, 0),
Vector2(1.8, 0), Vector2(1.8, 0),
Vector2(1.8, -2.2), Vector2(1.8, -2.2),
Vector2(0, -0.3), Vector2(0, -0.3),
]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle)) ]..forEach((vector) => vector.rotate(BoardDimensions.perspectiveAngle));
.toList();
final rightTriangleShape = PolygonShape()..set(rightShapeVertices); final rightTriangleShape = PolygonShape()..set(rightShapeVertices);
final rightTriangleFixtureDef = FixtureDef(rightTriangleShape) return [
..density = 80; FixtureDef(
fixturesDef.add(rightTriangleFixtureDef); leftTriangleShape,
density: 80,
return fixturesDef; ),
FixtureDef(
rightTriangleShape,
density: 80,
),
];
} }
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef( final bodyDef = BodyDef(
position: initialPosition, position: initialPosition,
userData: this,
type: BodyType.dynamic, type: BodyType.dynamic,
gravityScale: Vector2.zero(), gravityScale: Vector2.zero(),
); );
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
return body; 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 /// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition]. /// from its original [initialPosition].
void release() { void release() {
add(PlungerNoiseBehavior());
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!; final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
_pullingDownTime = 0; _pullingDownTime = 0;
@ -118,28 +117,6 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
} }
super.update(dt); 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]. /// Animation states associated with a [Plunger].
@ -206,46 +183,3 @@ class _PlungerSpriteAnimationGroupComponent
current = _PlungerAnimationState.release; 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: geometry:
path: ../geometry path: ../geometry
intl: ^0.17.0 intl: ^0.17.0
pinball_audio:
path: ../pinball_audio
pinball_flame: pinball_flame:
path: ../pinball_flame path: ../pinball_flame
pinball_theme: pinball_theme:
@ -27,6 +29,7 @@ dependencies:
pinball_ui: pinball_ui:
path: ../pinball_ui path: ../pinball_ui
dev_dependencies: dev_dependencies:
bloc_test: ^9.0.3 bloc_test: ^9.0.3
flame_test: ^1.3.0 flame_test: ^1.3.0

@ -26,8 +26,7 @@ class PlungerGame extends BallGame with KeyboardEvents, Traceable {
final center = screenToWorld(camera.viewport.canvasSize! / 2); final center = screenToWorld(camera.viewport.canvasSize! / 2);
await add( await add(
plunger = Plunger(compressionDistance: 29) plunger = Plunger()..initialPosition = Vector2(center.x - 8.8, center.y),
..initialPosition = Vector2(center.x - 8.8, center.y),
); );
await traceAllBodies(); 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:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -15,21 +15,23 @@ void main() {
const compressionDistance = 0.0; const compressionDistance = 0.0;
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(Plunger(), isA<Plunger>());
Plunger(compressionDistance: compressionDistance), expect(Plunger.test(), isA<Plunger>());
isA<Plunger>(),
);
expect(
Plunger.test(compressionDistance: compressionDistance),
isA<Plunger>(),
);
}); });
flameTester.test(
'loads correctly',
(game) async {
final plunger = Plunger();
await game.ensureAdd(plunger);
expect(game.children, contains(plunger));
},
);
flameTester.testGameWidget( flameTester.testGameWidget(
'renders correctly', 'renders correctly',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ensureAdd(Plunger(compressionDistance: compressionDistance)); await game.ensureAdd(Plunger());
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
game.camera.zoom = 4.1; 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', () { group('body', () {
flameTester.test( test('is dynamic', () {
'is dynamic', final body = Plunger().createBody();
(game) async { expect(body.bodyType, equals(BodyType.dynamic));
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())); test('ignores gravity', () {
}, final body = Plunger().createBody();
); expect(body.gravityScale, equals(Vector2.zero()));
});
}); });
group('fixture', () { group('fixture', () {
flameTester.test( test(
'exists', 'exists',
(game) async { () async {
final plunger = Plunger( final body = Plunger().createBody();
compressionDistance: compressionDistance, expect(body.fixtures[0], isA<Fixture>());
);
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( test(
'has density', 'has density',
(game) async { () {
final plunger = Plunger( final body = Plunger().createBody();
compressionDistance: compressionDistance, final fixture = body.fixtures[0];
);
await game.ensureAdd(plunger);
final fixture = plunger.body.fixtures[0];
expect(fixture.density, greaterThan(0)); expect(fixture.density, greaterThan(0));
}, },
); );
@ -136,9 +90,7 @@ void main() {
late Plunger plunger; late Plunger plunger;
setUp(() { setUp(() {
plunger = Plunger( plunger = Plunger();
compressionDistance: compressionDistance,
);
}); });
flameTester.testGameWidget( flameTester.testGameWidget(
@ -167,9 +119,7 @@ void main() {
late Plunger plunger; late Plunger plunger;
setUp(() { setUp(() {
plunger = Plunger( plunger = Plunger();
compressionDistance: compressionDistance,
);
}); });
flameTester.test( flameTester.test(
@ -200,9 +150,7 @@ void main() {
late Plunger plunger; late Plunger plunger;
setUp(() { setUp(() {
plunger = Plunger( plunger = Plunger();
compressionDistance: compressionDistance,
);
}); });
flameTester.test( flameTester.test(

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

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

Loading…
Cancel
Save