feat: spaceship ramp with contactcallback and behavior

feat/spaceship-ramp-logic
RuiAlonso 3 years ago
parent 32c2cd8cd9
commit a1f1b12798

@ -24,9 +24,9 @@ class AndroidAcres extends Blueprint {
ScoringBehavior(points: 20), ScoringBehavior(points: 20),
], ],
)..initialPosition = Vector2(-22.89, -17.35), )..initialPosition = Vector2(-22.89, -17.35),
AndroidRamp(),
], ],
blueprints: [ blueprints: [
SpaceshipRamp(),
Spaceship(position: Vector2(-26.5, -28.5)), Spaceship(position: Vector2(-26.5, -28.5)),
SpaceshipRail(), SpaceshipRail(),
], ],

@ -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<SpaceshipRampController>, HasGameRef<PinballGame> {
/// {@macro controlled_spaceship_ramp}
ControlledSpaceshipRamp() {
controller = SpaceshipRampController(this);
}
late final SpaceshipRamp _spaceshipRamp;
@override
Future<void> 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<ControlledSpaceshipRamp>
with HasGameRef<PinballGame> {
/// {@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<GameBloc>().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<GameBloc>().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<SpaceshipRampSensor, ControlledBall> {
/// {@macro spaceship_ramp_sensor_ball_contact_callback}
SpaceshipRampSensorBallContactCallback();
final Set<Ball> _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;
}
}
}

@ -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<PinballGame> {
/// {@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<void> 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<AndroidRamp>, 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);
}
}

@ -0,0 +1,2 @@
export 'ramp_multiplier_bonus_behavior.dart';
export 'ramp_shot_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<AndroidRamp>, HasGameRef<PinballGame> {
/// {@macro android_ramp_bonus_behavior}
AndroidRampBonusBehavior({
required int shotPoints,
required int bonusPoints,
}) : _shotPoints = shotPoints,
_bonusPoints = bonusPoints;
final int _shotPoints;
final int _bonusPoints;
final Set<Ball> _balls = HashSet();
int _previousHits = 0;
@override
void onMount() {
super.onMount();
final sensors = parent.children.whereType<AndroidRampSensor>();
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<GameBloc>().add(Scored(points: _shotPoints));
final multiplier = gameRef.read<GameBloc>().state.multiplier;
gameRef.read<GameBloc>().add(const MultiplierIncreased());
print("Increase multiplier $multiplier");
if (currentHits % _oneMillionPointsTarget == 0) {
print("Score $_oneMillionPointsTarget");
gameRef.read<GameBloc>().add(Scored(points: _bonusPoints));
gameRef.add(
ScoreText(
text: _bonusPoints.toString(),
position: Vector2(1.7, -20),
),
);
}
}
}

@ -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<AndroidRampSensor> {
@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;
}
}
}

@ -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<AndroidRampSensorState> {
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,
));
}
}

@ -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<Object?> get props => [type, ball];
}

@ -23,13 +23,13 @@ void main() {
Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.spaceship.ramp.arrow.active4.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName, Assets.images.spaceship.ramp.arrow.active5.keyName,
]; ];
final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); final flameTester = FlameTester(() => EmptyPinballTestGame(assets: assets));
group('ControlledSpaceshipRamp', () { group('ControlledSpaceshipRamp', () {
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final controlledSpaceshipRamp = ControlledSpaceshipRamp(); final controlledSpaceshipRamp = AndroidRamp();
await game.ensureAdd(controlledSpaceshipRamp); await game.ensureAdd(controlledSpaceshipRamp);
expect(game.contains(controlledSpaceshipRamp), isTrue); expect(game.contains(controlledSpaceshipRamp), isTrue);
@ -40,7 +40,7 @@ void main() {
flameTester.test( flameTester.test(
'four SpriteComponent (two rails, main and opening)', 'four SpriteComponent (two rails, main and opening)',
(game) async { (game) async {
final controlledSpaceshipRamp = ControlledSpaceshipRamp(); final controlledSpaceshipRamp = AndroidRamp();
await game.ensureAdd(controlledSpaceshipRamp); await game.ensureAdd(controlledSpaceshipRamp);
expect( expect(
@ -56,7 +56,7 @@ void main() {
flameTester.test( flameTester.test(
'a SpaceshipRampArrowSpriteComponent', 'a SpaceshipRampArrowSpriteComponent',
(game) async { (game) async {
final controlledSpaceshipRamp = ControlledSpaceshipRamp(); final controlledSpaceshipRamp = AndroidRamp();
await game.ensureAdd(controlledSpaceshipRamp); await game.ensureAdd(controlledSpaceshipRamp);
expect( expect(
@ -72,13 +72,13 @@ void main() {
flameTester.test( flameTester.test(
'two SpaceshipRampSensor', 'two SpaceshipRampSensor',
(game) async { (game) async {
final controlledSpaceshipRamp = ControlledSpaceshipRamp(); final controlledSpaceshipRamp = AndroidRamp();
await game.ensureAdd(controlledSpaceshipRamp); await game.ensureAdd(controlledSpaceshipRamp);
expect( expect(
controlledSpaceshipRamp controlledSpaceshipRamp
.descendants() .descendants()
.whereType<SpaceshipRampSensor>() .whereType<AndroidRampSensor>()
.length, .length,
equals(2), equals(2),
); );
@ -91,10 +91,10 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new); final flameTester = FlameTester(EmptyPinballTestGame.new);
late ControlledSpaceshipRamp controlledSpaceshipRamp; late AndroidRamp controlledSpaceshipRamp;
setUp(() { setUp(() {
controlledSpaceshipRamp = ControlledSpaceshipRamp(); controlledSpaceshipRamp = AndroidRamp();
}); });
test('can be instantiated', () { test('can be instantiated', () {
@ -116,7 +116,7 @@ void main() {
final ball = MockControlledBall(); final ball = MockControlledBall();
when(() => spaceshipRampSensor.type) when(() => spaceshipRampSensor.type)
.thenReturn(SpaceshipRampSensorType.door); .thenReturn(AndroidRampSensorType.door);
when(() => spaceshipRampSensor.parent) when(() => spaceshipRampSensor.parent)
.thenReturn(controlledSpaceshipRamp); .thenReturn(controlledSpaceshipRamp);
when(() => controlledSpaceshipRamp.controller).thenReturn(controller); when(() => controlledSpaceshipRamp.controller).thenReturn(controller);

@ -71,13 +71,12 @@ class MockPinballAudio extends Mock implements PinballAudio {}
class MockSparkyComputerSensor extends Mock implements SparkyComputerSensor {} class MockSparkyComputerSensor extends Mock implements SparkyComputerSensor {}
class MockControlledSpaceshipRamp extends Mock class MockControlledSpaceshipRamp extends Mock implements AndroidRamp {}
implements ControlledSpaceshipRamp {}
class MockSpaceshipRampController extends Mock class MockSpaceshipRampController extends Mock
implements SpaceshipRampController {} implements SpaceshipRampController {}
class MockSpaceshipRampSensor extends Mock implements SpaceshipRampSensor {} class MockSpaceshipRampSensor extends Mock implements AndroidRampSensor {}
class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}

Loading…
Cancel
Save