diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index bc84fb2b..5eef3538 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -21,7 +21,7 @@ export 'joint_anchor.dart'; export 'kicker/kicker.dart'; export 'launch_ramp.dart'; export 'layer.dart'; -export 'layer_sensor.dart'; +export 'layer_sensor/layer_sensor.dart'; export 'multiball/multiball.dart'; export 'multiplier/multiplier.dart'; export 'plunger.dart'; diff --git a/packages/pinball_components/lib/src/components/layer_sensor.dart b/packages/pinball_components/lib/src/components/layer_sensor.dart deleted file mode 100644 index 6b5f3832..00000000 --- a/packages/pinball_components/lib/src/components/layer_sensor.dart +++ /dev/null @@ -1,90 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template layer_entrance_orientation} -/// Determines if a layer entrance is oriented [up] or [down] on the board. -/// {@endtemplate} -enum LayerEntranceOrientation { - /// Facing up on the Board. - up, - - /// Facing down on the Board. - down, -} - -/// {@template layer_sensor} -/// [BodyComponent] located at the entrance and exit of a [Layer]. -/// -/// By default the base [layer] is set to [Layer.board] and the -/// [_outsideZIndex] is set to [ZIndexes.ballOnBoard]. -/// {@endtemplate} -abstract class LayerSensor extends BodyComponent - with InitialPosition, Layered, ContactCallbacks { - /// {@macro layer_sensor} - LayerSensor({ - required Layer insideLayer, - Layer? outsideLayer, - required int insideZIndex, - int? outsideZIndex, - required this.orientation, - }) : _insideLayer = insideLayer, - _outsideLayer = outsideLayer ?? Layer.board, - _insideZIndex = insideZIndex, - _outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard, - super(renderBody: false) { - layer = Layer.opening; - } - - final Layer _insideLayer; - final Layer _outsideLayer; - final int _insideZIndex; - final int _outsideZIndex; - - /// The [Shape] of the [LayerSensor]. - Shape get shape; - - /// {@macro layer_entrance_orientation} - // TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for - // collision calculations. - final LayerEntranceOrientation orientation; - - @override - Body createBody() { - final fixtureDef = FixtureDef( - shape, - isSensor: true, - ); - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); - if (other is! Ball) return; - - if (other.layer != _insideLayer) { - final isBallEnteringOpening = - (orientation == LayerEntranceOrientation.down && - other.body.linearVelocity.y < 0) || - (orientation == LayerEntranceOrientation.up && - other.body.linearVelocity.y > 0); - - if (isBallEnteringOpening) { - other - ..layer = _insideLayer - ..zIndex = _insideZIndex; - } - } else { - other - ..layer = _outsideLayer - ..zIndex = _outsideZIndex; - } - } -} diff --git a/packages/pinball_components/lib/src/components/layer_sensor/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/layer_sensor/behaviors/behaviors.dart new file mode 100644 index 00000000..060e313d --- /dev/null +++ b/packages/pinball_components/lib/src/components/layer_sensor/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'behaviors.dart'; +export 'layer_filtering_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart b/packages/pinball_components/lib/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart new file mode 100644 index 00000000..06dca4b6 --- /dev/null +++ b/packages/pinball_components/lib/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart @@ -0,0 +1,31 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class LayerFilteringBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + if (other.layer != parent.insideLayer) { + final isBallEnteringOpening = + (parent.orientation == LayerEntranceOrientation.down && + other.body.linearVelocity.y < 0) || + (parent.orientation == LayerEntranceOrientation.up && + other.body.linearVelocity.y > 0); + + if (isBallEnteringOpening) { + other + ..layer = parent.insideLayer + ..zIndex = parent.insideZIndex; + } + } else { + other + ..layer = parent.outsideLayer + ..zIndex = parent.outsideZIndex; + } + } +} diff --git a/packages/pinball_components/lib/src/components/layer_sensor/layer_sensor.dart b/packages/pinball_components/lib/src/components/layer_sensor/layer_sensor.dart new file mode 100644 index 00000000..4b1d6ae3 --- /dev/null +++ b/packages/pinball_components/lib/src/components/layer_sensor/layer_sensor.dart @@ -0,0 +1,66 @@ +// ignore_for_file: avoid_renaming_method_parameters, public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart'; + +/// {@template layer_entrance_orientation} +/// Determines if a layer entrance is oriented [up] or [down] on the board. +/// {@endtemplate} +enum LayerEntranceOrientation { + /// Facing up on the Board. + up, + + /// Facing down on the Board. + down, +} + +/// {@template layer_sensor} +/// [BodyComponent] located at the entrance and exit of a [Layer]. +/// +/// By default the base [layer] is set to [Layer.board] and the +/// [outsideZIndex] is set to [ZIndexes.ballOnBoard]. +/// {@endtemplate} +abstract class LayerSensor extends BodyComponent with InitialPosition, Layered { + /// {@macro layer_sensor} + LayerSensor({ + required this.insideLayer, + Layer? outsideLayer, + required this.insideZIndex, + int? outsideZIndex, + required this.orientation, + }) : outsideLayer = outsideLayer ?? Layer.board, + outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard, + super( + renderBody: false, + children: [LayerFilteringBehavior()], + ) { + layer = Layer.opening; + } + + final Layer insideLayer; + + final Layer outsideLayer; + + final int insideZIndex; + + final int outsideZIndex; + + /// The [Shape] of the [LayerSensor]. + Shape get shape; + + /// {@macro layer_entrance_orientation} + // TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for + // collision calculations. + final LayerEntranceOrientation orientation; + + @override + Body createBody() { + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef(position: initialPosition); + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/packages/pinball_components/test/src/components/layer_sensor/behavior/layer_filtering_behavior_test.dart b/packages/pinball_components/test/src/components/layer_sensor/behavior/layer_filtering_behavior_test.dart new file mode 100644 index 00000000..b7bc308b --- /dev/null +++ b/packages/pinball_components/test/src/components/layer_sensor/behavior/layer_filtering_behavior_test.dart @@ -0,0 +1,136 @@ +// 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/pinball_components.dart'; +import 'package:pinball_components/src/components/layer_sensor/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _TestLayerSensor extends LayerSensor { + _TestLayerSensor({ + required LayerEntranceOrientation orientation, + required int insideZIndex, + required Layer insideLayer, + }) : super( + insideLayer: insideLayer, + insideZIndex: insideZIndex, + orientation: orientation, + ); + + @override + Shape get shape => PolygonShape()..setAsBoxXY(1, 1); +} + +class _MockBall extends Mock implements Ball {} + +class _MockBody extends Mock implements Body {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'LayerSensorBehavior', + () { + test('can be instantiated', () { + expect( + LayerFilteringBehavior(), + isA(), + ); + }); + + flameTester.test( + 'loads', + (game) async { + final behavior = LayerFilteringBehavior(); + final parent = _TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insideZIndex: 1, + insideLayer: Layer.spaceshipEntranceRamp, + ); + + await parent.add(behavior); + await game.ensureAdd(parent); + + expect(game.contains(parent), isTrue); + }, + ); + + group('beginContact', () { + late Ball ball; + late Body body; + late int insideZIndex; + late Layer insideLayer; + + setUp(() { + ball = _MockBall(); + body = _MockBody(); + insideZIndex = 1; + insideLayer = Layer.spaceshipEntranceRamp; + + when(() => ball.body).thenReturn(body); + when(() => ball.layer).thenReturn(Layer.board); + }); + + flameTester.test( + 'changes ball layer and zIndex ' + 'when a ball enters and exits a downward oriented LayerSensor', + (game) async { + final parent = _TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insideZIndex: 1, + insideLayer: insideLayer, + )..initialPosition = Vector2(0, 10); + final behavior = LayerFilteringBehavior(); + + await parent.add(behavior); + await game.ensureAdd(parent); + + when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); + + behavior.beginContact(ball, _MockContact()); + verify(() => ball.layer = insideLayer).called(1); + verify(() => ball.zIndex = insideZIndex).called(1); + + when(() => ball.layer).thenReturn(insideLayer); + + behavior.beginContact(ball, _MockContact()); + verify(() => ball.layer = Layer.board); + verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); + }); + + flameTester.test( + 'changes ball layer and zIndex ' + 'when a ball enters and exits an upward oriented LayerSensor', + (game) async { + final parent = _TestLayerSensor( + orientation: LayerEntranceOrientation.up, + insideZIndex: 1, + insideLayer: insideLayer, + )..initialPosition = Vector2(0, 10); + final behavior = LayerFilteringBehavior(); + + await parent.add(behavior); + await game.ensureAdd(parent); + + when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); + + behavior.beginContact(ball, _MockContact()); + verify(() => ball.layer = insideLayer).called(1); + verify(() => ball.zIndex = 1).called(1); + + when(() => ball.layer).thenReturn(insideLayer); + + behavior.beginContact(ball, _MockContact()); + verify(() => ball.layer = Layer.board); + verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); + }); + }); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/layer_sensor_test.dart b/packages/pinball_components/test/src/components/layer_sensor/layer_sensor_test.dart similarity index 59% rename from packages/pinball_components/test/src/components/layer_sensor_test.dart rename to packages/pinball_components/test/src/components/layer_sensor/layer_sensor_test.dart index cfd19bb0..dd32ad56 100644 --- a/packages/pinball_components/test/src/components/layer_sensor_test.dart +++ b/packages/pinball_components/test/src/components/layer_sensor/layer_sensor_test.dart @@ -2,16 +2,10 @@ 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/pinball_components.dart'; +import 'package:pinball_components/src/components/layer_sensor/behaviors/behaviors.dart'; -import '../../helpers/helpers.dart'; - -class _MockBall extends Mock implements Ball {} - -class _MockBody extends Mock implements Body {} - -class _MockContact extends Mock implements Contact {} +import '../../../helpers/helpers.dart'; class TestLayerSensor extends LayerSensor { TestLayerSensor({ @@ -112,68 +106,22 @@ void main() { ); }); }); - }); - - group('beginContact', () { - late Ball ball; - late Body body; - late int insideZIndex; - late Layer insideLayer; - - setUp(() { - ball = _MockBall(); - body = _MockBody(); - insideZIndex = 1; - insideLayer = Layer.spaceshipEntranceRamp; - - when(() => ball.body).thenReturn(body); - when(() => ball.layer).thenReturn(Layer.board); - }); - - flameTester.test( - 'changes ball layer and zIndex ' - 'when a ball enters and exits a downward oriented LayerSensor', - (game) async { - final sensor = TestLayerSensor( - orientation: LayerEntranceOrientation.down, - insideZIndex: insidePriority, - insideLayer: insideLayer, - )..initialPosition = Vector2(0, 10); - - when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); - - sensor.beginContact(ball, _MockContact()); - verify(() => ball.layer = insideLayer).called(1); - verify(() => ball.zIndex = insideZIndex).called(1); - - when(() => ball.layer).thenReturn(insideLayer); - - sensor.beginContact(ball, _MockContact()); - verify(() => ball.layer = Layer.board); - verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); - }); flameTester.test( - 'changes ball layer and zIndex ' - 'when a ball enters and exits an upward oriented LayerSensor', - (game) async { - final sensor = TestLayerSensor( - orientation: LayerEntranceOrientation.up, - insideZIndex: insidePriority, - insideLayer: insideLayer, - )..initialPosition = Vector2(0, 10); - - when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - - sensor.beginContact(ball, _MockContact()); - verify(() => ball.layer = insideLayer).called(1); - verify(() => ball.zIndex = insidePriority).called(1); - - when(() => ball.layer).thenReturn(insideLayer); + 'adds a LayerFilteringBehavior', + (game) async { + final layerSensor = TestLayerSensor( + orientation: LayerEntranceOrientation.down, + insideZIndex: insidePriority, + insideLayer: Layer.spaceshipEntranceRamp, + ); + await game.ensureAdd(layerSensor); - sensor.beginContact(ball, _MockContact()); - verify(() => ball.layer = Layer.board); - verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); - }); + expect( + layerSensor.children.whereType().length, + equals(1), + ); + }, + ); }); }