feat: simplified Blueprint implementation (#212)

* feat: adjusted Blueprint implementation

* refactor: improved tests

* refactor: renamed attach

* feat: tested components setter

* refactor: removed empty line

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* refactor: removed unecessary InitialPosition

* fix: awaited test

* refactor: moved Sensor to ControlledSparkyComputer

* fix: coverage

* refactor: renamed ContactCallback

* docs: renamed template

* docs: renamed template

* refactor: renamed mock ans sensor

* refactor: simplified spaceship_ramp

* refactor: reduced redundancy code

* feat: changes test query

* test: pump on spaceship ramp golden tests

* test: fixed golden tests for inactive spaceship ramp

* refactor: golden tests

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
Co-authored-by: RuiAlonso <rui.alonso@verygood.ventures>
pull/216/head
Alejandro Santiago 3 years ago committed by GitHub
parent 0bc597d34f
commit c73d36d22b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,19 +11,16 @@ import 'package:pinball_flame/pinball_flame.dart';
/// [SparkyComputer] with a [SparkyComputerController] attached.
/// {@endtemplate}
class ControlledSparkyComputer extends SparkyComputer
with Controls<SparkyComputerController>, HasGameRef<PinballGame> {
with Controls<SparkyComputerController>, HasGameRef<Forge2DGame> {
/// {@macro controlled_sparky_computer}
ControlledSparkyComputer() {
ControlledSparkyComputer() : super() {
controller = SparkyComputerController(this);
}
@override
void build(Forge2DGame _) {
addContactCallback(SparkyTurboChargeSensorBallContactCallback());
final sparkyTurboChargeSensor = SparkyTurboChargeSensor()
..initialPosition = Vector2(-13, -49.8);
add(sparkyTurboChargeSensor);
super.build(_);
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(SparkyComputerSensorBallContactCallback());
}
}
@ -39,46 +36,17 @@ class SparkyComputerController
: super(controlledComputer);
}
/// {@template sparky_turbo_charge_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SparkyComputer] with the [SparkyTurboChargeSensorBallContactCallback].
/// {@template sparky_computer_sensor_ball_contact_callback}
/// Turbo charges the [Ball] when it enters the [SparkyComputer]
/// {@endtemplate}
@visibleForTesting
class SparkyTurboChargeSensor extends BodyComponent with InitialPosition {
/// {@macro sparky_turbo_charge_sensor}
SparkyTurboChargeSensor() {
renderBody = false;
}
@override
Body createBody() {
final shape = CircleShape()..radius = 0.1;
final fixtureDef = FixtureDef(shape)..isSensor = true;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@template sparky_turbo_charge_sensor_ball_contact_callback}
/// Turbo charges the [Ball] on contact with [SparkyTurboChargeSensor].
/// {@endtemplate}
@visibleForTesting
class SparkyTurboChargeSensorBallContactCallback
extends ContactCallback<SparkyTurboChargeSensor, ControlledBall> {
/// {@macro sparky_turbo_charge_sensor_ball_contact_callback}
SparkyTurboChargeSensorBallContactCallback();
class SparkyComputerSensorBallContactCallback
extends ContactCallback<SparkyComputerSensor, ControlledBall> {
/// {@macro sparky_computer_sensor_ball_contact_callback}
SparkyComputerSensorBallContactCallback();
@override
void begin(
SparkyTurboChargeSensor sparkyTurboChargeSensor,
ControlledBall ball,
_,
) {
void begin(_, ControlledBall ball, __) {
ball.controller.turboCharge();
}
}

@ -7,21 +7,15 @@ import 'package:pinball_flame/pinball_flame.dart';
/// A [Blueprint] which creates the [Plunger], [RocketSpriteComponent] and
/// [LaunchRamp].
/// {@endtemplate}
class Launcher extends Forge2DBlueprint {
class Launcher extends Blueprint {
/// {@macro launcher}
Launcher();
/// [Plunger] to launch the [Ball] onto the board.
late final Plunger plunger;
@override
void build(Forge2DGame gameRef) {
plunger = ControlledPlunger(compressionDistance: 14)
..initialPosition = Vector2(40.7, 38);
final _rocket = RocketSpriteComponent()..position = Vector2(43, 62);
addAll([_rocket, plunger]);
addBlueprint(LaunchRamp());
}
Launcher()
: super(
components: [
ControlledPlunger(compressionDistance: 14)
..initialPosition = Vector2(40.7, 38),
RocketSpriteComponent()..position = Vector2(43, 62),
],
blueprints: [LaunchRamp()],
);
}

@ -68,7 +68,7 @@ class PinballGame extends Forge2DGame
);
unawaited(addFromBlueprint(SpaceshipRail()));
controller.attachTo(launcher.plunger);
controller.attachTo(launcher.components.whereType<Plunger>().first);
await super.onLoad();
}

@ -1,5 +1,3 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
@ -8,14 +6,15 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template boundaries}
/// A [Blueprint] which creates the [_BottomBoundary] and [_OuterBoundary].
///{@endtemplate boundaries}
class Boundaries extends Forge2DBlueprint {
@override
void build(_) {
final bottomBoundary = _BottomBoundary();
final outerBoundary = _OuterBoundary();
addAll([outerBoundary, bottomBoundary]);
}
class Boundaries extends Blueprint {
/// {@macro boundaries}
Boundaries()
: super(
components: [
_BottomBoundary(),
_OuterBoundary(),
],
);
}
/// {@template bottom_boundary}

@ -1,5 +1,3 @@
// ignore_for_file: comment_references, avoid_renaming_method_parameters
import 'dart:async';
import 'package:flame/components.dart';
@ -11,17 +9,15 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template dinowalls}
/// A [Blueprint] which creates walls for the [ChromeDino].
/// {@endtemplate}
class DinoWalls extends Forge2DBlueprint {
class DinoWalls extends Blueprint {
/// {@macro dinowalls}
DinoWalls();
@override
void build(_) {
addAll([
DinoWalls()
: super(
components: [
_DinoTopWall(),
_DinoBottomWall(),
]);
}
],
);
}
/// {@template dino_top_wall}

@ -11,36 +11,23 @@ import 'package:pinball_flame/pinball_flame.dart';
/// A [Blueprint] which creates the [_LaunchRampBase] and
/// [_LaunchRampForegroundRailing].
/// {@endtemplate}
class LaunchRamp extends Forge2DBlueprint {
@override
void build(_) {
addAllContactCallback([
LayerSensorBallContactCallback<_LaunchRampExit>(),
]);
final launchRampBase = _LaunchRampBase();
final launchRampForegroundRailing = _LaunchRampForegroundRailing();
final launchRampExit = _LaunchRampExit(rotation: math.pi / 2)
..initialPosition = Vector2(0.6, -34);
final launchRampCloseWall = _LaunchRampCloseWall()
..initialPosition = Vector2(4, -69.5);
addAll([
launchRampBase,
launchRampForegroundRailing,
launchRampExit,
launchRampCloseWall,
]);
}
class LaunchRamp extends Blueprint {
/// {@macro launch_ramp}
LaunchRamp()
: super(
components: [
_LaunchRampBase(),
_LaunchRampForegroundRailing(),
_LaunchRampExit()..initialPosition = Vector2(0.6, -34),
_LaunchRampCloseWall()..initialPosition = Vector2(4, -69.5),
],
);
}
/// {@template launch_ramp_base}
/// Ramp the [Ball] is launched from at the beginning of each ball life.
/// {@endtemplate}
class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
class _LaunchRampBase extends BodyComponent with Layered {
/// {@macro launch_ramp_base}
_LaunchRampBase()
: super(
@ -115,16 +102,18 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
@override
Body createBody() {
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final body = world.createBody(bodyDef);
final body = world.createBody(BodyDef());
_createFixtureDefs().forEach(body.createFixture);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef
.addContactCallback(LayerSensorBallContactCallback<_LaunchRampExit>());
}
}
class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {
@ -162,7 +151,7 @@ class _LaunchRampBackgroundRailingSpriteComponent extends SpriteComponent
/// Foreground railing for the [_LaunchRampBase] to render in front of the
/// [Ball].
/// {@endtemplate}
class _LaunchRampForegroundRailing extends BodyComponent with InitialPosition {
class _LaunchRampForegroundRailing extends BodyComponent {
/// {@macro launch_ramp_foreground_railing}
_LaunchRampForegroundRailing()
: super(
@ -205,11 +194,7 @@ class _LaunchRampForegroundRailing extends BodyComponent with InitialPosition {
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
final body = world.createBody(bodyDef);
final body = world.createBody(BodyDef());
_createFixtureDefs().forEach(body.createFixture);
return body;
@ -258,10 +243,8 @@ class _LaunchRampCloseWall extends BodyComponent with InitialPosition, Layered {
/// {@endtemplate}
class _LaunchRampExit extends LayerSensor {
/// {@macro launch_ramp_exit}
_LaunchRampExit({
required double rotation,
}) : _rotation = rotation,
super(
_LaunchRampExit()
: super(
insideLayer: Layer.launcher,
outsideLayer: Layer.board,
orientation: LayerEntranceOrientation.down,
@ -272,8 +255,6 @@ class _LaunchRampExit extends LayerSensor {
renderBody = false;
}
final double _rotation;
static final Vector2 _size = Vector2(1.6, 0.1);
@override
@ -282,6 +263,6 @@ class _LaunchRampExit extends LayerSensor {
_size.x,
_size.y,
initialPosition,
_rotation,
math.pi / 2,
);
}

@ -1,5 +1,3 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
@ -9,26 +7,23 @@ import 'package:pinball_flame/pinball_flame.dart';
/// A [Blueprint] which creates the pair of [Slingshot]s on the right side of
/// the board.
/// {@endtemplate}
class Slingshots extends Forge2DBlueprint {
@override
void build(_) {
final upperSlingshot = Slingshot(
class Slingshots extends Blueprint {
/// {@macro slingshots}
Slingshots()
: super(
components: [
Slingshot(
length: 5.64,
angle: -0.017,
spritePath: Assets.images.slingshot.upper.keyName,
)..initialPosition = Vector2(22.3, -1.58);
final lowerSlingshot = Slingshot(
)..initialPosition = Vector2(22.3, -1.58),
Slingshot(
length: 3.46,
angle: -0.468,
spritePath: Assets.images.slingshot.lower.keyName,
)..initialPosition = Vector2(24.7, 6.2);
addAll([
upperSlingshot,
lowerSlingshot,
]);
}
)..initialPosition = Vector2(24.7, 6.2),
],
);
}
/// {@template slingshot}

@ -1,5 +1,3 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'dart:async';
import 'dart:math';
@ -12,24 +10,11 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship}
/// A [Blueprint] which creates the spaceship feature.
/// {@endtemplate}
class Spaceship extends Forge2DBlueprint {
class Spaceship extends Blueprint {
/// {@macro spaceship}
Spaceship({required this.position});
/// Total size of the spaceship.
static final size = Vector2(25, 19);
/// The [position] where the elements will be created
final Vector2 position;
@override
void build(_) {
addAllContactCallback([
LayerSensorBallContactCallback<_SpaceshipEntrance>(),
LayerSensorBallContactCallback<_SpaceshipHole>(),
]);
addAll([
Spaceship({required Vector2 position})
: super(
components: [
SpaceshipSaucer()..initialPosition = position,
_SpaceshipEntrance()..initialPosition = position,
AndroidHead()..initialPosition = position,
@ -42,8 +27,11 @@ class Spaceship extends Forge2DBlueprint {
outsidePriority: RenderPriority.ballOnBoard,
)..initialPosition = position - Vector2(-7.2, -0.8),
SpaceshipWall()..initialPosition = position,
]);
}
],
);
/// Total size of the spaceship.
static final size = Vector2(25, 19);
}
/// {@template spaceship_saucer}
@ -51,26 +39,28 @@ class Spaceship extends Forge2DBlueprint {
/// {@endtemplate}
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_saucer}
SpaceshipSaucer() : super(priority: RenderPriority.spaceshipSaucer) {
SpaceshipSaucer()
: super(
priority: RenderPriority.spaceshipSaucer,
children: [
_SpaceshipSaucerSpriteComponent(),
],
) {
layer = Layer.spaceship;
renderBody = false;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.spaceship.saucer.keyName,
);
await add(
SpriteComponent(
sprite: sprite,
size: Spaceship.size,
anchor: Anchor.center,
),
gameRef
..addContactCallback(
LayerSensorBallContactCallback<_SpaceshipEntrance>(),
)
..addContactCallback(
LayerSensorBallContactCallback<_SpaceshipHole>(),
);
renderBody = false;
}
@override
@ -89,6 +79,25 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
}
}
class _SpaceshipSaucerSpriteComponent extends SpriteComponent with HasGameRef {
_SpaceshipSaucerSpriteComponent()
: super(
anchor: Anchor.center,
// TODO(alestiago): Refactor to use sprite orignial size instead.
size: Spaceship.size,
);
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Use cached sprite.
sprite = await gameRef.loadSprite(
Assets.images.spaceship.saucer.keyName,
);
}
}
/// {@template spaceship_bridge}
/// A [BodyComponent] that provides both the collision and the rotation
/// animation for the bridge.

@ -1,5 +1,3 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'dart:math' as math;
import 'package:flame/components.dart';
@ -11,38 +9,30 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_rail}
/// A [Blueprint] for the spaceship drop tube.
/// {@endtemplate}
class SpaceshipRail extends Forge2DBlueprint {
class SpaceshipRail extends Blueprint {
/// {@macro spaceship_rail}
SpaceshipRail();
@override
void build(_) {
addAllContactCallback([
LayerSensorBallContactCallback<_SpaceshipRailExit>(),
]);
final railRamp = _SpaceshipRailRamp();
final railEnd = _SpaceshipRailExit();
final topBase = _SpaceshipRailBase(radius: 0.55)
..initialPosition = Vector2(-26.15, -18.65);
final bottomBase = _SpaceshipRailBase(radius: 0.8)
..initialPosition = Vector2(-25.5, 12.9);
final railForeground = _SpaceshipRailForeground();
addAll([
railRamp,
railEnd,
topBase,
bottomBase,
railForeground,
]);
}
SpaceshipRail()
: super(
components: [
_SpaceshipRailRamp(),
_SpaceshipRailExit(),
_SpaceshipRailBase(radius: 0.55)
..initialPosition = Vector2(-26.15, -18.65),
_SpaceshipRailBase(radius: 0.8)
..initialPosition = Vector2(-25.5, 12.9),
_SpaceshipRailForeground()
],
);
}
/// Represents the spaceship drop rail from the [Spaceship].
class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered {
_SpaceshipRailRamp() : super(priority: RenderPriority.spaceshipRail) {
class _SpaceshipRailRamp extends BodyComponent with Layered {
_SpaceshipRailRamp()
: super(
priority: RenderPriority.spaceshipRail,
children: [_SpaceshipRailRampSpriteComponent()],
) {
layer = Layer.spaceshipExitRail;
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() {
@ -120,23 +110,17 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered {
@override
Body createBody() {
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final body = world.createBody(bodyDef);
final body = world.createBody(BodyDef());
_createFixtureDefs().forEach(body.createFixture);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_SpaceshipRailRampSpriteComponent());
gameRef.addContactCallback(
LayerSensorBallContactCallback<_SpaceshipRailExit>(),
);
}
}
@ -188,7 +172,6 @@ class _SpaceshipRailBase extends BodyComponent with InitialPosition {
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);

@ -1,5 +1,3 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'dart:math' as math;
import 'package:flame/components.dart';
@ -12,62 +10,41 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_ramp}
/// A [Blueprint] which creates the ramp leading into the [Spaceship].
/// {@endtemplate}
class SpaceshipRamp extends Forge2DBlueprint {
class SpaceshipRamp extends Blueprint {
/// {@macro spaceship_ramp}
SpaceshipRamp();
/// [SpriteGroupComponent] representing the arrow that lights up.
@visibleForTesting
late final SpaceshipRampArrowSpriteComponent spaceshipRampArrow;
/// Forwards the sprite to the next [SpaceshipRampArrowSpriteState].
///
/// If the current state is the last one it cycles back to the initial state.
void progress() => spaceshipRampArrow.progress();
@override
void build(_) {
addAllContactCallback([
LayerSensorBallContactCallback<_SpaceshipRampOpening>(),
]);
final rightOpening = _SpaceshipRampOpening(
SpaceshipRamp()
: super(
components: [
_SpaceshipRampOpening(
outsidePriority: RenderPriority.ballOnBoard,
rotation: -5 * math.pi / 180,
rotation: math.pi,
)
..initialPosition = Vector2(1.7, -19.12)
..layer = Layer.opening;
final leftOpening = _SpaceshipRampOpening(
..initialPosition = Vector2(1.7, -19.8)
..layer = Layer.opening,
_SpaceshipRampOpening(
outsideLayer: Layer.spaceship,
outsidePriority: RenderPriority.ballOnSpaceship,
rotation: -5 * math.pi / 180,
rotation: math.pi,
)
..initialPosition = Vector2(-13.7, -19)
..layer = Layer.spaceshipEntranceRamp;
final spaceshipRamp = _SpaceshipRampBackground();
spaceshipRampArrow = SpaceshipRampArrowSpriteComponent();
final spaceshipRampBoardOpeningSprite =
..initialPosition = Vector2(-13.7, -18.6)
..layer = Layer.spaceshipEntranceRamp,
_SpaceshipRampBackground(),
_SpaceshipRampBoardOpeningSpriteComponent()
..position = Vector2(3.4, -39.5);
final spaceshipRampForegroundRailing = _SpaceshipRampForegroundRailing();
final baseRight = _SpaceshipRampBase()..initialPosition = Vector2(1.7, -20);
addAll([
spaceshipRampBoardOpeningSprite,
rightOpening,
leftOpening,
baseRight,
..position = Vector2(3.4, -39.5),
_SpaceshipRampForegroundRailing(),
_SpaceshipRampBase()..initialPosition = Vector2(1.7, -20),
_SpaceshipRampBackgroundRailingSpriteComponent(),
spaceshipRamp,
spaceshipRampArrow,
spaceshipRampForegroundRailing,
]);
}
_SpaceshipRampArrowSpriteComponent(),
],
);
/// Forwards the sprite to the next [SpaceshipRampArrowSpriteState].
///
/// If the current state is the last one it cycles back to the initial state.
void progress() => components
.whereType<_SpaceshipRampArrowSpriteComponent>()
.first
.progress();
}
/// Indicates the state of the arrow on the [SpaceshipRamp].
@ -133,8 +110,6 @@ class _SpaceshipRampBackground extends BodyComponent
static const width = 5.0;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final outerLeftCurveShape = BezierCurveShape(
controlPoints: [
Vector2(-30.75, -37.3),
@ -142,9 +117,6 @@ class _SpaceshipRampBackground extends BodyComponent
Vector2(-14.2, -71.25),
],
);
final outerLeftCurveFixtureDef = FixtureDef(outerLeftCurveShape);
fixturesDef.add(outerLeftCurveFixtureDef);
final outerRightCurveShape = BezierCurveShape(
controlPoints: [
outerLeftCurveShape.vertices.last,
@ -152,32 +124,35 @@ class _SpaceshipRampBackground extends BodyComponent
Vector2(6.1, -44.9),
],
);
final outerRightCurveFixtureDef = FixtureDef(outerRightCurveShape);
fixturesDef.add(outerRightCurveFixtureDef);
final boardOpeningEdgeShape = EdgeShape()
..set(
outerRightCurveShape.vertices.last,
Vector2(7.3, -41.1),
);
final boardOpeningEdgeShapeFixtureDef = FixtureDef(boardOpeningEdgeShape);
fixturesDef.add(boardOpeningEdgeShapeFixtureDef);
return fixturesDef;
return [
FixtureDef(outerRightCurveShape),
FixtureDef(outerLeftCurveShape),
FixtureDef(boardOpeningEdgeShape),
];
}
@override
Body createBody() {
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final bodyDef = BodyDef(position: initialPosition);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(
LayerSensorBallContactCallback<_SpaceshipRampOpening>(),
);
}
}
class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
@ -188,6 +163,7 @@ class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
position: Vector2(-11.7, -54.3),
priority: RenderPriority.spaceshipRampBackgroundRailing,
);
@override
Future<void> onLoad() async {
await super.onLoad();
@ -203,6 +179,12 @@ class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
with HasGameRef {
_SpaceshipRampBackgroundRampSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-10.7, -53.6),
);
@override
Future<void> onLoad() async {
await super.onLoad();
@ -213,21 +195,19 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-10.7, -53.6);
}
}
/// {@template spaceship_ramp_arrow_sprite_component}
/// An arrow inside [SpaceshipRamp].
///
/// Lights up a each dash whenever a [Ball] gets into [SpaceshipRamp].
/// Lights progressively whenever a [Ball] gets into [SpaceshipRamp].
/// {@endtemplate}
class SpaceshipRampArrowSpriteComponent
class _SpaceshipRampArrowSpriteComponent
extends SpriteGroupComponent<SpaceshipRampArrowSpriteState>
with HasGameRef {
/// {@macro spaceship_ramp_arrow_sprite_component}
SpaceshipRampArrowSpriteComponent()
_SpaceshipRampArrowSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-3.9, -56.5),
@ -255,6 +235,8 @@ class SpaceshipRampArrowSpriteComponent
class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
with HasGameRef {
_SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center);
@override
Future<void> onLoad() async {
await super.onLoad();
@ -265,7 +247,6 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
}
}
@ -281,8 +262,6 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
}
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final innerLeftCurveShape = BezierCurveShape(
controlPoints: [
Vector2(-24.5, -38),
@ -290,10 +269,6 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
Vector2(-13.8, -64.5),
],
);
final innerLeftCurveFixtureDef = FixtureDef(innerLeftCurveShape);
fixturesDef.add(innerLeftCurveFixtureDef);
final innerRightCurveShape = BezierCurveShape(
controlPoints: [
innerLeftCurveShape.vertices.last,
@ -301,28 +276,22 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
Vector2(0, -44.5),
],
);
final innerRightCurveFixtureDef = FixtureDef(innerRightCurveShape);
fixturesDef.add(innerRightCurveFixtureDef);
final boardOpeningEdgeShape = EdgeShape()
..set(
innerRightCurveShape.vertices.last,
Vector2(-0.85, -40.8),
);
final boardOpeningEdgeShapeFixtureDef = FixtureDef(boardOpeningEdgeShape);
fixturesDef.add(boardOpeningEdgeShapeFixtureDef);
return fixturesDef;
return [
FixtureDef(innerLeftCurveShape),
FixtureDef(innerRightCurveShape),
FixtureDef(boardOpeningEdgeShape),
];
}
@override
Body createBody() {
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final bodyDef = BodyDef(position: initialPosition);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
@ -371,10 +340,7 @@ class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered {
],
);
final fixtureDef = FixtureDef(baseShape);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final bodyDef = BodyDef(position: initialPosition);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}

@ -6,75 +6,64 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_computer}
/// A [Blueprint] which creates the [_ComputerBase] and
/// [_ComputerTopSpriteComponent].
/// A computer owned by Sparky.
///
/// Register a [ContactCallback] for [SparkyComputerSensor] to listen when
/// something enters the [SparkyComputer].
/// {@endtemplate}
class SparkyComputer extends Forge2DBlueprint {
@override
void build(_) {
final computerBase = _ComputerBase();
final computerTop = _ComputerTopSpriteComponent();
addAll([
computerBase,
computerTop,
]);
}
class SparkyComputer extends Blueprint {
/// {@macro sparky_computer}
SparkyComputer()
: super(
components: [
_ComputerBase(),
_ComputerTopSpriteComponent(),
SparkyComputerSensor(),
],
);
}
class _ComputerBase extends BodyComponent with InitialPosition {
_ComputerBase() : super(priority: RenderPriority.computerBase);
_ComputerBase()
: super(
priority: RenderPriority.computerBase,
children: [_ComputerBaseSpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final leftEdge = EdgeShape()
..set(
Vector2(-14.9, -46),
Vector2(-15.3, -49.6),
);
final leftEdgeFixtureDef = FixtureDef(leftEdge);
fixturesDef.add(leftEdgeFixtureDef);
final topEdge = EdgeShape()
..set(
Vector2(-15.3, -49.6),
Vector2(-10.7, -50.6),
);
final topEdgeFixtureDef = FixtureDef(topEdge);
fixturesDef.add(topEdgeFixtureDef);
final rightEdge = EdgeShape()
..set(
Vector2(-10.7, -50.6),
Vector2(-9, -47.2),
);
final rightEdgeFixtureDef = FixtureDef(rightEdge);
fixturesDef.add(rightEdgeFixtureDef);
return fixturesDef;
return [
FixtureDef(leftEdge),
FixtureDef(topEdge),
FixtureDef(rightEdge),
];
}
@override
Body createBody() {
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
final bodyDef = BodyDef(position: initialPosition);
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_ComputerBaseSpriteComponent());
}
}
class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef {
@ -115,3 +104,24 @@ class _ComputerTopSpriteComponent extends SpriteComponent with HasGameRef {
size = sprite.originalSize / 10;
}
}
/// {@template sparky_computer_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SparkyComputer].
/// {@endtemplate}
class SparkyComputerSensor extends BodyComponent with InitialPosition {
/// {@macro sparky_computer_sensor}
SparkyComputerSensor() {
renderBody = false;
}
@override
Body createBody() {
final shape = CircleShape()..radius = 0.1;
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -17,6 +17,8 @@ void main() {
'renders correctly',
setUp: (game, tester) async {
await game.addFromBlueprint(SpaceshipRail());
await game.ready();
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 8;
},

@ -1,7 +1,6 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
@ -37,6 +36,7 @@ void main() {
);
group('renders correctly', () {
const goldenFilePath = 'golden/spaceship_ramp/';
final centerForSpaceshipRamp = Vector2(-13, -55);
flameTester.testGameWidget(
@ -49,7 +49,10 @@ void main() {
await tester.pump();
expect(
spaceshipRamp.spaceshipRampArrow.current,
spaceshipRamp.components
.whereType<SpriteGroupComponent>()
.first
.current,
SpaceshipRampArrowSpriteState.inactive,
);
@ -58,7 +61,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/spaceship_ramp/inactive.png'),
matchesGoldenFile('${goldenFilePath}inactive.png'),
);
},
);
@ -74,7 +77,10 @@ void main() {
await tester.pump();
expect(
spaceshipRamp.spaceshipRampArrow.current,
spaceshipRamp.components
.whereType<SpriteGroupComponent>()
.first
.current,
SpaceshipRampArrowSpriteState.active1,
);
@ -83,7 +89,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/spaceship_ramp/active1.png'),
matchesGoldenFile('${goldenFilePath}active1.png'),
);
},
);
@ -101,7 +107,10 @@ void main() {
await tester.pump();
expect(
spaceshipRamp.spaceshipRampArrow.current,
spaceshipRamp.components
.whereType<SpriteGroupComponent>()
.first
.current,
SpaceshipRampArrowSpriteState.active2,
);
@ -110,7 +119,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/spaceship_ramp/active2.png'),
matchesGoldenFile('${goldenFilePath}active2.png'),
);
},
);
@ -129,7 +138,10 @@ void main() {
await tester.pump();
expect(
spaceshipRamp.spaceshipRampArrow.current,
spaceshipRamp.components
.whereType<SpriteGroupComponent>()
.first
.current,
SpaceshipRampArrowSpriteState.active3,
);
@ -138,7 +150,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/spaceship_ramp/active3.png'),
matchesGoldenFile('${goldenFilePath}active3.png'),
);
},
);
@ -158,7 +170,10 @@ void main() {
await tester.pump();
expect(
spaceshipRamp.spaceshipRampArrow.current,
spaceshipRamp.components
.whereType<SpriteGroupComponent>()
.first
.current,
SpaceshipRampArrowSpriteState.active4,
);
@ -167,7 +182,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/spaceship_ramp/active4.png'),
matchesGoldenFile('${goldenFilePath}active4.png'),
);
},
);
@ -188,7 +203,10 @@ void main() {
await tester.pump();
expect(
spaceshipRamp.spaceshipRampArrow.current,
spaceshipRamp.components
.whereType<SpriteGroupComponent>()
.first
.current,
SpaceshipRampArrowSpriteState.active5,
);
@ -197,7 +215,7 @@ void main() {
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/spaceship_ramp/active5.png'),
matchesGoldenFile('${goldenFilePath}active5.png'),
);
},
);

@ -16,6 +16,8 @@ void main() {
'renders correctly',
setUp: (game, tester) async {
await game.addFromBlueprint(SparkyComputer());
await game.ready();
game.camera.followVector2(Vector2(-15, -50));
},
verify: (game, tester) async {

@ -1,101 +1,43 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
const _attachedErrorMessage = "Can't add to attached Blueprints";
// TODO(erickzanardo): Keeping this inside our code base
// so we can experiment with the idea, but this is a
// potential upstream change on Flame.
/// A [Blueprint] is a virtual way of grouping [Component]s
/// that are related, but they need to be added directly on
/// the [FlameGame] level.
/// {@template blueprint}
/// A [Blueprint] is a virtual way of grouping [Component]s that are related,
/// but they need to be added directly on the [FlameGame] level.
/// {@endtemplate blueprint}
// TODO(alestiago): refactor with feat/make-blueprint-extend-component.
abstract class Blueprint<T extends FlameGame> extends Component {
final List<Component> _components = [];
final List<Blueprint> _blueprints = [];
bool _isAttached = false;
/// Called before the the [Component]s managed
/// by this blueprint is added to the [FlameGame]
void build(T gameRef);
/// Attach the [Component]s built on [build] to the [game]
/// instance
@mustCallSuper
Future<void> attach(T game) async {
build(game);
await Future.wait([
game.addAll(_components),
..._blueprints.map(game.addFromBlueprint).toList(),
]);
_isAttached = true;
class Blueprint extends Component {
/// {@macro blueprint}
Blueprint({
Iterable<Component>? components,
Iterable<Blueprint>? blueprints,
}) {
if (components != null) _components.addAll(components);
if (blueprints != null) {
for (final blueprint in blueprints) {
_components.addAll(blueprint.components);
}
/// Adds a single [Component] to this blueprint.
@override
Future<void> add(Component component) async {
assert(!_isAttached, _attachedErrorMessage);
_components.add(component);
}
/// Adds a list of [Blueprint]s to this blueprint.
void addAllBlueprints(List<Blueprint> blueprints) {
assert(!_isAttached, _attachedErrorMessage);
_blueprints.addAll(blueprints);
}
/// Adds a single [Blueprint] to this blueprint.
void addBlueprint(Blueprint blueprint) {
assert(!_isAttached, _attachedErrorMessage);
_blueprints.add(blueprint);
}
/// Returns a copy of the components built by this blueprint
List<Component> get components => List.unmodifiable(_components);
/// Returns a copy of the children blueprints
List<Blueprint> get blueprints => List.unmodifiable(_blueprints);
}
/// A [Blueprint] that provides additional
/// structures specific to flame_forge2d
abstract class Forge2DBlueprint extends Blueprint<Forge2DGame> {
final List<ContactCallback> _callbacks = [];
/// Adds a single [ContactCallback] to this blueprint
void addContactCallback(ContactCallback callback) {
assert(!_isAttached, _attachedErrorMessage);
_callbacks.add(callback);
}
/// Adds a collection of [ContactCallback]s to this blueprint
void addAllContactCallback(List<ContactCallback> callbacks) {
assert(!_isAttached, _attachedErrorMessage);
_callbacks.addAll(callbacks);
}
@override
Future<void> attach(Forge2DGame game) async {
await super.attach(game);
final List<Component> _components = [];
for (final callback in _callbacks) {
game.addContactCallback(callback);
}
Future<void> _addToParent(Component parent) async {
await parent.addAll(_components);
}
/// Returns a copy of the callbacks built by this blueprint
List<ContactCallback> get callbacks => List.unmodifiable(_callbacks);
/// Returns a copy of the components built by this blueprint.
List<Component> get components => List.unmodifiable(_components);
}
/// Adds helper methods regardin [Blueprint]s to [FlameGame]
extension FlameGameBlueprint on FlameGame {
/// Shortcut to attach a [Blueprint] instance to this game
/// equivalent to `MyBluepinrt().attach(game)`
/// Adds helper methods regarding [Blueprint]s to [FlameGame].
extension FlameGameBlueprint on Component {
/// Shortcut to add a [Blueprint]s components to its parent.
Future<void> addFromBlueprint(Blueprint blueprint) async {
await blueprint.attach(this);
await blueprint._addToParent(this);
}
}

@ -1,138 +1,74 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_forge2d/contact_callbacks.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../helpers/helpers.dart';
class TestContactCallback extends ContactCallback<dynamic, dynamic> {}
class MyBlueprint extends Blueprint {
@override
void build(_) {
add(Component());
addAll([Component(), Component()]);
}
}
class MyOtherBlueprint extends Blueprint {
@override
void build(_) {
add(Component());
}
}
class YetMyOtherBlueprint extends Blueprint {
@override
void build(_) {
add(Component());
}
}
class MyComposedBlueprint extends Blueprint {
@override
void build(_) {
addBlueprint(MyBlueprint());
addAllBlueprints([MyOtherBlueprint(), YetMyOtherBlueprint()]);
}
}
class MyForge2dBlueprint extends Forge2DBlueprint {
@override
void build(_) {
addContactCallback(MockContactCallback());
addAllContactCallback([MockContactCallback(), MockContactCallback()]);
}
}
void main() {
group('Blueprint', () {
setUpAll(() {
registerFallbackValue(MyBlueprint());
registerFallbackValue(Component());
});
test('components can be added to it', () {
final blueprint = MyBlueprint()..build(MockForge2DGame());
expect(blueprint.components.length, equals(3));
});
TestWidgetsFlutterBinding.ensureInitialized();
test('blueprints can be added to it', () {
final blueprint = MyComposedBlueprint()..build(MockForge2DGame());
expect(blueprint.blueprints.length, equals(3));
});
test('adds the components to a game on attach', () {
final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
MyBlueprint().attach(mockGame);
verify(() => mockGame.addAll(any())).called(1);
});
test('adds components from a child Blueprint the to a game on attach', () {
final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
MyComposedBlueprint().attach(mockGame);
group('Blueprint', () {
final flameTester = FlameTester(FlameGame.new);
test('correctly sets and gets components', () {
final component1 = Component();
final component2 = Component();
final blueprint = Blueprint(
components: [
component1,
component2,
],
);
verify(() => mockGame.addAll(any())).called(4);
expect(blueprint.components.length, 2);
expect(blueprint.components, contains(component1));
expect(blueprint.components, contains(component2));
});
test(
'throws assertion error when adding to an already attached blueprint',
() async {
final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
final blueprint = MyBlueprint();
await blueprint.attach(mockGame);
expect(() => blueprint.add(Component()), throwsAssertionError);
expect(() => blueprint.addAll([Component()]), throwsAssertionError);
},
flameTester.test('adds the components to parent on attach', (game) async {
final blueprint = Blueprint(
components: [
Component(),
Component(),
],
);
});
await game.addFromBlueprint(blueprint);
await game.ready();
group('Forge2DBlueprint', () {
setUpAll(() {
registerFallbackValue(TestContactCallback());
for (final component in blueprint.components) {
expect(game.children.contains(component), isTrue);
}
});
test('callbacks can be added to it', () {
final blueprint = MyForge2dBlueprint()..build(MockForge2DGame());
expect(blueprint.callbacks.length, equals(3));
});
flameTester.test('adds components from a child Blueprint', (game) async {
final childBlueprint = Blueprint(
components: [
Component(),
Component(),
],
);
final parentBlueprint = Blueprint(
components: [
Component(),
Component(),
],
blueprints: [
childBlueprint,
],
);
test('adds the callbacks to a game on attach', () async {
final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
await MyForge2dBlueprint().attach(mockGame);
await game.addFromBlueprint(parentBlueprint);
await game.ready();
verify(() => mockGame.addContactCallback(any())).called(3);
for (final component in childBlueprint.components) {
expect(game.children, contains(component));
expect(parentBlueprint.components, contains(component));
}
for (final component in parentBlueprint.components) {
expect(game.children, contains(component));
}
});
test(
'throws assertion error when adding to an already attached blueprint',
() async {
final mockGame = MockForge2DGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
final blueprint = MyForge2dBlueprint();
await blueprint.attach(mockGame);
expect(
() => blueprint.addContactCallback(MockContactCallback()),
throwsAssertionError,
);
expect(
() => blueprint.addAllContactCallback([MockContactCallback()]),
throwsAssertionError,
);
},
);
});
}

@ -8,28 +8,21 @@ import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
group('SparkyComputerController', () {
group('ControlledSparkyComputer', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
late ControlledSparkyComputer controlledSparkyComputer;
setUp(() {
controlledSparkyComputer = ControlledSparkyComputer();
});
test('can be instantiated', () {
expect(
SparkyComputerController(controlledSparkyComputer),
isA<SparkyComputerController>(),
);
flameTester.test('loads correctly', (game) async {
final sparkyComputer = ControlledSparkyComputer();
await game.ensureAdd(sparkyComputer);
expect(game.children, contains(sparkyComputer));
});
flameTester.testGameWidget(
'SparkyTurboChargeSensorBallContactCallback turbo charges the ball',
setUp: (game, tester) async {
final contackCallback = SparkyTurboChargeSensorBallContactCallback();
final sparkyTurboChargeSensor = MockSparkyTurboChargeSensor();
final contackCallback = SparkyComputerSensorBallContactCallback();
final sparkyTurboChargeSensor = MockSparkyComputerSensor();
final ball = MockControlledBall();
final controller = MockBallController();

@ -69,8 +69,7 @@ class MockDashNestBumper extends Mock implements DashNestBumper {}
class MockPinballAudio extends Mock implements PinballAudio {}
class MockSparkyTurboChargeSensor extends Mock
implements SparkyTurboChargeSensor {}
class MockSparkyComputerSensor extends Mock implements SparkyComputerSensor {}
class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}

Loading…
Cancel
Save