From 323756fd7346f9e6f9161d1a73c36efcadcf0851 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 2 May 2022 14:51:45 +0200 Subject: [PATCH] feat: spaceship ramp added cubit and behavior to sensor --- .../lib/src/components/components.dart | 2 +- .../spaceship_ramp/behavior/behavior.dart | 1 + .../behavior/ramp_shot_behavior.dart | 28 +++++ .../cubit/ramp_sensor_cubit.dart | 28 +++++ .../cubit/ramp_sensor_state.dart | 35 ++++++ .../{ => spaceship_ramp}/spaceship_ramp.dart | 73 ++++++++++++ .../test/helpers/mocks.dart | 2 + .../behavior/ramp_shot_behavior_test.dart | 103 ++++++++++++++++ .../cubit/ramp_sensor_cubit_test.dart | 66 +++++++++++ .../cubit/ramp_sensor_state_test.dart | 110 ++++++++++++++++++ 10 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart create mode 100644 packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_shot_behavior.dart create mode 100644 packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_cubit.dart create mode 100644 packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_state.dart rename packages/pinball_components/lib/src/components/{ => spaceship_ramp}/spaceship_ramp.dart (84%) create mode 100644 packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_shot_behavior_test.dart create mode 100644 packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_cubit_test.dart create mode 100644 packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_state_test.dart diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 43ba302f..f3570808 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -29,7 +29,7 @@ export 'shapes/shapes.dart'; export 'signpost.dart'; export 'slingshot.dart'; export 'spaceship_rail.dart'; -export 'spaceship_ramp.dart'; +export 'spaceship_ramp/spaceship_ramp.dart'; export 'sparky_animatronic.dart'; export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_computer.dart'; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart new file mode 100644 index 00000000..ffa738cc --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/behavior.dart @@ -0,0 +1 @@ +export 'ramp_shot_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_shot_behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_shot_behavior.dart new file mode 100644 index 00000000..15875e04 --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_shot_behavior.dart @@ -0,0 +1,28 @@ +// 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'; + +/// {@template ramp_shot_behavior} +/// Detects a [Ball]that enters in the [SpaceshipRamp]. +/// +/// The [Ball] can hit with sensor at door or sensor inside, just to recognize +/// when if [Ball] comes from out. +/// {@endtemplate} +class RampShotBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + + if (other is! Ball) return; + switch (parent.type) { + case RampSensorType.door: + parent.bloc.onDoor(other); + break; + case RampSensorType.inside: + parent.bloc.onInside(other); + break; + } + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_cubit.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_cubit.dart new file mode 100644 index 00000000..bcbd7737 --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_cubit.dart @@ -0,0 +1,28 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:pinball_components/pinball_components.dart'; + +part 'ramp_sensor_state.dart'; + +class RampSensorCubit extends Cubit { + RampSensorCubit() : super(const RampSensorState.initial()); + + void onDoor(Ball ball) { + emit( + state.copyWith( + type: RampSensorType.door, + ball: ball, + ), + ); + } + + void onInside(Ball ball) { + emit( + state.copyWith( + type: RampSensorType.inside, + ball: ball, + ), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_state.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_state.dart new file mode 100644 index 00000000..8583c067 --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_state.dart @@ -0,0 +1,35 @@ +// ignore_for_file: public_member_api_docs + +part of 'ramp_sensor_cubit.dart'; + +/// Used to know when a [Ball] gets into the [SpaceshipRamp] against every ball +/// that crosses the opening. +enum RampSensorType { + /// Sensor at the entrance of the opening. + door, + + /// Sensor inside the [SpaceshipRamp]. + inside, +} + +class RampSensorState { + const RampSensorState({ + required this.type, + this.ball, + }); + + const RampSensorState.initial() : this(type: RampSensorType.door); + + final RampSensorType type; + final Ball? ball; + + RampSensorState copyWith({ + RampSensorType? type, + Ball? ball, + }) { + return RampSensorState( + type: type ?? this.type, + ball: ball ?? this.ball, + ); + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart similarity index 84% rename from packages/pinball_components/lib/src/components/spaceship_ramp.dart rename to packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart index c1be0943..472752cd 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart @@ -5,8 +5,11 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; +export 'cubit/ramp_sensor_cubit.dart'; + /// {@template spaceship_ramp} /// Ramp leading into the [AndroidSpaceship]. /// {@endtemplate} @@ -15,6 +18,12 @@ class SpaceshipRamp extends Component { SpaceshipRamp() : super( children: [ + // TODO(ruimiguel): refactor RampSensor and RampOpening to be in + // only one sensor. + RampSensor(type: RampSensorType.door) + ..initialPosition = Vector2(1.7, -20.4), + RampSensor(type: RampSensorType.inside) + ..initialPosition = Vector2(1.7, -22), _SpaceshipRampOpening( outsidePriority: ZIndexes.ballOnBoard, rotation: math.pi, @@ -373,3 +382,67 @@ class _SpaceshipRampOpening extends LayerSensor { ); } } + +/// {@template ramp_sensor} +/// Small sensor body used to detect when a ball has entered the +/// [SpaceshipRamp]. +/// {@endtemplate} +@visibleForTesting +class RampSensor extends BodyComponent + with ParentIsA, InitialPosition, Layered { + /// {@macro ramp_sensor} + RampSensor({required this.type}) + : bloc = RampSensorCubit(), + super( + children: [ + RampShotBehavior(), + ], + renderBody: true, + ) { + layer = Layer.spaceshipEntranceRamp; + } + + /// Creates a [RampSensor] without any children. + /// + @visibleForTesting + RampSensor.test({ + required this.type, + required this.bloc, + }); + + /// Type for the sensor, to know if it's the one at the door or inside ramp. + final RampSensorType type; + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final RampSensorCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = PolygonShape() + ..setAsBox( + 2.6, + .5, + initialPosition, + -5 * math.pi / 180, + ); + + final fixtureDef = FixtureDef( + shape, + isSensor: true, + ); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index ab867e3b..58354c90 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -28,3 +28,5 @@ class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} class MockMultiplierCubit extends Mock implements MultiplierCubit {} class MockChromeDinoCubit extends Mock implements ChromeDinoCubit {} + +class MockRampSensorCubit extends Mock implements RampSensorCubit {} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_shot_behavior_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_shot_behavior_test.dart new file mode 100644 index 00000000..101e14e9 --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_shot_behavior_test.dart @@ -0,0 +1,103 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.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/spaceship_ramp/behavior/behavior.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + ]; + + final flameTester = FlameTester(() => TestGame(assets)); + + group( + 'RampShotBehavior', + () { + test('can be instantiated', () { + expect( + RampShotBehavior(), + isA(), + ); + }); + + flameTester.test( + "beginContact with door sensor calls bloc 'onDoor'", + (game) async { + final ball = Ball(baseColor: Colors.red); + final behavior = RampShotBehavior(); + final bloc = MockRampSensorCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: RampSensorState( + type: RampSensorType.door, + ball: ball, + ), + ); + + final rampSensor = RampSensor.test( + type: RampSensorType.door, + bloc: bloc, + ); + final spaceshipRamp = SpaceshipRamp(); + + await spaceshipRamp.add(rampSensor); + await game.ensureAddAll([spaceshipRamp, ball]); + await rampSensor.add(behavior); + + behavior.beginContact(ball, MockContact()); + + verify(() => bloc.onDoor(ball)).called(1); + }, + ); + + flameTester.test( + "beginContact with inside sensor calls bloc 'onInside'", + (game) async { + final ball = Ball(baseColor: Colors.red); + final behavior = RampShotBehavior(); + final bloc = MockRampSensorCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: RampSensorState( + type: RampSensorType.inside, + ball: ball, + ), + ); + + final rampSensor = RampSensor.test( + type: RampSensorType.inside, + bloc: bloc, + ); + final spaceshipRamp = SpaceshipRamp(); + + await spaceshipRamp.add(rampSensor); + await game.ensureAddAll([spaceshipRamp, ball]); + await rampSensor.add(behavior); + + behavior.beginContact(ball, MockContact()); + + verify(() => bloc.onInside(ball)).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_cubit_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_cubit_test.dart new file mode 100644 index 00000000..81f02f60 --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_cubit_test.dart @@ -0,0 +1,66 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('RampSensorCubit', () { + final ball = Ball(baseColor: Colors.red); + + blocTest( + 'onDoor emits [door]', + build: RampSensorCubit.new, + act: (bloc) => bloc.onDoor(ball), + expect: () => [ + isA() + ..having((state) => state.type, 'type', RampSensorType.door) + ..having((state) => state.ball, 'ball', ball), + ], + ); + + blocTest( + 'onDoor twice emits [door, door]', + build: RampSensorCubit.new, + act: (bloc) => bloc + ..onDoor(ball) + ..onDoor(ball), + expect: () => [ + isA() + ..having((state) => state.type, 'type', RampSensorType.door) + ..having((state) => state.ball, 'ball', ball), + isA() + ..having((state) => state.type, 'type', RampSensorType.door) + ..having((state) => state.ball, 'ball', ball), + ], + ); + + blocTest( + 'onInside emits [inside]', + build: RampSensorCubit.new, + act: (bloc) => bloc.onInside(ball), + expect: () => [ + isA() + ..having((state) => state.type, 'type', RampSensorType.inside) + ..having((state) => state.ball, 'ball', ball), + ], + ); + + blocTest( + 'onInside twice emits [inside,inside]', + build: RampSensorCubit.new, + act: (bloc) => bloc + ..onInside(ball) + ..onInside(ball), + expect: () => [ + isA() + ..having((state) => state.type, 'type', RampSensorType.inside) + ..having((state) => state.ball, 'ball', ball), + isA() + ..having((state) => state.type, 'type', RampSensorType.inside) + ..having((state) => state.ball, 'ball', ball), + ], + ); + }); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_state_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_state_test.dart new file mode 100644 index 00000000..7ac4f3dd --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_state_test.dart @@ -0,0 +1,110 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('RampSensorState', () { + test('same states are different even though they have same content', () { + final ball = Ball(baseColor: Colors.red); + + final rampSensorState = RampSensorState( + type: RampSensorType.door, + ball: ball, + ); + final otherRampSensorState = RampSensorState( + type: RampSensorType.door, + ball: ball, + ); + + expect( + rampSensorState, + isNot(equals(otherRampSensorState)), + ); + + expect( + rampSensorState.type, + equals(otherRampSensorState.type), + ); + + expect( + rampSensorState.ball, + equals(otherRampSensorState.ball), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + RampSensorState( + type: RampSensorType.door, + ball: Ball(baseColor: Colors.red), + ), + isNotNull, + ); + }); + }); + + group('copyWith', () { + test( + 'copies correctly ' + 'when no argument specified', + () { + final rampSensorState = RampSensorState( + type: RampSensorType.door, + ball: Ball(baseColor: Colors.red), + ); + + final copiedRampSensorState = rampSensorState.copyWith(); + + expect( + copiedRampSensorState.type, + equals(rampSensorState.type), + ); + + expect( + copiedRampSensorState.ball, + equals(rampSensorState.ball), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + final ball = Ball(baseColor: Colors.blue); + final rampSensorState = RampSensorState( + type: RampSensorType.door, + ball: Ball(baseColor: Colors.red), + ); + final otherRampSensorState = RampSensorState( + type: RampSensorType.inside, + ball: ball, + ); + + final copiedRampSensorState = rampSensorState.copyWith( + type: RampSensorType.inside, + ball: ball, + ); + + expect( + rampSensorState, + isNot(equals(otherRampSensorState)), + ); + + expect( + copiedRampSensorState.type, + equals(otherRampSensorState.type), + ); + + expect( + copiedRampSensorState.ball, + equals(otherRampSensorState.ball), + ); + }, + ); + }); + }); +}