diff --git a/lib/game/components/launcher.dart b/lib/game/components/launcher.dart index f3238152..d2e69174 100644 --- a/lib/game/components/launcher.dart +++ b/lib/game/components/launcher.dart @@ -16,8 +16,8 @@ class Launcher extends Forge2DBlueprint { @override void build(Forge2DGame gameRef) { - plunger = ControlledPlunger(compressionDistance: 12.3) - ..initialPosition = Vector2(40.1, 38); + plunger = ControlledPlunger(compressionDistance: 14) + ..initialPosition = Vector2(40.7, 38); final _rocket = RocketSpriteComponent()..position = Vector2(43, 62); diff --git a/packages/pinball_components/assets/images/plunger/plunger.png b/packages/pinball_components/assets/images/plunger/plunger.png index f3cbdf0f..2ec6e001 100644 Binary files a/packages/pinball_components/assets/images/plunger/plunger.png and b/packages/pinball_components/assets/images/plunger/plunger.png differ diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart index 15e93733..efa8817a 100644 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -16,23 +16,47 @@ class Plunger extends BodyComponent with InitialPosition, Layered { // are fixed. }) : super(priority: RenderPriority.plunger) { layer = Layer.launcher; + renderBody = false; } /// Distance the plunger can lower. final double compressionDistance; + late final _PlungerSpriteAnimationGroupComponent _spriteComponent; + + List _createFixtureDefs() { + final fixturesDef = []; + + 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 shape = PolygonShape() - ..setAsBox( - 1.35, - 0.5, - Vector2.zero(), - BoardDimensions.perspectiveAngle, - ); - - final fixtureDef = FixtureDef(shape)..density = 80; - final bodyDef = BodyDef( position: initialPosition, userData: this, @@ -40,12 +64,15 @@ class Plunger extends BodyComponent with InitialPosition, Layered { gravityScale: Vector2.zero(), ); - return world.createBody(bodyDef)..createFixture(fixtureDef); + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + return body; } /// Set a constant downward velocity on the [Plunger]. void pull() { body.linearVelocity = Vector2(0, 7); + _spriteComponent.pull(); } /// Set an upward velocity on the [Plunger]. @@ -55,6 +82,7 @@ class Plunger extends BodyComponent with InitialPosition, Layered { void release() { final velocity = (initialPosition.y - body.position.y) * 5; body.linearVelocity = Vector2(0, velocity); + _spriteComponent.release(); } /// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical @@ -77,24 +105,84 @@ class Plunger extends BodyComponent with InitialPosition, Layered { Future onLoad() async { await super.onLoad(); await _anchorToJoint(); - renderBody = false; - await add(_PlungerSpriteComponent()); + + _spriteComponent = _PlungerSpriteAnimationGroupComponent(); + await add(_spriteComponent); } } -class _PlungerSpriteComponent extends SpriteComponent with HasGameRef { +/// 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 onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( + + final spriteSheet = await gameRef.images.load( Assets.images.plunger.plunger.keyName, ); - this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(1.5, 13.4); - angle = -0.008; + const amountPerRow = 20; + const amountPerColumn = 1; + + final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, + ); + size = textureSize / 10; + + // TODO(ruimiguel): we only need plunger pull animation, and release is just + // to reverse it, so we need to divide by 2 while we don't have only half of + // the animation (but amountPerRow and amountPerColumn needs to be correct + // in order of calculate textureSize correctly). + + 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; } } diff --git a/packages/pinball_components/test/src/components/golden/plunger.png b/packages/pinball_components/test/src/components/golden/plunger.png deleted file mode 100644 index a33405f1..00000000 Binary files a/packages/pinball_components/test/src/components/golden/plunger.png and /dev/null differ diff --git a/packages/pinball_components/test/src/components/golden/plunger/pull.png b/packages/pinball_components/test/src/components/golden/plunger/pull.png new file mode 100644 index 00000000..0ec27a4e Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/plunger/pull.png differ diff --git a/packages/pinball_components/test/src/components/golden/plunger/release.png b/packages/pinball_components/test/src/components/golden/plunger/release.png new file mode 100644 index 00000000..61f7a4d9 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/plunger/release.png differ diff --git a/packages/pinball_components/test/src/components/plunger_test.dart b/packages/pinball_components/test/src/components/plunger_test.dart index 7922f060..eafc15d5 100644 --- a/packages/pinball_components/test/src/components/plunger_test.dart +++ b/packages/pinball_components/test/src/components/plunger_test.dart @@ -23,9 +23,21 @@ void main() { game.camera.zoom = 4.1; }, verify: (game, tester) async { + final plunger = game.descendants().whereType().first; + plunger.pull(); + game.update(1); + await tester.pump(); await expectLater( find.byGame(), - matchesGoldenFile('golden/plunger.png'), + matchesGoldenFile('golden/plunger/pull.png'), + ); + + plunger.release(); + game.update(1); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/plunger/release.png'), ); }, ); @@ -110,12 +122,17 @@ void main() { }); group('pull', () { + late Plunger plunger; + + setUp(() { + plunger = Plunger( + compressionDistance: compressionDistance, + ); + }); + flameTester.test( 'moves downwards when pull is called', (game) async { - final plunger = Plunger( - compressionDistance: compressionDistance, - ); await game.ensureAdd(plunger); plunger.pull(); @@ -123,6 +140,18 @@ void main() { 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', () {