diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index d52c7c31..2b55cdc7 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,6 +1,14 @@ import 'package:flame_forge2d/flame_forge2d.dart'; +/// {@template plunger} +/// Plunger body component to be pulled and released by the player to launch +/// the pinball. +/// +/// The plunger body ignores gravity so the player can control its downward +/// pull. +/// {@endtemplate} class Plunger extends BodyComponent { + /// {@macro plunger} Plunger(this._position); final Vector2 _position; @@ -9,24 +17,52 @@ class Plunger extends BodyComponent { Body createBody() { final shape = PolygonShape()..setAsBoxXY(2.5, 1.5); - final fixtureDef = FixtureDef(shape)..friction = 0.1; + final fixtureDef = FixtureDef(shape); final bodyDef = BodyDef() ..userData = this ..position = _position - ..type = BodyType.dynamic; + ..type = BodyType.dynamic + ..gravityScale = 0; return world.createBody(bodyDef)..createFixture(fixtureDef); } - // Unused for now - from the previous kinematic plunger implementation. + /// Set a contstant downward velocity on the plunger body. void pull() { - body.linearVelocity = Vector2(0, -5); + body.linearVelocity = Vector2(0, -7); } - // Unused for now - from the previous kinematic plunger implementation. + /// Set an upward velocity on the plunger body. The velocity's magnitude + /// depends on how far the plunger has been pulled from its original position. void release() { final velocity = (_position.y - body.position.y) * 9; body.linearVelocity = Vector2(0, velocity); } } + +/// {@template plunger_anchor_prismatic_joint_def} +/// Prismatic joint def between a [Plunger] and an anchor body given motion on +/// the vertical axis. +/// +/// The [Plunger] is constrained to vertical motion between its starting +/// position and the anchor body. The anchor needs to be below the plunger for +/// this joint to function properly. +/// {@endtemplate} +class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { + /// {@macro plunger_anchor_prismatic_joint_def} + PlungerAnchorPrismaticJointDef({ + required Plunger plunger, + required BodyComponent anchor, + }) { + initialize( + plunger.body, + anchor.body, + anchor.body.position, + Vector2(0, -1), + ); + enableLimit = true; + lowerTranslation = double.negativeInfinity; + collideConnected = true; + } +} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index ff112a43..fbcea02f 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -7,7 +7,6 @@ import 'package:pinball/game/game.dart'; class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { late Plunger plunger; - late PrismaticJointDef prismaticJointDef; @override Future onLoad() async { @@ -19,32 +18,11 @@ class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { final center = screenToWorld(camera.viewport.effectiveSize / 2); - await add(plunger = Plunger(Vector2(center.x, center.y - 50))); + await add(plunger = Plunger(Vector2(center.x, center.y))); - prismaticJointDef = PrismaticJointDef() - ..initialize( - plunger.body, - bottomWall.body, - plunger.body.position, - // Logically, I feel like this should be (0, 1), but it has to be - // negative for lowerTranslation limit to work as expected. - Vector2(0, -1), - ) - ..enableLimit = true - // Given the above inverted vertical axis, the lowerTranslation works as - // expected and this lets the plunger fall down 10 units before being - // stopped. - // - // Ideally, we shouldn't need to set any limits here - this is just for - // demo purposes to see how the limits work. We should be leaving this at - // 0 and altering it as the user holds the space bar. The longer they hold - // it, the lower the lowerTranslation becomes - allowing the plunger to - // slowly fall down (see key event handlers below). - ..lowerTranslation = -10 - // This prevents the plunger from falling through the bottom wall. - ..collideConnected = true; - - world.createJoint(prismaticJointDef); + world.createJoint( + PlungerAnchorPrismaticJointDef(plunger: plunger, anchor: bottomWall), + ); } @override @@ -54,19 +32,11 @@ class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents { ) { if (event is RawKeyUpEvent && event.data.logicalKey == LogicalKeyboardKey.space) { - // I haven't been able to successfully pull down the plunger, so this is - // completely untested. I imagine we could calculate the distance between - // the prismaticJoinDef.upperTranslation (plunger starting position) and - // the ground, then use that value as a multiplier on the speed so the - // ball moves faster when you pull the plunger farther down. - prismaticJointDef.motorSpeed = 5; + plunger.release(); } if (event is RawKeyDownEvent && event.data.logicalKey == LogicalKeyboardKey.space) { - // This was my attempt to decrement the lower limit but it doesn't seem to - // render. If you debug, you can see that this value is being lowered, - // but the game isn't reflecting these value changes. - prismaticJointDef.lowerTranslation--; + plunger.pull(); } return KeyEventResult.handled; } diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart new file mode 100644 index 00000000..6156c5ff --- /dev/null +++ b/test/game/components/plunger_test.dart @@ -0,0 +1,192 @@ +// 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/game/game.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(PinballGame.new); + + group('Plunger', () { + flameTester.test( + 'loads correctly', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + expect(game.contains(plunger), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final plunger = Plunger(position); + await game.ensureAdd(plunger); + game.contains(plunger); + + expect(plunger.body.position, position); + }, + ); + + flameTester.test( + 'is dynamic', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + expect(plunger.body.bodyType, equals(BodyType.dynamic)); + }, + ); + + flameTester.test( + 'ignores gravity', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + expect(plunger.body.gravityScale, isZero); + }, + ); + }); + + group('first fixture', () { + flameTester.test( + 'exists', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + expect(plunger.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'shape is a polygon', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + final fixture = plunger.body.fixtures[0]; + expect(fixture.shape.shapeType, equals(ShapeType.polygon)); + }, + ); + }); + + flameTester.test( + 'pull sets a negative linear velocity', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + plunger.pull(); + + expect(plunger.body.linearVelocity.y, isNegative); + }, + ); + + group('release', () { + flameTester.test( + 'does not set a linear velocity ' + 'when plunger is in starting position', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + plunger.release(); + + expect(plunger.body.linearVelocity.y, isZero); + }, + ); + + flameTester.test( + 'sets a positive linear velocity ' + 'when plunger is below starting position', + (game) async { + final plunger = Plunger(Vector2.zero()); + await game.ensureAdd(plunger); + + plunger.body.setTransform(Vector2(0, -1), 0); + plunger.release(); + + expect(plunger.body.linearVelocity.y, isPositive); + }, + ); + }); + }); + + group('PlungerAnchorPrismaticJointDef', () { + final plunger = Plunger(Vector2.zero())..createBody(); + final anchor = Plunger(Vector2(0, -5))..createBody(); + + group('initializes with', () { + flameTester.test( + 'plunger as bodyA', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + + expect(jointDef.bodyA, equals(plunger)); + }, + ); + + flameTester.test( + 'anchor as bodyB', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + expect(jointDef.bodyB, equals(anchor)); + }, + ); + + flameTester.test( + 'limits enabled', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + expect(jointDef.enableLimit, isTrue); + }, + ); + + flameTester.test( + 'lower translation limit as negative infinity', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + expect(jointDef.lowerTranslation, equals(double.negativeInfinity)); + }, + ); + + flameTester.test( + 'connected body collison enabled', + (game) async { + final jointDef = PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: anchor, + ); + game.world.createJoint(jointDef); + + expect(jointDef.collideConnected, isTrue); + }, + ); + }); + }); +}