diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 2a2a8bb8..3c1a4302 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -10,4 +10,5 @@ export 'kicker.dart'; export 'launcher_ramp.dart'; export 'plunger.dart'; export 'score_points.dart'; +export 'spaceship_exit_rail.dart'; export 'wall.dart'; diff --git a/lib/game/components/jetpack_ramp.dart b/lib/game/components/jetpack_ramp.dart index e5eae883..b58ddfa6 100644 --- a/lib/game/components/jetpack_ramp.dart +++ b/lib/game/components/jetpack_ramp.dart @@ -114,7 +114,7 @@ class _JetpackRampOpening extends RampOpening { final double _rotation; - static final Vector2 _size = Vector2(JetpackRamp.width / 3, .1); + static final Vector2 _size = Vector2(JetpackRamp.width / 4, .1); @override Shape get shape => PolygonShape() diff --git a/lib/game/components/spaceship_exit_rail.dart b/lib/game/components/spaceship_exit_rail.dart new file mode 100644 index 00000000..0dc38322 --- /dev/null +++ b/lib/game/components/spaceship_exit_rail.dart @@ -0,0 +1,198 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; + +/// {@template spaceship_exit_rail} +/// A [Blueprint] for the spaceship drop tube. +/// {@endtemplate} +class SpaceshipExitRail extends Forge2DBlueprint { + /// {@macro spaceship_exit_rail} + SpaceshipExitRail({required this.position}); + + /// The [position] where the elements will be created + final Vector2 position; + + @override + void build(_) { + addAllContactCallback([ + SpaceshipExitRailEndBallContactCallback(), + ]); + + final spaceshipExitRailRamp = _SpaceshipExitRailRamp() + ..initialPosition = position; + final exitRail = SpaceshipExitRailEnd() + ..initialPosition = position + _SpaceshipExitRailRamp.exitPoint; + + addAll([ + spaceshipExitRailRamp, + exitRail, + ]); + } +} + +class _SpaceshipExitRailRamp extends BodyComponent + with InitialPosition, Layered { + _SpaceshipExitRailRamp() : super(priority: 2) { + layer = Layer.spaceshipExitRail; + // TODO(ruimiguel): remove color once asset is placed. + paint = Paint() + ..color = const Color.fromARGB(255, 249, 65, 3) + ..style = PaintingStyle.stroke; + } + + static final exitPoint = Vector2(9.2, -48.5); + + List _createFixtureDefs() { + const entranceRotationAngle = 175 * math.pi / 180; + const curveRotationAngle = 275 * math.pi / 180; + const exitRotationAngle = 340 * math.pi / 180; + const width = 5.5; + + final fixturesDefs = []; + + final entranceWall = ArcShape( + center: Vector2(width / 2, 0), + arcRadius: width / 2, + angle: math.pi, + rotation: entranceRotationAngle, + ); + final entranceFixtureDef = FixtureDef(entranceWall); + fixturesDefs.add(entranceFixtureDef); + + final topLeftControlPoints = [ + Vector2(0, 0), + Vector2(10, .5), + Vector2(7, 4), + Vector2(15.5, 8.3), + ]; + final topLeftCurveShape = BezierCurveShape( + controlPoints: topLeftControlPoints, + )..rotate(curveRotationAngle); + final topLeftFixtureDef = FixtureDef(topLeftCurveShape); + fixturesDefs.add(topLeftFixtureDef); + + final topRightControlPoints = [ + Vector2(0, width), + Vector2(10, 6.5), + Vector2(7, 10), + Vector2(15.5, 13.2), + ]; + final topRightCurveShape = BezierCurveShape( + controlPoints: topRightControlPoints, + )..rotate(curveRotationAngle); + final topRightFixtureDef = FixtureDef(topRightCurveShape); + fixturesDefs.add(topRightFixtureDef); + + final mediumLeftControlPoints = [ + topLeftControlPoints.last, + Vector2(21, 12.9), + Vector2(30, 7.1), + Vector2(32, 4.8), + ]; + final mediumLeftCurveShape = BezierCurveShape( + controlPoints: mediumLeftControlPoints, + )..rotate(curveRotationAngle); + final mediumLeftFixtureDef = FixtureDef(mediumLeftCurveShape); + fixturesDefs.add(mediumLeftFixtureDef); + + final mediumRightControlPoints = [ + topRightControlPoints.last, + Vector2(21, 17.2), + Vector2(30, 12.1), + Vector2(32, 10.2), + ]; + final mediumRightCurveShape = BezierCurveShape( + controlPoints: mediumRightControlPoints, + )..rotate(curveRotationAngle); + final mediumRightFixtureDef = FixtureDef(mediumRightCurveShape); + fixturesDefs.add(mediumRightFixtureDef); + + final bottomLeftControlPoints = [ + mediumLeftControlPoints.last, + Vector2(40, -1), + Vector2(48, 1.9), + Vector2(50.5, 2.5), + ]; + final bottomLeftCurveShape = BezierCurveShape( + controlPoints: bottomLeftControlPoints, + )..rotate(curveRotationAngle); + final bottomLeftFixtureDef = FixtureDef(bottomLeftCurveShape); + fixturesDefs.add(bottomLeftFixtureDef); + + final bottomRightControlPoints = [ + mediumRightControlPoints.last, + Vector2(40, 4), + Vector2(46, 6.5), + Vector2(48.8, 7.6), + ]; + final bottomRightCurveShape = BezierCurveShape( + controlPoints: bottomRightControlPoints, + )..rotate(curveRotationAngle); + final bottomRightFixtureDef = FixtureDef(bottomRightCurveShape); + fixturesDefs.add(bottomRightFixtureDef); + + final exitWall = ArcShape( + center: exitPoint, + arcRadius: width / 2, + angle: math.pi, + rotation: exitRotationAngle, + ); + final exitFixtureDef = FixtureDef(exitWall); + fixturesDefs.add(exitFixtureDef); + + return fixturesDefs; + } + + @override + Body createBody() { + final bodyDef = BodyDef() + ..userData = this + ..position = initialPosition; + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } +} + +/// {@template spaceship_exit_rail_end} +/// A sensor [BodyComponent] responsible for sending the [Ball] +/// back to the board. +/// {@endtemplate} +class SpaceshipExitRailEnd extends RampOpening { + /// {@macro spaceship_exit_rail_end} + SpaceshipExitRailEnd() + : super( + pathwayLayer: Layer.spaceshipExitRail, + orientation: RampOrientation.down, + ) { + layer = Layer.spaceshipExitRail; + } + + @override + Shape get shape { + return CircleShape()..radius = 1; + } +} + +/// [ContactCallback] that handles the contact between the [Ball] +/// and a [SpaceshipExitRailEnd]. +/// +/// It resets the [Ball] priority and filter data so it will "be back" on the +/// board. +class SpaceshipExitRailEndBallContactCallback + extends ContactCallback { + @override + void begin(SpaceshipExitRailEnd exitRail, Ball ball, _) { + ball + ..priority = 1 + ..gameRef.reorderChildren() + ..layer = exitRail.outsideLayer; + } +} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index e50fbd68..9673b2d2 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -53,6 +53,13 @@ class PinballGame extends Forge2DGame ), ), ); + unawaited( + addFromBlueprint( + SpaceshipExitRail( + position: Vector2(-34.3, 23.8), + ), + ), + ); // Fix camera on the center of the board. camera diff --git a/packages/pinball_components/lib/src/components/layer.dart b/packages/pinball_components/lib/src/components/layer.dart index a3f11f46..e0e64ddc 100644 --- a/packages/pinball_components/lib/src/components/layer.dart +++ b/packages/pinball_components/lib/src/components/layer.dart @@ -61,6 +61,9 @@ enum Layer { /// Collide only with Spaceship group elements. spaceship, + + /// Collide only with Spaceship exit rail group elements. + spaceshipExitRail, } /// {@template layer_mask_bits} @@ -89,6 +92,8 @@ extension LayerMaskBits on Layer { return 0x0005; case Layer.spaceship: return 0x000A; + case Layer.spaceshipExitRail: + return 0x0004; } } } diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index 257f7fcd..7e9d097e 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -32,7 +32,10 @@ class Spaceship extends Forge2DBlueprint { SpaceshipSaucer()..initialPosition = position, SpaceshipEntrance()..initialPosition = position, AndroidHead()..initialPosition = position, - SpaceshipHole()..initialPosition = position - Vector2(5.2, 4.8), + SpaceshipHole( + onExitLayer: Layer.spaceshipExitRail, + onExitElevation: 2, + )..initialPosition = position - Vector2(5.2, 4.8), SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8), SpaceshipWall()..initialPosition = position, ]); @@ -44,7 +47,8 @@ class Spaceship extends Forge2DBlueprint { /// {@endtemplate} class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_saucer} - SpaceshipSaucer() : super(priority: 2) { + // TODO(ruimiguel): apply Elevated when PR merged. + SpaceshipSaucer() : super(priority: 3) { layer = Layer.spaceship; } @@ -88,7 +92,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@endtemplate} class AndroidHead extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_bridge} - AndroidHead() : super(priority: 3) { + AndroidHead() : super(priority: 4) { layer = Layer.spaceship; } @@ -149,6 +153,10 @@ class SpaceshipEntrance extends RampOpening { layer = Layer.spaceship; } + /// Priority order for [SpaceshipHole] on enter. + // TODO(ruimiguel): apply Elevated when PR merged. + final int onEnterElevation = 4; + @override Shape get shape { renderBody = false; @@ -169,29 +177,31 @@ class SpaceshipEntrance extends RampOpening { /// {@template spaceship_hole} /// A sensor [BodyComponent] responsible for sending the [Ball] -/// back to the board. +/// out from the [Spaceship]. /// {@endtemplate} -class SpaceshipHole extends BodyComponent with InitialPosition, Layered { +class SpaceshipHole extends RampOpening { /// {@macro spaceship_hole} - SpaceshipHole() { + SpaceshipHole({Layer? onExitLayer, this.onExitElevation = 1}) + : super( + pathwayLayer: Layer.spaceship, + outsideLayer: onExitLayer, + orientation: RampOrientation.up, + ) { layer = Layer.spaceship; } - @override - Body createBody() { - renderBody = false; - final shape = ArcShape(center: Vector2(-3.5, 2), arcRadius: 6, angle: 1); + /// Priority order for [SpaceshipHole] on exit. + // TODO(ruimiguel): apply Elevated when PR merged. + final int onExitElevation; - final bodyDef = BodyDef() - ..userData = this - ..position = initialPosition - ..angle = 5.2 - ..type = BodyType.static; - - return world.createBody(bodyDef) - ..createFixture( - FixtureDef(shape)..isSensor = true, - ); + @override + Shape get shape { + return ArcShape( + center: Vector2(0, 4.2), + arcRadius: 6, + angle: 1, + rotation: 60 * pi / 180, + ); } } @@ -225,6 +235,7 @@ class _SpaceshipWallShape extends ChainShape { /// {@endtemplate} class SpaceshipWall extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_wall} + // TODO(ruimiguel): apply Elevated when PR merged SpaceshipWall() : super(priority: 4) { layer = Layer.spaceship; } @@ -258,7 +269,8 @@ class SpaceshipEntranceBallContactCallback @override void begin(SpaceshipEntrance entrance, Ball ball, _) { ball - ..priority = 3 + // TODO(ruimiguel): apply Elevated when PR merged. + ..priority = entrance.onEnterElevation ..gameRef.reorderChildren() ..layer = Layer.spaceship; } @@ -267,15 +279,16 @@ class SpaceshipEntranceBallContactCallback /// [ContactCallback] that handles the contact between the [Ball] /// and a [SpaceshipHole]. /// -/// It resets the [Ball] priority and filter data so it will "be back" on the +/// It sets the [Ball] priority and filter data so it will "be back" on the /// board. class SpaceshipHoleBallContactCallback extends ContactCallback { @override void begin(SpaceshipHole hole, Ball ball, _) { ball - ..priority = 1 + // TODO(ruimiguel): apply Elevated when PR merged. + ..priority = hole.onExitElevation ..gameRef.reorderChildren() - ..layer = Layer.board; + ..layer = hole.outsideLayer; } } diff --git a/packages/pinball_components/test/src/components/spaceship_test.dart b/packages/pinball_components/test/src/components/spaceship_test.dart index 87eb4716..4c185675 100644 --- a/packages/pinball_components/test/src/components/spaceship_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_test.dart @@ -58,16 +58,20 @@ void main() { group('SpaceshipEntranceBallContactCallback', () { test('changes the ball priority on contact', () { + when(() => entrance.onEnterElevation).thenReturn(3); + SpaceshipEntranceBallContactCallback().begin( entrance, ball, MockContact(), ); - verify(() => ball.priority = 3).called(1); + verify(() => ball.priority = entrance.onEnterElevation).called(1); }); test('re order the game children', () { + when(() => entrance.onEnterElevation).thenReturn(3); + SpaceshipEntranceBallContactCallback().begin( entrance, ball, @@ -80,16 +84,22 @@ void main() { group('SpaceshipHoleBallContactCallback', () { test('changes the ball priority on contact', () { + when(() => hole.outsideLayer).thenReturn(Layer.board); + when(() => hole.onExitElevation).thenReturn(1); + SpaceshipHoleBallContactCallback().begin( hole, ball, MockContact(), ); - verify(() => ball.priority = 1).called(1); + verify(() => ball.priority = hole.onExitElevation).called(1); }); test('re order the game children', () { + when(() => hole.outsideLayer).thenReturn(Layer.board); + when(() => hole.onExitElevation).thenReturn(1); + SpaceshipHoleBallContactCallback().begin( hole, ball, diff --git a/test/game/components/spaceship_exit_rail_test.dart b/test/game/components/spaceship_exit_rail_test.dart new file mode 100644 index 00000000..99afc808 --- /dev/null +++ b/test/game/components/spaceship_exit_rail_test.dart @@ -0,0 +1,60 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('SpaceshipExitRail', () { + late PinballGame game; + late SpaceshipExitRailEnd exitRailEnd; + late Ball ball; + late Body body; + late Fixture fixture; + late Filter filterData; + + setUp(() { + game = MockPinballGame(); + + exitRailEnd = MockSpaceshipExitRailEnd(); + + ball = MockBall(); + body = MockBody(); + when(() => ball.gameRef).thenReturn(game); + when(() => ball.body).thenReturn(body); + + fixture = MockFixture(); + filterData = MockFilter(); + when(() => body.fixtures).thenReturn([fixture]); + when(() => fixture.filterData).thenReturn(filterData); + }); + + group('SpaceshipExitHoleBallContactCallback', () { + test('changes the ball priority on contact', () { + when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board); + + SpaceshipExitRailEndBallContactCallback().begin( + exitRailEnd, + ball, + MockContact(), + ); + + verify(() => ball.priority = 1).called(1); + }); + + test('reorders the game children', () { + when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board); + + SpaceshipExitRailEndBallContactCallback().begin( + exitRailEnd, + ball, + MockContact(), + ); + + verify(game.reorderChildren).called(1); + }); + }); + }); +} diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 206b25a4..9453c93a 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -70,6 +70,8 @@ class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {} class MockSpaceshipHole extends Mock implements SpaceshipHole {} +class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {} + class MockComponentSet extends Mock implements ComponentSet {} class MockDashNestBumper extends Mock implements DashNestBumper {}