diff --git a/lib/game/components/jetpack_ramp.dart b/lib/game/components/jetpack_ramp.dart index 4c4c8be9..aa5a9dbd 100644 --- a/lib/game/components/jetpack_ramp.dart +++ b/lib/game/components/jetpack_ramp.dart @@ -107,7 +107,7 @@ class _JetpackRampOpening extends RampOpening { required double rotation, }) : _rotation = rotation, super( - pathwayLayer: Layer.jetpack, + insideLayer: Layer.jetpack, outsideLayer: outsideLayer, orientation: RampOrientation.down, ); diff --git a/lib/game/components/launcher_ramp.dart b/lib/game/components/launcher_ramp.dart index 79b7c831..c05f8aa2 100644 --- a/lib/game/components/launcher_ramp.dart +++ b/lib/game/components/launcher_ramp.dart @@ -122,7 +122,7 @@ class _LauncherRampOpening extends RampOpening { required double rotation, }) : _rotation = rotation, super( - pathwayLayer: Layer.launcher, + insideLayer: Layer.launcher, orientation: RampOrientation.down, ); diff --git a/lib/game/components/spaceship_exit_rail.dart b/lib/game/components/spaceship_exit_rail.dart index 0dc38322..4a6c44cd 100644 --- a/lib/game/components/spaceship_exit_rail.dart +++ b/lib/game/components/spaceship_exit_rail.dart @@ -169,7 +169,7 @@ class SpaceshipExitRailEnd extends RampOpening { /// {@macro spaceship_exit_rail_end} SpaceshipExitRailEnd() : super( - pathwayLayer: Layer.spaceshipExitRail, + insideLayer: Layer.spaceshipExitRail, orientation: RampOrientation.down, ) { layer = Layer.spaceshipExitRail; diff --git a/packages/pinball_components/lib/src/components/ramp_opening.dart b/packages/pinball_components/lib/src/components/ramp_opening.dart index 8f33e813..cb6066f2 100644 --- a/packages/pinball_components/lib/src/components/ramp_opening.dart +++ b/packages/pinball_components/lib/src/components/ramp_opening.dart @@ -20,28 +20,41 @@ enum RampOrientation { /// [RampOpeningBallContactCallback] detects when a [Ball] passes /// through this opening. /// -/// By default the base [layer] is set to [Layer.board]. +/// By default the base [layer] is set to [Layer.board] and the +/// [outsidePriority] is set to the lowest possible [Layer]. /// {@endtemplate} // TODO(ruialonso): Consider renaming the class. abstract class RampOpening extends BodyComponent with InitialPosition, Layered { /// {@macro ramp_opening} RampOpening({ - required Layer pathwayLayer, + required Layer insideLayer, Layer? outsideLayer, + int? insidePriority, + int? outsidePriority, required this.orientation, - }) : _pathwayLayer = pathwayLayer, - _outsideLayer = outsideLayer ?? Layer.board { - layer = Layer.board; + }) : _insideLayer = insideLayer, + _outsideLayer = outsideLayer ?? Layer.board, + _insidePriority = insidePriority ?? 0, + _outsidePriority = outsidePriority ?? 0 { + layer = Layer.opening; } - final Layer _pathwayLayer; + final Layer _insideLayer; final Layer _outsideLayer; + final int _insidePriority; + final int _outsidePriority; - /// Mask of category bits for collision inside pathway. - Layer get pathwayLayer => _pathwayLayer; + /// Mask of category bits for collision inside ramp. + Layer get insideLayer => _insideLayer; - /// Mask of category bits for collision outside pathway. + /// Mask of category bits for collision outside ramp. Layer get outsideLayer => _outsideLayer; + /// Priority for the [Ball] inside ramp. + int get insidePriority => _insidePriority; + + /// Priority for the [Ball] outside ramp. + int get outsidePriority => _outsidePriority; + /// The [Shape] of the [RampOpening]. Shape get shape; @@ -64,8 +77,7 @@ abstract class RampOpening extends BodyComponent with InitialPosition, Layered { } /// {@template ramp_opening_ball_contact_callback} -/// Detects when a [Ball] enters or exits a pathway ramp through a -/// [RampOpening]. +/// Detects when a [Ball] enters or exits a ramp through a [RampOpening]. /// /// Modifies [Ball]'s [Layer] accordingly depending on whether the [Ball] is /// outside or inside a ramp. @@ -80,9 +92,11 @@ class RampOpeningBallContactCallback Layer layer; if (!_ballsInside.contains(ball)) { - layer = opening.pathwayLayer; + layer = opening.insideLayer; _ballsInside.add(ball); - ball.layer = layer; + ball + ..sendTo(opening.insidePriority) + ..layer = layer; } else { _ballsInside.remove(ball); } @@ -103,7 +117,9 @@ class RampOpeningBallContactCallback ball.body.linearVelocity.y > 0); if (isBallOutsideOpening) { - ball.layer = opening.outsideLayer; + ball + ..sendTo(opening.outsidePriority) + ..layer = opening.outsideLayer; _ballsInside.remove(ball); } } diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index 7e9d097e..4d84eb68 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -21,6 +21,9 @@ class Spaceship extends Forge2DBlueprint { /// The [position] where the elements will be created final Vector2 position; + /// Base priority for wall while be on spaceship. + static const ballPriorityWhenOnSpaceship = 4; + @override void build(_) { addAllContactCallback([ @@ -33,8 +36,8 @@ class Spaceship extends Forge2DBlueprint { SpaceshipEntrance()..initialPosition = position, AndroidHead()..initialPosition = position, SpaceshipHole( - onExitLayer: Layer.spaceshipExitRail, - onExitElevation: 2, + outsideLayer: Layer.spaceshipExitRail, + outsidePriority: 2, )..initialPosition = position - Vector2(5.2, 4.8), SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8), SpaceshipWall()..initialPosition = position, @@ -47,8 +50,8 @@ class Spaceship extends Forge2DBlueprint { /// {@endtemplate} class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_saucer} - // TODO(ruimiguel): apply Elevated when PR merged. - SpaceshipSaucer() : super(priority: 3) { + SpaceshipSaucer() + : super(priority: Spaceship.ballPriorityWhenOnSpaceship - 1) { layer = Layer.spaceship; } @@ -92,7 +95,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { /// {@endtemplate} class AndroidHead extends BodyComponent with InitialPosition, Layered { /// {@macro spaceship_bridge} - AndroidHead() : super(priority: 4) { + AndroidHead() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) { layer = Layer.spaceship; } @@ -147,16 +150,13 @@ class SpaceshipEntrance extends RampOpening { /// {@macro spaceship_entrance} SpaceshipEntrance() : super( - pathwayLayer: Layer.spaceship, + insideLayer: Layer.spaceship, orientation: RampOrientation.up, + insidePriority: Spaceship.ballPriorityWhenOnSpaceship, ) { 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; @@ -181,19 +181,17 @@ class SpaceshipEntrance extends RampOpening { /// {@endtemplate} class SpaceshipHole extends RampOpening { /// {@macro spaceship_hole} - SpaceshipHole({Layer? onExitLayer, this.onExitElevation = 1}) + SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1}) : super( - pathwayLayer: Layer.spaceship, - outsideLayer: onExitLayer, + insideLayer: Layer.spaceship, + outsideLayer: outsideLayer, + outsidePriority: outsidePriority, orientation: RampOrientation.up, ) { + renderBody = false; layer = Layer.spaceship; } - /// Priority order for [SpaceshipHole] on exit. - // TODO(ruimiguel): apply Elevated when PR merged. - final int onExitElevation; - @override Shape get shape { return ArcShape( @@ -235,8 +233,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) { + SpaceshipWall() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) { layer = Layer.spaceship; } @@ -269,9 +266,7 @@ class SpaceshipEntranceBallContactCallback @override void begin(SpaceshipEntrance entrance, Ball ball, _) { ball - // TODO(ruimiguel): apply Elevated when PR merged. - ..priority = entrance.onEnterElevation - ..gameRef.reorderChildren() + ..sendTo(entrance.insidePriority) ..layer = Layer.spaceship; } } @@ -279,16 +274,14 @@ class SpaceshipEntranceBallContactCallback /// [ContactCallback] that handles the contact between the [Ball] /// and a [SpaceshipHole]. /// -/// It sets the [Ball] priority and filter data so it will "be back" on the -/// board. +/// It sets the [Ball] priority and filter data so it will outside of the +/// [Spaceship]. class SpaceshipHoleBallContactCallback extends ContactCallback { @override void begin(SpaceshipHole hole, Ball ball, _) { ball - // TODO(ruimiguel): apply Elevated when PR merged. - ..priority = hole.onExitElevation - ..gameRef.reorderChildren() + ..sendTo(hole.outsidePriority) ..layer = hole.outsideLayer; } } diff --git a/packages/pinball_components/lib/src/flame/flame.dart b/packages/pinball_components/lib/src/flame/flame.dart index c46a6fd2..9af8dba6 100644 --- a/packages/pinball_components/lib/src/flame/flame.dart +++ b/packages/pinball_components/lib/src/flame/flame.dart @@ -1 +1,2 @@ export 'blueprint.dart'; +export 'priority.dart'; diff --git a/packages/pinball_components/lib/src/flame/priority.dart b/packages/pinball_components/lib/src/flame/priority.dart new file mode 100644 index 00000000..f4dccabf --- /dev/null +++ b/packages/pinball_components/lib/src/flame/priority.dart @@ -0,0 +1,39 @@ +import 'dart:math' as math; +import 'package:flame/components.dart'; + +/// Helper methods to change the [priority] of a [Component]. +extension ComponentPriorityX on Component { + static const _lowestPriority = 0; + + /// Changes the priority to a specific one. + void sendTo(int destinationPriority) { + if (priority != destinationPriority) { + priority = math.max(destinationPriority, _lowestPriority); + reorderChildren(); + } + } + + /// Changes the priority to the lowest possible. + void sendToBack() { + if (priority != _lowestPriority) { + priority = _lowestPriority; + reorderChildren(); + } + } + + /// Decreases the priority to be lower than another [Component]. + void showBehindOf(Component other) { + if (priority >= other.priority) { + priority = math.max(other.priority - 1, _lowestPriority); + reorderChildren(); + } + } + + /// Increases the priority to be higher than another [Component]. + void showInFrontOf(Component other) { + if (priority <= other.priority) { + priority = other.priority + 1; + reorderChildren(); + } + } +} diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index 8d6f45b3..c36afff2 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -24,3 +25,5 @@ class MockContact extends Mock implements Contact {} class MockContactCallback extends Mock implements ContactCallback {} + +class MockComponent extends Mock implements Component {} diff --git a/packages/pinball_components/test/src/components/ramp_opening_test.dart b/packages/pinball_components/test/src/components/ramp_opening_test.dart index c49e9164..cb42203a 100644 --- a/packages/pinball_components/test/src/components/ramp_opening_test.dart +++ b/packages/pinball_components/test/src/components/ramp_opening_test.dart @@ -12,7 +12,7 @@ class TestRampOpening extends RampOpening { required RampOrientation orientation, required Layer pathwayLayer, }) : super( - pathwayLayer: pathwayLayer, + insideLayer: pathwayLayer, orientation: orientation, ); @@ -129,14 +129,12 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); }); flameTester.test( @@ -152,14 +150,12 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); }); flameTester.test( @@ -174,15 +170,13 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); callback.end(ball, area, MockContact()); verify(() => ball.layer = Layer.board); @@ -200,15 +194,13 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); callback.end(ball, area, MockContact()); verify(() => ball.layer = Layer.board); @@ -226,21 +218,19 @@ void main() { final callback = TestRampOpeningBallContactCallback(); when(() => ball.body).thenReturn(body); + when(() => ball.priority).thenReturn(1); when(() => body.position).thenReturn(Vector2.zero()); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => ball.layer).thenReturn(Layer.board); - await game.ready(); - await game.ensureAdd(area); - callback.begin(ball, area, MockContact()); - verify(() => ball.layer = area.pathwayLayer).called(1); + verify(() => ball.layer = area.insideLayer).called(1); callback.end(ball, area, MockContact()); verifyNever(() => ball.layer = Layer.board); callback.begin(ball, area, MockContact()); - verifyNever(() => ball.layer = area.pathwayLayer); + verifyNever(() => ball.layer = area.insideLayer); callback.end(ball, area, MockContact()); verify(() => ball.layer = Layer.board); diff --git a/packages/pinball_components/test/src/components/spaceship_test.dart b/packages/pinball_components/test/src/components/spaceship_test.dart index f89408f7..4d980c69 100644 --- a/packages/pinball_components/test/src/components/spaceship_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_test.dart @@ -59,7 +59,8 @@ void main() { group('SpaceshipEntranceBallContactCallback', () { test('changes the ball priority on contact', () { - when(() => entrance.onEnterElevation).thenReturn(3); + when(() => ball.priority).thenReturn(2); + when(() => entrance.insidePriority).thenReturn(3); SpaceshipEntranceBallContactCallback().begin( entrance, @@ -67,39 +68,15 @@ void main() { MockContact(), ); - verify(() => ball.priority = entrance.onEnterElevation).called(1); - }); - - test('re order the game children', () { - when(() => entrance.onEnterElevation).thenReturn(3); - - SpaceshipEntranceBallContactCallback().begin( - entrance, - ball, - MockContact(), - ); - - verify(game.reorderChildren).called(1); + verify(() => ball.sendTo(entrance.insidePriority)).called(1); }); }); group('SpaceshipHoleBallContactCallback', () { test('changes the ball priority on contact', () { + when(() => ball.priority).thenReturn(2); when(() => hole.outsideLayer).thenReturn(Layer.board); - when(() => hole.onExitElevation).thenReturn(1); - - SpaceshipHoleBallContactCallback().begin( - hole, - ball, - MockContact(), - ); - - verify(() => ball.priority = hole.onExitElevation).called(1); - }); - - test('re order the game children', () { - when(() => hole.outsideLayer).thenReturn(Layer.board); - when(() => hole.onExitElevation).thenReturn(1); + when(() => hole.outsidePriority).thenReturn(1); SpaceshipHoleBallContactCallback().begin( hole, @@ -107,7 +84,7 @@ void main() { MockContact(), ); - verify(game.reorderChildren).called(1); + verify(() => ball.sendTo(hole.outsidePriority)).called(1); }); }); }); diff --git a/packages/pinball_components/test/src/flame/priority_test.dart b/packages/pinball_components/test/src/flame/priority_test.dart new file mode 100644 index 00000000..231c7744 --- /dev/null +++ b/packages/pinball_components/test/src/flame/priority_test.dart @@ -0,0 +1,221 @@ +// 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:mocktail/mocktail.dart'; +import 'package:pinball_components/src/flame/priority.dart'; + +import '../../helpers/helpers.dart'; + +class TestBodyComponent extends BodyComponent { + @override + Body createBody() { + final fixtureDef = FixtureDef(CircleShape()); + return world.createBody(BodyDef())..createFixture(fixtureDef); + } +} + +void main() { + final flameTester = FlameTester(Forge2DGame.new); + + group('ComponentPriorityX', () { + group('sendTo', () { + flameTester.test( + 'changes the priority correctly to other level', + (game) async { + const newPriority = 5; + final component = TestBodyComponent()..priority = 4; + + component.sendTo(newPriority); + + expect(component.priority, equals(newPriority)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the new priority is different', + (game) async { + const newPriority = 5; + final component = MockComponent(); + when(() => component.priority).thenReturn(4); + + component.sendTo(newPriority); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is the same", + (game) async { + const newPriority = 5; + final component = MockComponent(); + when(() => component.priority).thenReturn(newPriority); + + component.sendTo(newPriority); + + verifyNever(component.reorderChildren); + }, + ); + }); + + group('sendToBack', () { + flameTester.test( + 'changes the priority correctly to board level', + (game) async { + final component = TestBodyComponent()..priority = 4; + + component.sendToBack(); + + expect(component.priority, equals(0)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the priority is greater than lowest level', + (game) async { + final component = MockComponent(); + when(() => component.priority).thenReturn(4); + + component.sendToBack(); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is the lowest level", + (game) async { + final component = MockComponent(); + when(() => component.priority).thenReturn(0); + + component.sendToBack(); + + verifyNever(component.reorderChildren); + }, + ); + }); + + group('showBehindOf', () { + flameTester.test( + 'changes the priority if it is greater than other component', + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority - 1; + + component.showBehindOf(otherComponent); + + expect(component.priority, equals(otherComponent.priority - 1)); + }, + ); + + flameTester.test( + "doesn't change the priority if it is lower than other component", + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority + 1; + + component.showBehindOf(otherComponent); + + expect(component.priority, equals(startPriority)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the priority is greater than other component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority - 1); + + component.showBehindOf(otherComponent); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is lower than other " + 'component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority + 1); + + component.showBehindOf(otherComponent); + + verifyNever(component.reorderChildren); + }, + ); + }); + + group('showInFrontOf', () { + flameTester.test( + 'changes the priority if it is lower than other component', + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority + 1; + + component.showInFrontOf(otherComponent); + + expect(component.priority, equals(otherComponent.priority + 1)); + }, + ); + + flameTester.test( + "doesn't change the priority if it is greater than other component", + (game) async { + const startPriority = 2; + final component = TestBodyComponent()..priority = startPriority; + final otherComponent = TestBodyComponent() + ..priority = startPriority - 1; + + component.showInFrontOf(otherComponent); + + expect(component.priority, equals(startPriority)); + }, + ); + + flameTester.test( + 'calls reorderChildren if the priority is lower than other component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority + 1); + + component.showInFrontOf(otherComponent); + + verify(component.reorderChildren).called(1); + }, + ); + + flameTester.test( + "doesn't call reorderChildren if the priority is greater than other " + 'component', + (game) async { + const startPriority = 2; + final component = MockComponent(); + final otherComponent = MockComponent(); + when(() => component.priority).thenReturn(startPriority); + when(() => otherComponent.priority).thenReturn(startPriority - 1); + + component.showInFrontOf(otherComponent); + + verifyNever(component.reorderChildren); + }, + ); + }); + }); +} diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 9453c93a..fbe8edfb 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -66,10 +66,6 @@ class MockFilter extends Mock implements Filter {} class MockFixture extends Mock implements Fixture {} -class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {} - -class MockSpaceshipHole extends Mock implements SpaceshipHole {} - class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {} class MockComponentSet extends Mock implements ComponentSet {}