From a1f1b127981bc81451ca7ee75ee000b943424558 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Fri, 29 Apr 2022 00:01:26 +0200 Subject: [PATCH] feat: spaceship ramp with contactcallback and behavior --- lib/game/components/android_acres.dart | 2 +- .../components/controlled_spaceship_ramp.dart | 165 ------------------ .../spaceship_ramp/android_ramp.dart | 110 ++++++++++++ .../spaceship_ramp/behaviors/behaviors.dart | 2 + .../ramp_multiplier_bonus_behavior.dart | 92 ++++++++++ .../behaviors/ramp_shot_behavior.dart | 25 +++ .../cubit/android_ramp_sensor_cubit.dart | 27 +++ .../cubit/android_ramp_sensor_state.dart | 37 ++++ .../controlled_spaceship_ramp_test.dart | 18 +- test/helpers/mocks.dart | 5 +- 10 files changed, 305 insertions(+), 178 deletions(-) delete mode 100644 lib/game/components/controlled_spaceship_ramp.dart create mode 100644 lib/game/components/spaceship_ramp/android_ramp.dart create mode 100644 lib/game/components/spaceship_ramp/behaviors/behaviors.dart create mode 100644 lib/game/components/spaceship_ramp/behaviors/ramp_multiplier_bonus_behavior.dart create mode 100644 lib/game/components/spaceship_ramp/behaviors/ramp_shot_behavior.dart create mode 100644 lib/game/components/spaceship_ramp/cubit/android_ramp_sensor_cubit.dart create mode 100644 lib/game/components/spaceship_ramp/cubit/android_ramp_sensor_state.dart diff --git a/lib/game/components/android_acres.dart b/lib/game/components/android_acres.dart index 752f68f9..f6108668 100644 --- a/lib/game/components/android_acres.dart +++ b/lib/game/components/android_acres.dart @@ -24,9 +24,9 @@ class AndroidAcres extends Blueprint { ScoringBehavior(points: 20), ], )..initialPosition = Vector2(-22.89, -17.35), + AndroidRamp(), ], blueprints: [ - SpaceshipRamp(), Spaceship(position: Vector2(-26.5, -28.5)), SpaceshipRail(), ], diff --git a/lib/game/components/controlled_spaceship_ramp.dart b/lib/game/components/controlled_spaceship_ramp.dart deleted file mode 100644 index 22ca3c5b..00000000 --- a/lib/game/components/controlled_spaceship_ramp.dart +++ /dev/null @@ -1,165 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'dart:collection'; -import 'dart:math' as math; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template controlled_spaceship_ramp} -/// [SpaceshipRamp] with a [SpaceshipRampController] attached. -/// {@endtemplate} -class ControlledSpaceshipRamp extends Component - with Controls, HasGameRef { - /// {@macro controlled_spaceship_ramp} - ControlledSpaceshipRamp() { - controller = SpaceshipRampController(this); - } - - late final SpaceshipRamp _spaceshipRamp; - - @override - Future onLoad() async { - await super.onLoad(); - gameRef.addContactCallback(SpaceshipRampSensorBallContactCallback()); - - _spaceshipRamp = SpaceshipRamp(); - await addFromBlueprint(_spaceshipRamp); - await addAll([ - SpaceshipRampSensor(type: SpaceshipRampSensorType.door) - ..initialPosition = Vector2(1.7, -20), - SpaceshipRampSensor(type: SpaceshipRampSensorType.inside) - ..initialPosition = Vector2(1.7, -21.5), - ]); - } -} - -/// {@template spaceship_ramp_controller} -/// Controller attached to a [SpaceshipRamp] that handles its game related -/// logic. -/// {@endtemplate} - -class SpaceshipRampController - extends ComponentController - with HasGameRef { - /// {@macro spaceship_ramp_controller} - SpaceshipRampController(ControlledSpaceshipRamp controlledSpaceshipRamp) - : super(controlledSpaceshipRamp); - - final int _oneMillionPointsTarget = 10; - - int _hitsCounter = 0; - - /// When a [Ball] shot the [SpaceshipRamp] it achieve improvements for the - /// current game, like multipliers or score. - void shot() { - _hitsCounter++; - - component._spaceshipRamp.progress(); - - gameRef.read().add(const Scored(points: 5000)); - - // TODO(ruimiguel): increase here multiplier at GameBloc. - - if (_hitsCounter % _oneMillionPointsTarget == 0) { - // TODO(ruimiguel): One million by bonus?? - const oneMillion = 1000000; - gameRef.read().add(const Scored(points: oneMillion)); - gameRef.add( - ScoreText( - text: oneMillion.toString(), - position: Vector2(1.7, -20), - ), - ); - } - } -} - -/// Used to know when a [Ball] gets into the [SpaceshipRamp] against every ball -/// that crosses the opening. -@visibleForTesting -enum SpaceshipRampSensorType { - /// Sensor at the entrance of the opening. - door, - - /// Sensor inside the [SpaceshipRamp]. - inside, -} - -/// {@template spaceship_ramp_sensor} -/// Small sensor body used to detect when a ball has entered the -/// [SpaceshipRamp]. -/// {@endtemplate} -@visibleForTesting -class SpaceshipRampSensor extends BodyComponent with InitialPosition, Layered { - /// {@macro spaceship_ramp_sensor} - SpaceshipRampSensor({required this.type}) : super() { - layer = Layer.spaceshipEntranceRamp; - renderBody = false; - } - - /// Type for the sensor, to know if it's the one at the door or inside ramp. - final SpaceshipRampSensorType type; - - @override - Body createBody() { - final shape = PolygonShape() - ..setAsBox( - 2, - 2, - initialPosition, - -5 * math.pi / 180, - ); - - final fixtureDef = FixtureDef( - shape, - isSensor: true, - ); - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -/// {@template spaceship_ramp_sensor_ball_contact_callback} -/// Turbo charges the [Ball] on contact with [SpaceshipRampSensor]. -/// {@endtemplate} -@visibleForTesting -class SpaceshipRampSensorBallContactCallback - extends ContactCallback { - /// {@macro spaceship_ramp_sensor_ball_contact_callback} - SpaceshipRampSensorBallContactCallback(); - - final Set _balls = HashSet(); - - @override - void begin( - SpaceshipRampSensor spaceshipRampSensor, - ControlledBall ball, - __, - ) { - switch (spaceshipRampSensor.type) { - case SpaceshipRampSensorType.door: - if (!_balls.contains(ball)) { - _balls.add(ball); - } - break; - case SpaceshipRampSensorType.inside: - if (_balls.contains(ball)) { - final parent = spaceshipRampSensor.parent; - if (parent is ControlledSpaceshipRamp) { - parent.controller.shot(); - } - _balls.remove(ball); - } - break; - } - } -} diff --git a/lib/game/components/spaceship_ramp/android_ramp.dart b/lib/game/components/spaceship_ramp/android_ramp.dart new file mode 100644 index 00000000..1df2c5b0 --- /dev/null +++ b/lib/game/components/spaceship_ramp/android_ramp.dart @@ -0,0 +1,110 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/components.dart'; +import 'package:pinball/game/components/spaceship_ramp/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/android_ramp_sensor_cubit.dart'; + +/// {@template android_ramp} +/// [AndroidRamp] with a for the [SpaceshipRamp]. +/// {@endtemplate} +class AndroidRamp extends Component with HasGameRef { + /// {@macro android_ramp} + AndroidRamp() + : super( + children: [ + AndroidRampSensor(type: AndroidRampSensorType.door) + ..initialPosition = Vector2(1.7, -20), + AndroidRampSensor(type: AndroidRampSensorType.inside) + ..initialPosition = Vector2(1.7, -21.5), + AndroidRampBonusBehavior( + shotPoints: 5000, + bonusPoints: 1000000, + ), + ], + ); + + late final SpaceshipRamp spaceshipRamp = SpaceshipRamp(); + + @override + Future onLoad() async { + await super.onLoad(); + gameRef.addFromBlueprint(spaceshipRamp); + } + + /// Creates a [AndroidRamp] without any children. + /// + /// This can be used for testing [AndroidRamp]'s behaviors in isolation. + @visibleForTesting + AndroidRamp.test(); +} + +/// {@template android_ramp_sensor} +/// Small sensor body used to detect when a ball has entered the +/// [SpaceshipRamp]. +/// {@endtemplate} +@visibleForTesting +class AndroidRampSensor extends BodyComponent + with ParentIsA, InitialPosition { + /// {@macro android_ramp_sensor} + AndroidRampSensor({required this.type}) + : bloc = AndroidRampSensorCubit(), + super( + children: [ + RampShotBehavior(), + ], + renderBody: false, + ); + + /// Creates a [AndroidRampSensor] without any children. + /// + @visibleForTesting + AndroidRampSensor.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 AndroidRampSensorType type; + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final AndroidRampSensorCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = PolygonShape() + ..setAsBox( + 2, + 2, + 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/lib/game/components/spaceship_ramp/behaviors/behaviors.dart b/lib/game/components/spaceship_ramp/behaviors/behaviors.dart new file mode 100644 index 00000000..1b0a965b --- /dev/null +++ b/lib/game/components/spaceship_ramp/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'ramp_multiplier_bonus_behavior.dart'; +export 'ramp_shot_behavior.dart'; diff --git a/lib/game/components/spaceship_ramp/behaviors/ramp_multiplier_bonus_behavior.dart b/lib/game/components/spaceship_ramp/behaviors/ramp_multiplier_bonus_behavior.dart new file mode 100644 index 00000000..9509f0d9 --- /dev/null +++ b/lib/game/components/spaceship_ramp/behaviors/ramp_multiplier_bonus_behavior.dart @@ -0,0 +1,92 @@ +import 'dart:collection'; + +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template android_ramp_bonus_behavior} +/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] +/// is awarded, and the [DashNestBumper.main] releases a new [Ball]. +/// {@endtemplate} +class AndroidRampBonusBehavior extends Component + with ParentIsA, HasGameRef { + /// {@macro android_ramp_bonus_behavior} + AndroidRampBonusBehavior({ + required int shotPoints, + required int bonusPoints, + }) : _shotPoints = shotPoints, + _bonusPoints = bonusPoints; + + final int _shotPoints; + final int _bonusPoints; + + final Set _balls = HashSet(); + int _previousHits = 0; + + @override + void onMount() { + super.onMount(); + + final sensors = parent.children.whereType(); + for (final sensor in sensors) { + sensor.bloc.stream.listen((state) { + switch (state.type) { + case AndroidRampSensorType.door: + _handleOnDoor(state.ball!); + break; + case AndroidRampSensorType.inside: + _handleOnInside(state.ball!); + break; + } + }); + } + } + + void _handleOnDoor(Ball ball) { + print("_handleOnDoor $ball"); + print("$_balls"); + if (!_balls.contains(ball)) { + _balls.add(ball); + print("added $_balls"); + } + } + + void _handleOnInside(Ball ball) { + print("_handleOnInside $ball"); + print("$_balls"); + if (_balls.contains(ball)) { + _balls.remove(ball); + print("removed $_balls"); + _previousHits++; + shot(_previousHits); + } + } + + final int _oneMillionPointsTarget = 10; + + /// When a [Ball] shot the [SpaceshipRamp] it achieve improvements for the + /// current game, like multipliers or score. + void shot(int currentHits) { + parent.spaceshipRamp.progress(); + print("SHOT $currentHits"); + + print("Score $_shotPoints"); + gameRef.read().add(Scored(points: _shotPoints)); + + final multiplier = gameRef.read().state.multiplier; + gameRef.read().add(const MultiplierIncreased()); + print("Increase multiplier $multiplier"); + + if (currentHits % _oneMillionPointsTarget == 0) { + print("Score $_oneMillionPointsTarget"); + gameRef.read().add(Scored(points: _bonusPoints)); + gameRef.add( + ScoreText( + text: _bonusPoints.toString(), + position: Vector2(1.7, -20), + ), + ); + } + } +} diff --git a/lib/game/components/spaceship_ramp/behaviors/ramp_shot_behavior.dart b/lib/game/components/spaceship_ramp/behaviors/ramp_shot_behavior.dart new file mode 100644 index 00000000..6ec0a278 --- /dev/null +++ b/lib/game/components/spaceship_ramp/behaviors/ramp_shot_behavior.dart @@ -0,0 +1,25 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:collection'; + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/components/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class RampShotBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + + if (other is! Ball) return; + switch (parent.type) { + case AndroidRampSensorType.door: + parent.bloc.onDoor(other); + break; + case AndroidRampSensorType.inside: + parent.bloc.onInside(other); + break; + } + } +} diff --git a/lib/game/components/spaceship_ramp/cubit/android_ramp_sensor_cubit.dart b/lib/game/components/spaceship_ramp/cubit/android_ramp_sensor_cubit.dart new file mode 100644 index 00000000..189d14fb --- /dev/null +++ b/lib/game/components/spaceship_ramp/cubit/android_ramp_sensor_cubit.dart @@ -0,0 +1,27 @@ +import 'dart:collection'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball_components/pinball_components.dart'; + +part 'android_ramp_sensor_state.dart'; + +class AndroidRampSensorCubit extends Cubit { + AndroidRampSensorCubit() : super(AndroidRampSensorState.initial()); + + void onDoor(Ball ball) { + emit( + state.copyWith( + type: AndroidRampSensorType.door, + ball: ball, + ), + ); + } + + void onInside(Ball ball) { + emit(state.copyWith( + type: AndroidRampSensorType.inside, + ball: ball, + )); + } +} diff --git a/lib/game/components/spaceship_ramp/cubit/android_ramp_sensor_state.dart b/lib/game/components/spaceship_ramp/cubit/android_ramp_sensor_state.dart new file mode 100644 index 00000000..a30f87c1 --- /dev/null +++ b/lib/game/components/spaceship_ramp/cubit/android_ramp_sensor_state.dart @@ -0,0 +1,37 @@ +part of 'android_ramp_sensor_cubit.dart'; + +/// Used to know when a [Ball] gets into the [SpaceshipRamp] against every ball +/// that crosses the opening. +enum AndroidRampSensorType { + /// Sensor at the entrance of the opening. + door, + + /// Sensor inside the [SpaceshipRamp]. + inside, +} + +class AndroidRampSensorState extends Equatable { + AndroidRampSensorState({ + required this.type, + this.ball, + }); + + /// {@macro assets_manager_state} + AndroidRampSensorState.initial() : this(type: AndroidRampSensorType.door); + + final AndroidRampSensorType type; + final Ball? ball; + + AndroidRampSensorState copyWith({ + AndroidRampSensorType? type, + Ball? ball, + }) { + return AndroidRampSensorState( + type: type ?? this.type, + ball: ball ?? this.ball, + ); + } + + @override + List get props => [type, ball]; +} diff --git a/test/game/components/controlled_spaceship_ramp_test.dart b/test/game/components/controlled_spaceship_ramp_test.dart index d3d5f6e2..19110ba5 100644 --- a/test/game/components/controlled_spaceship_ramp_test.dart +++ b/test/game/components/controlled_spaceship_ramp_test.dart @@ -23,13 +23,13 @@ void main() { Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.spaceship.ramp.arrow.active5.keyName, ]; - final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); + final flameTester = FlameTester(() => EmptyPinballTestGame(assets: assets)); group('ControlledSpaceshipRamp', () { flameTester.test( 'loads correctly', (game) async { - final controlledSpaceshipRamp = ControlledSpaceshipRamp(); + final controlledSpaceshipRamp = AndroidRamp(); await game.ensureAdd(controlledSpaceshipRamp); expect(game.contains(controlledSpaceshipRamp), isTrue); @@ -40,7 +40,7 @@ void main() { flameTester.test( 'four SpriteComponent (two rails, main and opening)', (game) async { - final controlledSpaceshipRamp = ControlledSpaceshipRamp(); + final controlledSpaceshipRamp = AndroidRamp(); await game.ensureAdd(controlledSpaceshipRamp); expect( @@ -56,7 +56,7 @@ void main() { flameTester.test( 'a SpaceshipRampArrowSpriteComponent', (game) async { - final controlledSpaceshipRamp = ControlledSpaceshipRamp(); + final controlledSpaceshipRamp = AndroidRamp(); await game.ensureAdd(controlledSpaceshipRamp); expect( @@ -72,13 +72,13 @@ void main() { flameTester.test( 'two SpaceshipRampSensor', (game) async { - final controlledSpaceshipRamp = ControlledSpaceshipRamp(); + final controlledSpaceshipRamp = AndroidRamp(); await game.ensureAdd(controlledSpaceshipRamp); expect( controlledSpaceshipRamp .descendants() - .whereType() + .whereType() .length, equals(2), ); @@ -91,10 +91,10 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(EmptyPinballTestGame.new); - late ControlledSpaceshipRamp controlledSpaceshipRamp; + late AndroidRamp controlledSpaceshipRamp; setUp(() { - controlledSpaceshipRamp = ControlledSpaceshipRamp(); + controlledSpaceshipRamp = AndroidRamp(); }); test('can be instantiated', () { @@ -116,7 +116,7 @@ void main() { final ball = MockControlledBall(); when(() => spaceshipRampSensor.type) - .thenReturn(SpaceshipRampSensorType.door); + .thenReturn(AndroidRampSensorType.door); when(() => spaceshipRampSensor.parent) .thenReturn(controlledSpaceshipRamp); when(() => controlledSpaceshipRamp.controller).thenReturn(controller); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 685a3118..f46b670b 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -71,13 +71,12 @@ class MockPinballAudio extends Mock implements PinballAudio {} class MockSparkyComputerSensor extends Mock implements SparkyComputerSensor {} -class MockControlledSpaceshipRamp extends Mock - implements ControlledSpaceshipRamp {} +class MockControlledSpaceshipRamp extends Mock implements AndroidRamp {} class MockSpaceshipRampController extends Mock implements SpaceshipRampController {} -class MockSpaceshipRampSensor extends Mock implements SpaceshipRampSensor {} +class MockSpaceshipRampSensor extends Mock implements AndroidRampSensor {} class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}