Merge branch 'main' into feat/launcher-ramp-assets

pull/120/head
Allison Ryan 4 years ago
commit 2259ff1e67

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

@ -1,5 +1,6 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/foundation.dart';
/// {@template component_controller} /// {@template component_controller}
/// A [ComponentController] is a [Component] in charge of handling the logic /// A [ComponentController] is a [Component] in charge of handling the logic
@ -22,6 +23,11 @@ abstract class ComponentController<T extends Component> extends Component {
); );
await super.addToParent(parent); await super.addToParent(parent);
} }
@override
Future<void> add(Component component) {
throw Exception('ComponentController cannot add other components.');
}
} }
/// Mixin that attaches a single [ComponentController] to a [Component]. /// Mixin that attaches a single [ComponentController] to a [Component].
@ -30,6 +36,7 @@ mixin Controls<T extends ComponentController> on Component {
late final T controller; late final T controller;
@override @override
@mustCallSuper
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await add(controller); await add(controller);

@ -1,11 +1,10 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -16,10 +15,47 @@ import 'package:pinball_components/pinball_components.dart';
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] /// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [BigDashNestBumper] releases a new [Ball]. /// is awarded, and the [BigDashNestBumper] releases a new [Ball].
/// {@endtemplate} /// {@endtemplate}
// TODO(alestiago): Make a [Blueprint] once nesting [Blueprint] is implemented. // TODO(alestiago): Make a [Blueprint] once [Blueprint] inherits from
class FlutterForest extends Component // [Component].
with HasGameRef<PinballGame>, BlocComponent<GameBloc, GameState> { class FlutterForest extends Component with Controls<_FlutterForestController> {
/// {@macro flutter_forest} /// {@macro flutter_forest}
FlutterForest() {
controller = _FlutterForestController(this);
}
@override
Future<void> onLoad() async {
await super.onLoad();
final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3);
final bigNest = _ControlledBigDashNestBumper(
id: 'big_nest_bumper',
)..initialPosition = Vector2(18.55, 59.35);
final smallLeftNest = _ControlledSmallDashNestBumper.a(
id: 'small_nest_bumper_a',
)..initialPosition = Vector2(8.95, 51.95);
final smallRightNest = _ControlledSmallDashNestBumper.b(
id: 'small_nest_bumper_b',
)..initialPosition = Vector2(23.3, 46.75);
await addAll([
signPost,
smallLeftNest,
smallRightNest,
bigNest,
]);
}
}
class _FlutterForestController extends ComponentController<FlutterForest>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
_FlutterForestController(FlutterForest flutterForest) : super(flutterForest);
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_ControlledDashNestBumperBallContactCallback());
}
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
@ -32,117 +68,90 @@ class FlutterForest extends Component
void onNewState(GameState state) { void onNewState(GameState state) {
super.onNewState(state); super.onNewState(state);
add( component.add(
ControlledBall.bonus( ControlledBall.bonus(theme: gameRef.theme)
theme: gameRef.theme, ..initialPosition = Vector2(17.2, 52.7),
)..initialPosition = Vector2(17.2, 52.7),
); );
} }
}
@override class _ControlledBigDashNestBumper extends BigDashNestBumper
Future<void> onLoad() async { with Controls<DashNestBumperController>, ScorePoints {
gameRef.addContactCallback(DashNestBumperBallContactCallback()); _ControlledBigDashNestBumper({required String id}) : super() {
controller = DashNestBumperController(this, id: id);
}
final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); @override
int get points => 20;
}
// TODO(alestiago): adjust positioning once sprites are added. class _ControlledSmallDashNestBumper extends SmallDashNestBumper
final smallLeftNest = SmallDashNestBumper(id: 'small_left_nest') with Controls<DashNestBumperController>, ScorePoints {
..initialPosition = Vector2(8.95, 51.95); _ControlledSmallDashNestBumper.a({required String id}) : super.a() {
final smallRightNest = SmallDashNestBumper(id: 'small_right_nest') controller = DashNestBumperController(this, id: id);
..initialPosition = Vector2(23.3, 46.75); }
final bigNest = BigDashNestBumper(id: 'big_nest')
..initialPosition = Vector2(18.55, 59.35);
await addAll([ _ControlledSmallDashNestBumper.b({required String id}) : super.b() {
signPost, controller = DashNestBumperController(this, id: id);
smallLeftNest,
smallRightNest,
bigNest,
]);
} }
@override
int get points => 10;
} }
/// {@template dash_nest_bumper} /// {@template dash_nest_bumper_controller}
/// Bumper located in the [FlutterForest]. /// Controls a [DashNestBumper].
/// {@endtemplate} /// {@endtemplate}
@visibleForTesting @visibleForTesting
abstract class DashNestBumper extends BodyComponent<PinballGame> class DashNestBumperController extends ComponentController<DashNestBumper>
with ScorePoints, InitialPosition { with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
/// {@macro dash_nest_bumper} /// {@macro dash_nest_bumper_controller}
DashNestBumper({required this.id}) { DashNestBumperController(
paint = Paint() DashNestBumper dashNestBumper, {
..color = Colors.blue.withOpacity(0.5) required this.id,
..style = PaintingStyle.fill; }) : super(dashNestBumper);
}
/// Unique identifier for the controlled [DashNestBumper].
/// Unique identifier for this [DashNestBumper].
/// ///
/// Used to identify [DashNestBumper]s in [GameState.activatedDashNests]. /// Used to identify [DashNestBumper]s in [GameState.activatedDashNests].
final String id; final String id;
}
/// Listens when a [Ball] bounces bounces against a [DashNestBumper].
@visibleForTesting
class DashNestBumperBallContactCallback
extends ContactCallback<DashNestBumper, Ball> {
@override @override
void begin(DashNestBumper dashNestBumper, Ball ball, Contact _) { bool listenWhen(GameState? previousState, GameState newState) {
dashNestBumper.gameRef.read<GameBloc>().add( final wasActive = previousState?.activatedDashNests.contains(id) ?? false;
DashNestActivated(dashNestBumper.id), final isActive = newState.activatedDashNests.contains(id);
);
}
}
/// {@macro dash_nest_bumper} return wasActive != isActive;
@visibleForTesting }
class BigDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
BigDashNestBumper({required String id}) : super(id: id);
@override @override
int get points => 20; void onNewState(GameState state) {
super.onNewState(state);
@override if (state.activatedDashNests.contains(id)) {
Body createBody() { component.activate();
final shape = EllipseShape( } else {
center: Vector2.zero(), component.deactivate();
majorRadius: 4.85, }
minorRadius: 3.95,
)..rotate(math.pi / 2);
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
}
/// {@macro dash_nest_bumper} /// Registers when a [DashNestBumper] is hit by a [Ball].
@visibleForTesting ///
class SmallDashNestBumper extends DashNestBumper { /// Triggered by [_ControlledDashNestBumperBallContactCallback].
/// {@macro dash_nest_bumper} void hit() {
SmallDashNestBumper({required String id}) : super(id: id); gameRef.read<GameBloc>().add(DashNestActivated(id));
}
@override }
int get points => 10;
/// Listens when a [Ball] bounces bounces against a [DashNestBumper].
class _ControlledDashNestBumperBallContactCallback
extends ContactCallback<Controls<DashNestBumperController>, Ball> {
@override @override
Body createBody() { void begin(
final shape = EllipseShape( Controls<DashNestBumperController> controlledDashNestBumper,
center: Vector2.zero(), Ball _,
majorRadius: 3, Contact __,
minorRadius: 2.25, ) {
)..rotate(math.pi / 2); controlledDashNestBumper.controller.hit();
final fixtureDef = FixtureDef(shape)
..friction = 0
..restitution = 4;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
} }

@ -107,7 +107,7 @@ class _JetpackRampOpening extends RampOpening {
required double rotation, required double rotation,
}) : _rotation = rotation, }) : _rotation = rotation,
super( super(
pathwayLayer: Layer.jetpack, insideLayer: Layer.jetpack,
outsideLayer: outsideLayer, outsideLayer: outsideLayer,
orientation: RampOrientation.down, orientation: RampOrientation.down,
); );

@ -1,10 +1,10 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui';
import 'package:flame/extensions.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template spaceship_exit_rail} /// {@template spaceship_exit_rail}
@ -12,10 +12,10 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@endtemplate} /// {@endtemplate}
class SpaceshipExitRail extends Forge2DBlueprint { class SpaceshipExitRail extends Forge2DBlueprint {
/// {@macro spaceship_exit_rail} /// {@macro spaceship_exit_rail}
SpaceshipExitRail({required this.position}); SpaceshipExitRail();
/// The [position] where the elements will be created /// Base priority for wall while be on jetpack ramp.
final Vector2 position; static const ballPriorityWhenOnSpaceshipExitRail = 2;
@override @override
void build(_) { void build(_) {
@ -23,127 +23,101 @@ class SpaceshipExitRail extends Forge2DBlueprint {
SpaceshipExitRailEndBallContactCallback(), SpaceshipExitRailEndBallContactCallback(),
]); ]);
final spaceshipExitRailRamp = _SpaceshipExitRailRamp() final exitRailRamp = _SpaceshipExitRailRamp();
..initialPosition = position; final exitRailEnd = SpaceshipExitRailEnd();
final exitRail = SpaceshipExitRailEnd() final topBase = _SpaceshipExitRailBase(radius: 0.55)
..initialPosition = position + _SpaceshipExitRailRamp.exitPoint; ..initialPosition = Vector2(-26.15, 18.65);
final bottomBase = _SpaceshipExitRailBase(radius: 0.8)
..initialPosition = Vector2(-25.5, -12.9);
addAll([ addAll([
spaceshipExitRailRamp, exitRailRamp,
exitRail, exitRailEnd,
topBase,
bottomBase,
]); ]);
} }
} }
class _SpaceshipExitRailRamp extends BodyComponent class _SpaceshipExitRailRamp extends BodyComponent
with InitialPosition, Layered { with InitialPosition, Layered {
_SpaceshipExitRailRamp() : super(priority: 2) { _SpaceshipExitRailRamp()
: super(
priority: SpaceshipExitRail.ballPriorityWhenOnSpaceshipExitRail - 1,
) {
renderBody = false;
layer = Layer.spaceshipExitRail; layer = Layer.spaceshipExitRail;
// TODO(ruimiguel): remove color once asset is placed.
paint = Paint()
..color = const Color.fromARGB(255, 249, 65, 3)
..style = PaintingStyle.stroke;
} }
static final exitPoint = Vector2(9.2, -48.5);
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
const entranceRotationAngle = 175 * math.pi / 180;
const curveRotationAngle = 275 * math.pi / 180;
const exitRotationAngle = 340 * math.pi / 180;
const width = 5.5;
final fixturesDefs = <FixtureDef>[]; final fixturesDefs = <FixtureDef>[];
final entranceWall = ArcShape( final topArcShape = ArcShape(
center: Vector2(width / 2, 0), center: Vector2(-35.5, 30.9),
arcRadius: width / 2, arcRadius: 2.5,
angle: math.pi, angle: math.pi,
rotation: entranceRotationAngle, rotation: 2.9,
); );
final entranceFixtureDef = FixtureDef(entranceWall); final topArcFixtureDef = FixtureDef(topArcShape);
fixturesDefs.add(entranceFixtureDef); fixturesDefs.add(topArcFixtureDef);
final topLeftControlPoints = [
Vector2(0, 0),
Vector2(10, .5),
Vector2(7, 4),
Vector2(15.5, 8.3),
];
final topLeftCurveShape = BezierCurveShape( final topLeftCurveShape = BezierCurveShape(
controlPoints: topLeftControlPoints, controlPoints: [
)..rotate(curveRotationAngle); Vector2(-37.9, 30.4),
final topLeftFixtureDef = FixtureDef(topLeftCurveShape); Vector2(-38, 23.9),
fixturesDefs.add(topLeftFixtureDef); Vector2(-30.93, 18.2),
],
final topRightControlPoints = [ );
Vector2(0, width), final topLeftCurveFixtureDef = FixtureDef(topLeftCurveShape);
Vector2(10, 6.5), fixturesDefs.add(topLeftCurveFixtureDef);
Vector2(7, 10),
Vector2(15.5, 13.2), final middleLeftCurveShape = BezierCurveShape(
]; controlPoints: [
final topRightCurveShape = BezierCurveShape( Vector2(-30.93, 18.2),
controlPoints: topRightControlPoints, Vector2(-22.6, 10.3),
)..rotate(curveRotationAngle); Vector2(-30, 0.2),
final topRightFixtureDef = FixtureDef(topRightCurveShape); ],
fixturesDefs.add(topRightFixtureDef); );
final middleLeftCurveFixtureDef = FixtureDef(middleLeftCurveShape);
final mediumLeftControlPoints = [ fixturesDefs.add(middleLeftCurveFixtureDef);
topLeftControlPoints.last,
Vector2(21, 12.9),
Vector2(30, 7.1),
Vector2(32, 4.8),
];
final mediumLeftCurveShape = BezierCurveShape(
controlPoints: mediumLeftControlPoints,
)..rotate(curveRotationAngle);
final mediumLeftFixtureDef = FixtureDef(mediumLeftCurveShape);
fixturesDefs.add(mediumLeftFixtureDef);
final mediumRightControlPoints = [
topRightControlPoints.last,
Vector2(21, 17.2),
Vector2(30, 12.1),
Vector2(32, 10.2),
];
final mediumRightCurveShape = BezierCurveShape(
controlPoints: mediumRightControlPoints,
)..rotate(curveRotationAngle);
final mediumRightFixtureDef = FixtureDef(mediumRightCurveShape);
fixturesDefs.add(mediumRightFixtureDef);
final bottomLeftControlPoints = [
mediumLeftControlPoints.last,
Vector2(40, -1),
Vector2(48, 1.9),
Vector2(50.5, 2.5),
];
final bottomLeftCurveShape = BezierCurveShape( final bottomLeftCurveShape = BezierCurveShape(
controlPoints: bottomLeftControlPoints, controlPoints: [
)..rotate(curveRotationAngle); Vector2(-30, 0.2),
final bottomLeftFixtureDef = FixtureDef(bottomLeftCurveShape); Vector2(-36, -8.6),
fixturesDefs.add(bottomLeftFixtureDef); Vector2(-32.04, -18.3),
],
final bottomRightControlPoints = [ );
mediumRightControlPoints.last, final bottomLeftCurveFixtureDef = FixtureDef(bottomLeftCurveShape);
Vector2(40, 4), fixturesDefs.add(bottomLeftCurveFixtureDef);
Vector2(46, 6.5),
Vector2(48.8, 7.6), final topRightStraightShape = EdgeShape()
]; ..set(
Vector2(-33, 31.3),
Vector2(-27.2, 21.3),
);
final topRightStraightFixtureDef = FixtureDef(topRightStraightShape);
fixturesDefs.add(topRightStraightFixtureDef);
final middleRightCurveShape = BezierCurveShape(
controlPoints: [
Vector2(-27.2, 21.3),
Vector2(-16.5, 11.4),
Vector2(-25.29, -1.7),
],
);
final middleRightCurveFixtureDef = FixtureDef(middleRightCurveShape);
fixturesDefs.add(middleRightCurveFixtureDef);
final bottomRightCurveShape = BezierCurveShape( final bottomRightCurveShape = BezierCurveShape(
controlPoints: bottomRightControlPoints, controlPoints: [
)..rotate(curveRotationAngle); Vector2(-25.29, -1.7),
final bottomRightFixtureDef = FixtureDef(bottomRightCurveShape); Vector2(-29.91, -8.5),
fixturesDefs.add(bottomRightFixtureDef); Vector2(-26.8, -15.7),
],
final exitWall = ArcShape(
center: exitPoint,
arcRadius: width / 2,
angle: math.pi,
rotation: exitRotationAngle,
); );
final exitFixtureDef = FixtureDef(exitWall); final bottomRightCurveFixtureDef = FixtureDef(bottomRightCurveShape);
fixturesDefs.add(exitFixtureDef); fixturesDefs.add(bottomRightCurveFixtureDef);
return fixturesDefs; return fixturesDefs;
} }
@ -159,6 +133,52 @@ class _SpaceshipExitRailRamp extends BodyComponent
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
}
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.components.spaceshipDropTube.path,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: Vector2(17.5, 55.7),
anchor: Anchor.center,
position: Vector2(-29.4, -5.7),
);
await add(spriteComponent);
}
}
class _SpaceshipExitRailBase extends BodyComponent
with InitialPosition, Layered {
_SpaceshipExitRailBase({required this.radius})
: super(
priority: SpaceshipExitRail.ballPriorityWhenOnSpaceshipExitRail + 1,
) {
renderBody = false;
layer = Layer.board;
}
final double radius;
@override
Body createBody() {
final shape = CircleShape()..radius = radius;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
} }
/// {@template spaceship_exit_rail_end} /// {@template spaceship_exit_rail_end}
@ -169,15 +189,21 @@ class SpaceshipExitRailEnd extends RampOpening {
/// {@macro spaceship_exit_rail_end} /// {@macro spaceship_exit_rail_end}
SpaceshipExitRailEnd() SpaceshipExitRailEnd()
: super( : super(
pathwayLayer: Layer.spaceshipExitRail, insideLayer: Layer.spaceshipExitRail,
orientation: RampOrientation.down, orientation: RampOrientation.down,
) { ) {
renderBody = false;
layer = Layer.spaceshipExitRail; layer = Layer.spaceshipExitRail;
} }
@override @override
Shape get shape { Shape get shape {
return CircleShape()..radius = 1; return ArcShape(
center: Vector2(-29, -17.8),
arcRadius: 2.5,
angle: math.pi * 0.8,
rotation: -0.16,
);
} }
} }
@ -191,8 +217,7 @@ class SpaceshipExitRailEndBallContactCallback
@override @override
void begin(SpaceshipExitRailEnd exitRail, Ball ball, _) { void begin(SpaceshipExitRailEnd exitRail, Ball ball, _) {
ball ball
..priority = 1 ..sendTo(exitRail.outsidePriority)
..gameRef.reorderChildren()
..layer = exitRail.outsideLayer; ..layer = exitRail.outsideLayer;
} }
} }

@ -2,8 +2,8 @@
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/components/components.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template wall} /// {@template wall}
/// A continuous generic and [BodyType.static] barrier that divides a game area. /// A continuous generic and [BodyType.static] barrier that divides a game area.

@ -19,9 +19,16 @@ extension PinballGameAssetsX on PinballGame {
images.load( images.load(
components.Assets.images.launchRamp.foregroundRailing.keyName, components.Assets.images.launchRamp.foregroundRailing.keyName,
), ),
images.load(components.Assets.images.dino.dinoLandTop.keyName),
images.load(components.Assets.images.dino.dinoLandBottom.keyName),
images.load(components.Assets.images.dashBumper.a.active.keyName),
images.load(components.Assets.images.dashBumper.a.inactive.keyName),
images.load(components.Assets.images.dashBumper.b.active.keyName),
images.load(components.Assets.images.dashBumper.b.inactive.keyName),
images.load(components.Assets.images.dashBumper.main.active.keyName),
images.load(components.Assets.images.dashBumper.main.inactive.keyName),
images.load(Assets.images.components.background.path), images.load(Assets.images.components.background.path),
images.load(Assets.images.components.launchRamp.launchRamp.path), images.load(Assets.images.components.spaceshipDropTube.path),
images.load(Assets.images.components.launchRamp.launchRailFG.path),
]); ]);
} }
} }

@ -45,9 +45,7 @@ class PinballGame extends Forge2DGame
); );
unawaited( unawaited(
addFromBlueprint( addFromBlueprint(
SpaceshipExitRail( SpaceshipExitRail(),
position: Vector2(-34.3, 23.8),
),
), ),
); );
@ -66,6 +64,13 @@ class PinballGame extends Forge2DGame
Future<void> _addGameBoundaries() async { Future<void> _addGameBoundaries() async {
await add(BottomWall()); await add(BottomWall());
createBoundaries(this).forEach(add); createBoundaries(this).forEach(add);
unawaited(
addFromBlueprint(
DinoWalls(
position: Vector2(-2.4, 0),
),
),
);
} }
Future<void> _addPlunger() async { Future<void> _addPlunger() async {

@ -3,6 +3,8 @@
/// FlutterGen /// FlutterGen
/// ***************************************************** /// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
@ -15,23 +17,13 @@ class $AssetsImagesGen {
class $AssetsImagesComponentsGen { class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
/// File path: assets/images/components/background.png
AssetGenImage get background => AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png'); const AssetGenImage('assets/images/components/background.png');
$AssetsImagesComponentsLaunchRampGen get launchRamp => /// File path: assets/images/components/spaceship-drop-tube.png
const $AssetsImagesComponentsLaunchRampGen(); AssetGenImage get spaceshipDropTube =>
} const AssetGenImage('assets/images/components/spaceship-drop-tube.png');
class $AssetsImagesComponentsLaunchRampGen {
const $AssetsImagesComponentsLaunchRampGen();
/// File path: assets/images/components/launch_ramp/launch-ramp.png
AssetGenImage get launchRamp => const AssetGenImage(
'assets/images/components/launch_ramp/launch-ramp.png');
/// File path: assets/images/components/launch_ramp/launch-rail-FG.png
AssetGenImage get launchRailFG => const AssetGenImage(
'assets/images/components/launch_ramp/launch-rail-FG.png');
} }
class Assets { class Assets {

@ -82,7 +82,7 @@ List<Vector2> calculateEllipse({
/// For more information read: https://en.wikipedia.org/wiki/B%C3%A9zier_curve /// For more information read: https://en.wikipedia.org/wiki/B%C3%A9zier_curve
List<Vector2> calculateBezierCurve({ List<Vector2> calculateBezierCurve({
required List<Vector2> controlPoints, required List<Vector2> controlPoints,
double step = 0.001, double step = 0.01,
}) { }) {
assert( assert(
0 <= step && step <= 1, 0 <= step && step <= 1,

@ -98,14 +98,14 @@ void main() {
); );
}); });
test('returns by default 1000 points as indicated by step', () { test('returns by default 100 points as indicated by step', () {
final points = calculateBezierCurve( final points = calculateBezierCurve(
controlPoints: [ controlPoints: [
Vector2(0, 0), Vector2(0, 0),
Vector2(10, 10), Vector2(10, 10),
], ],
); );
expect(points.length, 1000); expect(points.length, 100);
}); });
test('returns as many points as indicated by step', () { test('returns as many points as indicated by step', () {
@ -114,9 +114,9 @@ void main() {
Vector2(0, 0), Vector2(0, 0),
Vector2(10, 10), Vector2(10, 10),
], ],
step: 0.01, step: 0.02,
); );
expect(points.length, 100); expect(points.length, 50);
}); });
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -14,7 +14,9 @@ class $AssetsImagesGen {
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
$AssetsImagesDashBumperGen get dashBumper =>
const $AssetsImagesDashBumperGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
/// File path: assets/images/flutter_sign_post.png /// File path: assets/images/flutter_sign_post.png
@ -45,6 +47,27 @@ class $AssetsImagesBaseboardGen {
const AssetGenImage('assets/images/baseboard/right.png'); const AssetGenImage('assets/images/baseboard/right.png');
} }
class $AssetsImagesDashBumperGen {
const $AssetsImagesDashBumperGen();
$AssetsImagesDashBumperAGen get a => const $AssetsImagesDashBumperAGen();
$AssetsImagesDashBumperBGen get b => const $AssetsImagesDashBumperBGen();
$AssetsImagesDashBumperMainGen get main =>
const $AssetsImagesDashBumperMainGen();
}
class $AssetsImagesDinoGen {
const $AssetsImagesDinoGen();
/// File path: assets/images/dino/dino-land-bottom.png
AssetGenImage get dinoLandBottom =>
const AssetGenImage('assets/images/dino/dino-land-bottom.png');
/// File path: assets/images/dino/dino-land-top.png
AssetGenImage get dinoLandTop =>
const AssetGenImage('assets/images/dino/dino-land-top.png');
}
class $AssetsImagesFlipperGen { class $AssetsImagesFlipperGen {
const $AssetsImagesFlipperGen(); const $AssetsImagesFlipperGen();
@ -69,6 +92,42 @@ class $AssetsImagesLaunchRampGen {
const AssetGenImage('assets/images/launch_ramp/ramp.png'); const AssetGenImage('assets/images/launch_ramp/ramp.png');
} }
class $AssetsImagesDashBumperAGen {
const $AssetsImagesDashBumperAGen();
/// File path: assets/images/dash_bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash_bumper/a/active.png');
/// File path: assets/images/dash_bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash_bumper/a/inactive.png');
}
class $AssetsImagesDashBumperBGen {
const $AssetsImagesDashBumperBGen();
/// File path: assets/images/dash_bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash_bumper/b/active.png');
/// File path: assets/images/dash_bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash_bumper/b/inactive.png');
}
class $AssetsImagesDashBumperMainGen {
const $AssetsImagesDashBumperMainGen();
/// File path: assets/images/dash_bumper/main/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash_bumper/main/active.png');
/// File path: assets/images/dash_bumper/main/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash_bumper/main/inactive.png');
}
class Assets { class Assets {
Assets._(); Assets._();

@ -83,8 +83,9 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
final direction = body.linearVelocity.normalized(); final direction = body.linearVelocity.normalized();
final effect = FireEffect( final effect = FireEffect(
burstPower: _boostTimer, burstPower: _boostTimer,
direction: direction, direction: -direction,
position: body.position, position: Vector2(body.position.x, -body.position.y),
priority: priority - 1,
); );
unawaited(gameRef.add(effect)); unawaited(gameRef.add(effect));

@ -2,6 +2,8 @@ export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board_dimensions.dart'; export 'board_dimensions.dart';
export 'board_side.dart'; export 'board_side.dart';
export 'dash_nest_bumper.dart';
export 'dino_walls.dart';
export 'fire_effect.dart'; export 'fire_effect.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'flutter_sign_post.dart'; export 'flutter_sign_post.dart';

@ -0,0 +1,142 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template dash_nest_bumper}
/// Bumper with a nest appearance.
/// {@endtemplate}
abstract class DashNestBumper extends BodyComponent with InitialPosition {
/// {@macro dash_nest_bumper}
DashNestBumper._({
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent;
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
Future<void> _loadSprites() async {
// TODO(alestiago): I think ideally we would like to do:
// Sprite(path).load so we don't require to store the activeAssetPath and
// the inactive assetPath.
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
}
/// Activates the [DashNestBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
}
/// Deactivates the [DashNestBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
// TODO(erickzanardo): Look into using onNewState instead.
// Currently doing: onNewState(gameRef.read<GameState>()) will throw an
// `Exception: build context is not available yet`
deactivate();
await add(_spriteComponent);
}
}
/// {@macro dash_nest_bumper}
class BigDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
BigDashNestBumper()
: super._(
activeAssetPath: Assets.images.dashBumper.main.active.keyName,
inactiveAssetPath: Assets.images.dashBumper.main.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
),
);
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 4.85,
minorRadius: 3.95,
)..rotate(math.pi / 2);
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@macro dash_nest_bumper}
class SmallDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
SmallDashNestBumper._({
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : super._(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
spriteComponent: spriteComponent,
);
/// {@macro dash_nest_bumper}
SmallDashNestBumper.a()
: this._(
activeAssetPath: Assets.images.dashBumper.a.active.keyName,
inactiveAssetPath: Assets.images.dashBumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
);
/// {@macro dash_nest_bumper}
SmallDashNestBumper.b()
: this._(
activeAssetPath: Assets.images.dashBumper.b.active.keyName,
inactiveAssetPath: Assets.images.dashBumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
);
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 3,
minorRadius: 2.25,
)..rotate(math.pi / 2);
final fixtureDef = FixtureDef(shape)
..friction = 0
..restitution = 4;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -0,0 +1,225 @@
// ignore_for_file: comment_references, avoid_renaming_method_parameters
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template dinowalls}
/// A [Blueprint] which creates walls for the [ChromeDino].
/// {@endtemplate}
class DinoWalls extends Forge2DBlueprint {
/// {@macro dinowalls}
DinoWalls({required this.position});
/// The [position] where the elements will be created
final Vector2 position;
@override
void build(_) {
addAll([
_DinoTopWall()..initialPosition = position,
_DinoBottomWall()..initialPosition = position,
]);
}
}
/// {@template dino_top_wall}
/// Wall segment located above [ChromeDino].
/// {@endtemplate}
class _DinoTopWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall}
_DinoTopWall() : super(priority: 2);
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final topStraightShape = EdgeShape()
..set(
Vector2(29.5, 35.1),
Vector2(28.4, 35.1),
);
final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef);
final topCurveShape = BezierCurveShape(
controlPoints: [
topStraightShape.vertex1,
Vector2(17.4, 26.38),
Vector2(25.5, 20.7),
],
);
fixturesDef.add(FixtureDef(topCurveShape));
final middleCurveShape = BezierCurveShape(
controlPoints: [
topCurveShape.vertices.last,
Vector2(27.8, 20.1),
Vector2(26.8, 19.5),
],
);
fixturesDef.add(FixtureDef(middleCurveShape));
final bottomCurveShape = BezierCurveShape(
controlPoints: [
middleCurveShape.vertices.last,
Vector2(21.15, 16),
Vector2(25.6, 15.2),
],
);
fixturesDef.add(FixtureDef(bottomCurveShape));
final bottomStraightShape = EdgeShape()
..set(
bottomCurveShape.vertices.last,
Vector2(31, 14.5),
);
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
fixturesDef.add(bottomStraightFixtureDef);
return fixturesDef;
}
@override
Body createBody() {
renderBody = false;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(
(fixture) => body.createFixture(
fixture
..restitution = 0.1
..friction = 0,
),
);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
}
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.dino.dinoLandTop.keyName,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: Vector2(10.6, 27.7),
anchor: Anchor.center,
position: Vector2(27, -28.2),
);
await add(spriteComponent);
}
}
/// {@template dino_bottom_wall}
/// Wall segment located below [ChromeDino].
/// {@endtemplate}
class _DinoBottomWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall}
_DinoBottomWall() : super(priority: 2);
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final topStraightControlPoints = [
Vector2(32.4, 8.3),
Vector2(25, 7.7),
];
final topStraightShape = EdgeShape()
..set(
topStraightControlPoints.first,
topStraightControlPoints.last,
);
final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef);
final topLeftCurveControlPoints = [
topStraightControlPoints.last,
Vector2(21.8, 7),
Vector2(29.5, -13.8),
];
final topLeftCurveShape = BezierCurveShape(
controlPoints: topLeftCurveControlPoints,
);
fixturesDef.add(FixtureDef(topLeftCurveShape));
final bottomLeftStraightControlPoints = [
topLeftCurveControlPoints.last,
Vector2(31.8, -44.1),
];
final bottomLeftStraightShape = EdgeShape()
..set(
bottomLeftStraightControlPoints.first,
bottomLeftStraightControlPoints.last,
);
final bottomLeftStraightFixtureDef = FixtureDef(bottomLeftStraightShape);
fixturesDef.add(bottomLeftStraightFixtureDef);
final bottomStraightControlPoints = [
bottomLeftStraightControlPoints.last,
Vector2(37.8, -44.1),
];
final bottomStraightShape = EdgeShape()
..set(
bottomStraightControlPoints.first,
bottomStraightControlPoints.last,
);
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
fixturesDef.add(bottomStraightFixtureDef);
return fixturesDef;
}
@override
Body createBody() {
renderBody = false;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(
(fixture) => body.createFixture(
fixture
..restitution = 0.1
..friction = 0,
),
);
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
}
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(
Assets.images.dino.dinoLandBottom.keyName,
);
final spriteComponent = SpriteComponent(
sprite: sprite,
size: Vector2(15.6, 54.8),
anchor: Anchor.center,
)..position = Vector2(31.7, 18);
await add(spriteComponent);
}
}

@ -1,5 +1,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame/particles.dart'; import 'package:flame/particles.dart';
import 'package:flame_forge2d/flame_forge2d.dart' hide Particle; import 'package:flame_forge2d/flame_forge2d.dart' hide Particle;
@ -19,33 +20,24 @@ const _particleRadius = 0.25;
/// A [BodyComponent] which creates a fire trail effect using the given /// A [BodyComponent] which creates a fire trail effect using the given
/// parameters /// parameters
/// {@endtemplate} /// {@endtemplate}
class FireEffect extends BodyComponent { class FireEffect extends ParticleSystemComponent {
/// {@macro fire_effect} /// {@macro fire_effect}
FireEffect({ FireEffect({
required this.burstPower, required this.burstPower,
required this.position,
required this.direction, required this.direction,
}); Vector2? position,
int? priority,
}) : super(
position: position,
priority: priority,
);
/// A [double] value that will define how "strong" the burst of particles /// A [double] value that will define how "strong" the burst of particles
/// will be /// will be.
final double burstPower; final double burstPower;
/// The position of the burst /// Which direction the burst will aim.
final Vector2 position;
/// Which direction the burst will aim
final Vector2 direction; final Vector2 direction;
late Particle _particle;
@override
Body createBody() {
final bodyDef = BodyDef()..position = position;
final fixtureDef = FixtureDef(CircleShape()..radius = 0)..isSensor = true;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -71,15 +63,15 @@ class FireEffect extends BodyComponent {
); );
}), }),
]; ];
final rng = math.Random(); final random = math.Random();
final spreadTween = Tween<double>(begin: -0.2, end: 0.2); final spreadTween = Tween<double>(begin: -0.2, end: 0.2);
_particle = Particle.generate( particle = Particle.generate(
count: (rng.nextDouble() * (burstPower * 10)).toInt(), count: math.max((random.nextDouble() * (burstPower * 10)).toInt(), 1),
generator: (_) { generator: (_) {
final spread = Vector2( final spread = Vector2(
spreadTween.transform(rng.nextDouble()), spreadTween.transform(random.nextDouble()),
spreadTween.transform(rng.nextDouble()), spreadTween.transform(random.nextDouble()),
); );
final finalDirection = Vector2(direction.x, -direction.y) + spread; final finalDirection = Vector2(direction.x, -direction.y) + spread;
final speed = finalDirection * (burstPower * 20); final speed = finalDirection * (burstPower * 20);
@ -88,26 +80,9 @@ class FireEffect extends BodyComponent {
lifespan: 5 / burstPower, lifespan: 5 / burstPower,
position: Vector2.zero(), position: Vector2.zero(),
speed: speed, speed: speed,
child: children[rng.nextInt(children.length)], child: children[random.nextInt(children.length)],
); );
}, },
); );
} }
@override
void update(double dt) {
super.update(dt);
_particle.update(dt);
if (_particle.shouldRemove) {
removeFromParent();
}
}
@override
void render(Canvas canvas) {
super.render(canvas);
_particle.render(canvas);
}
} }

@ -20,28 +20,41 @@ enum RampOrientation {
/// [RampOpeningBallContactCallback] detects when a [Ball] passes /// [RampOpeningBallContactCallback] detects when a [Ball] passes
/// through this opening. /// through this opening.
/// ///
/// By default the base [layer] is set to [Layer.board]. /// By default the base [layer] is set to [Layer.board] and the
/// [outsidePriority] is set to the lowest possible [Layer].
/// {@endtemplate} /// {@endtemplate}
// TODO(ruialonso): Consider renaming the class. // TODO(ruialonso): Consider renaming the class.
abstract class RampOpening extends BodyComponent with InitialPosition, Layered { abstract class RampOpening extends BodyComponent with InitialPosition, Layered {
/// {@macro ramp_opening} /// {@macro ramp_opening}
RampOpening({ RampOpening({
required Layer pathwayLayer, required Layer insideLayer,
Layer? outsideLayer, Layer? outsideLayer,
int? insidePriority,
int? outsidePriority,
required this.orientation, required this.orientation,
}) : _pathwayLayer = pathwayLayer, }) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board { _outsideLayer = outsideLayer ?? Layer.board,
layer = Layer.board; _insidePriority = insidePriority ?? 0,
_outsidePriority = outsidePriority ?? 0 {
layer = Layer.opening;
} }
final Layer _pathwayLayer; final Layer _insideLayer;
final Layer _outsideLayer; final Layer _outsideLayer;
final int _insidePriority;
final int _outsidePriority;
/// Mask of category bits for collision inside pathway. /// Mask of category bits for collision inside ramp.
Layer get pathwayLayer => _pathwayLayer; Layer get insideLayer => _insideLayer;
/// Mask of category bits for collision outside pathway. /// Mask of category bits for collision outside ramp.
Layer get outsideLayer => _outsideLayer; Layer get outsideLayer => _outsideLayer;
/// Priority for the [Ball] inside ramp.
int get insidePriority => _insidePriority;
/// Priority for the [Ball] outside ramp.
int get outsidePriority => _outsidePriority;
/// The [Shape] of the [RampOpening]. /// The [Shape] of the [RampOpening].
Shape get shape; Shape get shape;
@ -64,8 +77,7 @@ abstract class RampOpening extends BodyComponent with InitialPosition, Layered {
} }
/// {@template ramp_opening_ball_contact_callback} /// {@template ramp_opening_ball_contact_callback}
/// Detects when a [Ball] enters or exits a pathway ramp through a /// Detects when a [Ball] enters or exits a ramp through a [RampOpening].
/// [RampOpening].
/// ///
/// Modifies [Ball]'s [Layer] accordingly depending on whether the [Ball] is /// Modifies [Ball]'s [Layer] accordingly depending on whether the [Ball] is
/// outside or inside a ramp. /// outside or inside a ramp.
@ -80,9 +92,11 @@ class RampOpeningBallContactCallback<Opening extends RampOpening>
Layer layer; Layer layer;
if (!_ballsInside.contains(ball)) { if (!_ballsInside.contains(ball)) {
layer = opening.pathwayLayer; layer = opening.insideLayer;
_ballsInside.add(ball); _ballsInside.add(ball);
ball.layer = layer; ball
..sendTo(opening.insidePriority)
..layer = layer;
} else { } else {
_ballsInside.remove(ball); _ballsInside.remove(ball);
} }
@ -103,7 +117,9 @@ class RampOpeningBallContactCallback<Opening extends RampOpening>
ball.body.linearVelocity.y > 0); ball.body.linearVelocity.y > 0);
if (isBallOutsideOpening) { if (isBallOutsideOpening) {
ball.layer = opening.outsideLayer; ball
..sendTo(opening.outsidePriority)
..layer = opening.outsideLayer;
_ballsInside.remove(ball); _ballsInside.remove(ball);
} }
} }

@ -21,6 +21,9 @@ class Spaceship extends Forge2DBlueprint {
/// The [position] where the elements will be created /// The [position] where the elements will be created
final Vector2 position; final Vector2 position;
/// Base priority for wall while be on spaceship.
static const ballPriorityWhenOnSpaceship = 4;
@override @override
void build(_) { void build(_) {
addAllContactCallback([ addAllContactCallback([
@ -33,8 +36,8 @@ class Spaceship extends Forge2DBlueprint {
SpaceshipEntrance()..initialPosition = position, SpaceshipEntrance()..initialPosition = position,
AndroidHead()..initialPosition = position, AndroidHead()..initialPosition = position,
SpaceshipHole( SpaceshipHole(
onExitLayer: Layer.spaceshipExitRail, outsideLayer: Layer.spaceshipExitRail,
onExitElevation: 2, outsidePriority: 2,
)..initialPosition = position - Vector2(5.2, 4.8), )..initialPosition = position - Vector2(5.2, 4.8),
SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8), SpaceshipHole()..initialPosition = position - Vector2(-7.2, 0.8),
SpaceshipWall()..initialPosition = position, SpaceshipWall()..initialPosition = position,
@ -47,8 +50,8 @@ class Spaceship extends Forge2DBlueprint {
/// {@endtemplate} /// {@endtemplate}
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_saucer} /// {@macro spaceship_saucer}
// TODO(ruimiguel): apply Elevated when PR merged. SpaceshipSaucer()
SpaceshipSaucer() : super(priority: 3) { : super(priority: Spaceship.ballPriorityWhenOnSpaceship - 1) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@ -92,7 +95,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@endtemplate} /// {@endtemplate}
class AndroidHead extends BodyComponent with InitialPosition, Layered { class AndroidHead extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_bridge} /// {@macro spaceship_bridge}
AndroidHead() : super(priority: 4) { AndroidHead() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@ -147,16 +150,13 @@ class SpaceshipEntrance extends RampOpening {
/// {@macro spaceship_entrance} /// {@macro spaceship_entrance}
SpaceshipEntrance() SpaceshipEntrance()
: super( : super(
pathwayLayer: Layer.spaceship, insideLayer: Layer.spaceship,
orientation: RampOrientation.up, orientation: RampOrientation.up,
insidePriority: Spaceship.ballPriorityWhenOnSpaceship,
) { ) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
/// Priority order for [SpaceshipHole] on enter.
// TODO(ruimiguel): apply Elevated when PR merged.
final int onEnterElevation = 4;
@override @override
Shape get shape { Shape get shape {
renderBody = false; renderBody = false;
@ -181,24 +181,22 @@ class SpaceshipEntrance extends RampOpening {
/// {@endtemplate} /// {@endtemplate}
class SpaceshipHole extends RampOpening { class SpaceshipHole extends RampOpening {
/// {@macro spaceship_hole} /// {@macro spaceship_hole}
SpaceshipHole({Layer? onExitLayer, this.onExitElevation = 1}) SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1})
: super( : super(
pathwayLayer: Layer.spaceship, insideLayer: Layer.spaceship,
outsideLayer: onExitLayer, outsideLayer: outsideLayer,
outsidePriority: outsidePriority,
orientation: RampOrientation.up, orientation: RampOrientation.up,
) { ) {
renderBody = false;
layer = Layer.spaceship; layer = Layer.spaceship;
} }
/// Priority order for [SpaceshipHole] on exit.
// TODO(ruimiguel): apply Elevated when PR merged.
final int onExitElevation;
@override @override
Shape get shape { Shape get shape {
return ArcShape( return ArcShape(
center: Vector2(0, 4.2), center: Vector2(0, 3.2),
arcRadius: 6, arcRadius: 5,
angle: 1, angle: 1,
rotation: 60 * pi / 180, rotation: 60 * pi / 180,
); );
@ -235,8 +233,7 @@ class _SpaceshipWallShape extends ChainShape {
/// {@endtemplate} /// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered { class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_wall} /// {@macro spaceship_wall}
// TODO(ruimiguel): apply Elevated when PR merged SpaceshipWall() : super(priority: Spaceship.ballPriorityWhenOnSpaceship + 1) {
SpaceshipWall() : super(priority: 4) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@ -269,9 +266,7 @@ class SpaceshipEntranceBallContactCallback
@override @override
void begin(SpaceshipEntrance entrance, Ball ball, _) { void begin(SpaceshipEntrance entrance, Ball ball, _) {
ball ball
// TODO(ruimiguel): apply Elevated when PR merged. ..sendTo(entrance.insidePriority)
..priority = entrance.onEnterElevation
..gameRef.reorderChildren()
..layer = Layer.spaceship; ..layer = Layer.spaceship;
} }
} }
@ -279,16 +274,14 @@ class SpaceshipEntranceBallContactCallback
/// [ContactCallback] that handles the contact between the [Ball] /// [ContactCallback] that handles the contact between the [Ball]
/// and a [SpaceshipHole]. /// and a [SpaceshipHole].
/// ///
/// It sets the [Ball] priority and filter data so it will "be back" on the /// It sets the [Ball] priority and filter data so it will outside of the
/// board. /// [Spaceship].
class SpaceshipHoleBallContactCallback class SpaceshipHoleBallContactCallback
extends ContactCallback<SpaceshipHole, Ball> { extends ContactCallback<SpaceshipHole, Ball> {
@override @override
void begin(SpaceshipHole hole, Ball ball, _) { void begin(SpaceshipHole hole, Ball ball, _) {
ball ball
// TODO(ruimiguel): apply Elevated when PR merged. ..sendTo(hole.outsidePriority)
..priority = hole.onExitElevation
..gameRef.reorderChildren()
..layer = hole.outsideLayer; ..layer = hole.outsideLayer;
} }
} }

@ -1 +1,2 @@
export 'blueprint.dart'; export 'blueprint.dart';
export 'priority.dart';

@ -0,0 +1,39 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
/// Helper methods to change the [priority] of a [Component].
extension ComponentPriorityX on Component {
static const _lowestPriority = 0;
/// Changes the priority to a specific one.
void sendTo(int destinationPriority) {
if (priority != destinationPriority) {
priority = math.max(destinationPriority, _lowestPriority);
reorderChildren();
}
}
/// Changes the priority to the lowest possible.
void sendToBack() {
if (priority != _lowestPriority) {
priority = _lowestPriority;
reorderChildren();
}
}
/// Decreases the priority to be lower than another [Component].
void showBehindOf(Component other) {
if (priority >= other.priority) {
priority = math.max(other.priority - 1, _lowestPriority);
reorderChildren();
}
}
/// Increases the priority to be higher than another [Component].
void showInFrontOf(Component other) {
if (priority <= other.priority) {
priority = other.priority + 1;
reorderChildren();
}
}
}

@ -27,8 +27,12 @@ flutter:
assets: assets:
- assets/images/ - assets/images/
- assets/images/baseboard/ - assets/images/baseboard/
- assets/images/dino/
- assets/images/flipper/ - assets/images/flipper/
- assets/images/launch_ramp/ - assets/images/launch_ramp/
- assets/images/dash_bumper/a/
- assets/images/dash_bumper/b/
- assets/images/dash_bumper/main/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -34,7 +34,6 @@ class _EffectEmitter extends Component {
add( add(
FireEffect( FireEffect(
burstPower: (_timer / _timerLimit) * _force, burstPower: (_timer / _timerLimit) * _force,
position: Vector2.zero(),
direction: _direction, direction: _direction,
), ),
); );

@ -1,11 +1,8 @@
import 'dart:ui'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
class MockCanvas extends Mock implements Canvas {}
class MockFilter extends Mock implements Filter {} class MockFilter extends Mock implements Filter {}
class MockFixture extends Mock implements Fixture {} class MockFixture extends Mock implements Fixture {}
@ -24,3 +21,5 @@ class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock class MockContactCallback extends Mock
implements ContactCallback<Object, Object> {} implements ContactCallback<Object, Object> {}
class MockComponent extends Mock implements Component {}

@ -0,0 +1,116 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('BigDashNestBumper', () {
flameTester.test('loads correctly', (game) async {
final bumper = BigDashNestBumper();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
final bumper = BigDashNestBumper();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = BigDashNestBumper();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
flameTester.test('changes sprite', (game) async {
final bumper = BigDashNestBumper();
await game.ensureAdd(bumper);
final spriteComponent = bumper.firstChild<SpriteComponent>()!;
final deactivatedSprite = spriteComponent.sprite;
bumper.activate();
expect(
spriteComponent.sprite,
isNot(equals(deactivatedSprite)),
);
final activatedSprite = spriteComponent.sprite;
bumper.deactivate();
expect(
spriteComponent.sprite,
isNot(equals(activatedSprite)),
);
expect(
activatedSprite,
isNot(equals(deactivatedSprite)),
);
});
});
group('SmallDashNestBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final bumper = SmallDashNestBumper.b();
await game.ensureAdd(bumper);
expect(game.contains(bumper), isTrue);
});
flameTester.test('activate returns normally', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
expect(bumper.activate, returnsNormally);
});
flameTester.test('deactivate returns normally', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
expect(bumper.deactivate, returnsNormally);
});
flameTester.test('changes sprite', (game) async {
final bumper = SmallDashNestBumper.a();
await game.ensureAdd(bumper);
final spriteComponent = bumper.firstChild<SpriteComponent>()!;
final deactivatedSprite = spriteComponent.sprite;
bumper.activate();
expect(
spriteComponent.sprite,
isNot(equals(deactivatedSprite)),
);
final activatedSprite = spriteComponent.sprite;
bumper.deactivate();
expect(
spriteComponent.sprite,
isNot(equals(activatedSprite)),
);
expect(
activatedSprite,
isNot(equals(deactivatedSprite)),
);
});
});
}

@ -0,0 +1,28 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('DinoWalls', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
flameTester.test(
'loads correctly',
(game) async {
final dinoWalls = DinoWalls(position: Vector2.zero());
await game.addFromBlueprint(dinoWalls);
await game.ready();
for (final wall in dinoWalls.components) {
expect(game.contains(wall), isTrue);
}
},
);
});
}

@ -1,11 +1,8 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -14,42 +11,16 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final flameTester = FlameTester(TestGame.new);
setUpAll(() { flameTester.test(
registerFallbackValue(Offset.zero); 'loads correctly',
registerFallbackValue(Paint()); (game) async {
}); final fireEffect = FireEffect(
group('FireEffect', () {
flameTester.test('is removed once its particles are done', (game) async {
await game.ensureAdd(
FireEffect(
burstPower: 1,
position: Vector2.zero(),
direction: Vector2.all(2),
),
);
await game.ready();
expect(game.children.whereType<FireEffect>().length, equals(1));
game.update(5);
await game.ready();
expect(game.children.whereType<FireEffect>().length, equals(0));
});
flameTester.test('render circles on the canvas', (game) async {
final effect = FireEffect(
burstPower: 1, burstPower: 1,
position: Vector2.zero(), direction: Vector2.zero(),
direction: Vector2.all(2),
); );
await game.ensureAdd(effect); await game.ensureAdd(fireEffect);
await game.ready();
final canvas = MockCanvas();
effect.render(canvas);
verify(() => canvas.drawCircle(any(), any(), any())) expect(game.contains(fireEffect), isTrue);
.called(greaterThan(0)); },
}); );
});
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@ -12,7 +12,7 @@ class TestRampOpening extends RampOpening {
required RampOrientation orientation, required RampOrientation orientation,
required Layer pathwayLayer, required Layer pathwayLayer,
}) : super( }) : super(
pathwayLayer: pathwayLayer, insideLayer: pathwayLayer,
orientation: orientation, orientation: orientation,
); );
@ -129,14 +129,12 @@ void main() {
final callback = TestRampOpeningBallContactCallback(); final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body); when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero()); when(() => body.position).thenReturn(Vector2.zero());
when(() => ball.layer).thenReturn(Layer.board); when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact()); callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1); verify(() => ball.layer = area.insideLayer).called(1);
}); });
flameTester.test( flameTester.test(
@ -152,14 +150,12 @@ void main() {
final callback = TestRampOpeningBallContactCallback(); final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body); when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero()); when(() => body.position).thenReturn(Vector2.zero());
when(() => ball.layer).thenReturn(Layer.board); when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact()); callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1); verify(() => ball.layer = area.insideLayer).called(1);
}); });
flameTester.test( flameTester.test(
@ -174,15 +170,13 @@ void main() {
final callback = TestRampOpeningBallContactCallback(); final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body); when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero()); when(() => body.position).thenReturn(Vector2.zero());
when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
when(() => ball.layer).thenReturn(Layer.board); when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact()); callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1); verify(() => ball.layer = area.insideLayer).called(1);
callback.end(ball, area, MockContact()); callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board); verify(() => ball.layer = Layer.board);
@ -200,15 +194,13 @@ void main() {
final callback = TestRampOpeningBallContactCallback(); final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body); when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero()); when(() => body.position).thenReturn(Vector2.zero());
when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
when(() => ball.layer).thenReturn(Layer.board); when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact()); callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1); verify(() => ball.layer = area.insideLayer).called(1);
callback.end(ball, area, MockContact()); callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board); verify(() => ball.layer = Layer.board);
@ -226,21 +218,19 @@ void main() {
final callback = TestRampOpeningBallContactCallback(); final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body); when(() => ball.body).thenReturn(body);
when(() => ball.priority).thenReturn(1);
when(() => body.position).thenReturn(Vector2.zero()); when(() => body.position).thenReturn(Vector2.zero());
when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
when(() => ball.layer).thenReturn(Layer.board); when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact()); callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1); verify(() => ball.layer = area.insideLayer).called(1);
callback.end(ball, area, MockContact()); callback.end(ball, area, MockContact());
verifyNever(() => ball.layer = Layer.board); verifyNever(() => ball.layer = Layer.board);
callback.begin(ball, area, MockContact()); callback.begin(ball, area, MockContact());
verifyNever(() => ball.layer = area.pathwayLayer); verifyNever(() => ball.layer = area.insideLayer);
callback.end(ball, area, MockContact()); callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board); verify(() => ball.layer = Layer.board);

@ -59,7 +59,8 @@ void main() {
group('SpaceshipEntranceBallContactCallback', () { group('SpaceshipEntranceBallContactCallback', () {
test('changes the ball priority on contact', () { test('changes the ball priority on contact', () {
when(() => entrance.onEnterElevation).thenReturn(3); when(() => ball.priority).thenReturn(2);
when(() => entrance.insidePriority).thenReturn(3);
SpaceshipEntranceBallContactCallback().begin( SpaceshipEntranceBallContactCallback().begin(
entrance, entrance,
@ -67,39 +68,15 @@ void main() {
MockContact(), MockContact(),
); );
verify(() => ball.priority = entrance.onEnterElevation).called(1); verify(() => ball.sendTo(entrance.insidePriority)).called(1);
});
test('re order the game children', () {
when(() => entrance.onEnterElevation).thenReturn(3);
SpaceshipEntranceBallContactCallback().begin(
entrance,
ball,
MockContact(),
);
verify(game.reorderChildren).called(1);
}); });
}); });
group('SpaceshipHoleBallContactCallback', () { group('SpaceshipHoleBallContactCallback', () {
test('changes the ball priority on contact', () { test('changes the ball priority on contact', () {
when(() => ball.priority).thenReturn(2);
when(() => hole.outsideLayer).thenReturn(Layer.board); when(() => hole.outsideLayer).thenReturn(Layer.board);
when(() => hole.onExitElevation).thenReturn(1); when(() => hole.outsidePriority).thenReturn(1);
SpaceshipHoleBallContactCallback().begin(
hole,
ball,
MockContact(),
);
verify(() => ball.priority = hole.onExitElevation).called(1);
});
test('re order the game children', () {
when(() => hole.outsideLayer).thenReturn(Layer.board);
when(() => hole.onExitElevation).thenReturn(1);
SpaceshipHoleBallContactCallback().begin( SpaceshipHoleBallContactCallback().begin(
hole, hole,
@ -107,7 +84,7 @@ void main() {
MockContact(), MockContact(),
); );
verify(game.reorderChildren).called(1); verify(() => ball.sendTo(hole.outsidePriority)).called(1);
}); });
}); });
}); });

@ -0,0 +1,221 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/src/flame/priority.dart';
import '../../helpers/helpers.dart';
class TestBodyComponent extends BodyComponent {
@override
Body createBody() {
final fixtureDef = FixtureDef(CircleShape());
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}
void main() {
final flameTester = FlameTester(Forge2DGame.new);
group('ComponentPriorityX', () {
group('sendTo', () {
flameTester.test(
'changes the priority correctly to other level',
(game) async {
const newPriority = 5;
final component = TestBodyComponent()..priority = 4;
component.sendTo(newPriority);
expect(component.priority, equals(newPriority));
},
);
flameTester.test(
'calls reorderChildren if the new priority is different',
(game) async {
const newPriority = 5;
final component = MockComponent();
when(() => component.priority).thenReturn(4);
component.sendTo(newPriority);
verify(component.reorderChildren).called(1);
},
);
flameTester.test(
"doesn't call reorderChildren if the priority is the same",
(game) async {
const newPriority = 5;
final component = MockComponent();
when(() => component.priority).thenReturn(newPriority);
component.sendTo(newPriority);
verifyNever(component.reorderChildren);
},
);
});
group('sendToBack', () {
flameTester.test(
'changes the priority correctly to board level',
(game) async {
final component = TestBodyComponent()..priority = 4;
component.sendToBack();
expect(component.priority, equals(0));
},
);
flameTester.test(
'calls reorderChildren if the priority is greater than lowest level',
(game) async {
final component = MockComponent();
when(() => component.priority).thenReturn(4);
component.sendToBack();
verify(component.reorderChildren).called(1);
},
);
flameTester.test(
"doesn't call reorderChildren if the priority is the lowest level",
(game) async {
final component = MockComponent();
when(() => component.priority).thenReturn(0);
component.sendToBack();
verifyNever(component.reorderChildren);
},
);
});
group('showBehindOf', () {
flameTester.test(
'changes the priority if it is greater than other component',
(game) async {
const startPriority = 2;
final component = TestBodyComponent()..priority = startPriority;
final otherComponent = TestBodyComponent()
..priority = startPriority - 1;
component.showBehindOf(otherComponent);
expect(component.priority, equals(otherComponent.priority - 1));
},
);
flameTester.test(
"doesn't change the priority if it is lower than other component",
(game) async {
const startPriority = 2;
final component = TestBodyComponent()..priority = startPriority;
final otherComponent = TestBodyComponent()
..priority = startPriority + 1;
component.showBehindOf(otherComponent);
expect(component.priority, equals(startPriority));
},
);
flameTester.test(
'calls reorderChildren if the priority is greater than other component',
(game) async {
const startPriority = 2;
final component = MockComponent();
final otherComponent = MockComponent();
when(() => component.priority).thenReturn(startPriority);
when(() => otherComponent.priority).thenReturn(startPriority - 1);
component.showBehindOf(otherComponent);
verify(component.reorderChildren).called(1);
},
);
flameTester.test(
"doesn't call reorderChildren if the priority is lower than other "
'component',
(game) async {
const startPriority = 2;
final component = MockComponent();
final otherComponent = MockComponent();
when(() => component.priority).thenReturn(startPriority);
when(() => otherComponent.priority).thenReturn(startPriority + 1);
component.showBehindOf(otherComponent);
verifyNever(component.reorderChildren);
},
);
});
group('showInFrontOf', () {
flameTester.test(
'changes the priority if it is lower than other component',
(game) async {
const startPriority = 2;
final component = TestBodyComponent()..priority = startPriority;
final otherComponent = TestBodyComponent()
..priority = startPriority + 1;
component.showInFrontOf(otherComponent);
expect(component.priority, equals(otherComponent.priority + 1));
},
);
flameTester.test(
"doesn't change the priority if it is greater than other component",
(game) async {
const startPriority = 2;
final component = TestBodyComponent()..priority = startPriority;
final otherComponent = TestBodyComponent()
..priority = startPriority - 1;
component.showInFrontOf(otherComponent);
expect(component.priority, equals(startPriority));
},
);
flameTester.test(
'calls reorderChildren if the priority is lower than other component',
(game) async {
const startPriority = 2;
final component = MockComponent();
final otherComponent = MockComponent();
when(() => component.priority).thenReturn(startPriority);
when(() => otherComponent.priority).thenReturn(startPriority + 1);
component.showInFrontOf(otherComponent);
verify(component.reorderChildren).called(1);
},
);
flameTester.test(
"doesn't call reorderChildren if the priority is greater than other "
'component',
(game) async {
const startPriority = 2;
final component = MockComponent();
final otherComponent = MockComponent();
when(() => component.priority).thenReturn(startPriority);
when(() => otherComponent.priority).thenReturn(startPriority - 1);
component.showInFrontOf(otherComponent);
verifyNever(component.reorderChildren);
},
);
});
});
}

@ -31,6 +31,7 @@ void main() {
); );
}, },
); );
flameTester.test( flameTester.test(
'throws AssertionError when not attached to controlled component', 'throws AssertionError when not attached to controlled component',
(game) async { (game) async {
@ -44,6 +45,35 @@ void main() {
); );
}, },
); );
flameTester.test(
'throws Exception when adding a component',
(game) async {
final component = ControlledComponent();
final controller = TestComponentController(component);
await expectLater(
() async => controller.add(Component()),
throwsException,
);
},
);
flameTester.test(
'throws Exception when adding multiple components',
(game) async {
final component = ControlledComponent();
final controller = TestComponentController(component);
await expectLater(
() async => controller.addAll([
Component(),
Component(),
]),
throwsException,
);
},
);
}); });
group('Controls', () { group('Controls', () {

@ -196,10 +196,10 @@ void main() {
group('bonus letter activation', () { group('bonus letter activation', () {
late GameBloc gameBloc; late GameBloc gameBloc;
final tester = flameBlocTester<PinballGame>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
// TODO(alestiago): Use TestGame once BonusLetter has controller. // TODO(alestiago): Use TestGame once BonusLetter has controller.
game: PinballGameTest.create, gameBuilder: PinballGameTest.create,
gameBloc: () => gameBloc, blocBuilder: () => gameBloc,
); );
setUp(() { setUp(() {
@ -211,7 +211,7 @@ void main() {
); );
}); });
tester.testGameWidget( flameBlocTester.testGameWidget(
'adds BonusLetterActivated to GameBloc when not activated', 'adds BonusLetterActivated to GameBloc when not activated',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ready(); await game.ready();
@ -225,7 +225,7 @@ void main() {
}, },
); );
tester.testGameWidget( flameBlocTester.testGameWidget(
"doesn't add BonusLetterActivated to GameBloc when already activated", "doesn't add BonusLetterActivated to GameBloc when already activated",
setUp: (game, tester) async { setUp: (game, tester) async {
const state = GameState( const state = GameState(
@ -253,7 +253,7 @@ void main() {
}, },
); );
tester.testGameWidget( flameBlocTester.testGameWidget(
'adds a ColorEffect', 'adds a ColorEffect',
setUp: (game, tester) async { setUp: (game, tester) async {
const state = GameState( const state = GameState(
@ -284,7 +284,7 @@ void main() {
}, },
); );
tester.testGameWidget( flameBlocTester.testGameWidget(
'only listens when there is a change on the letter status', 'only listens when there is a change on the letter status',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ready(); await game.ready();

@ -66,12 +66,12 @@ void main() {
); );
}); });
final tester = flameBlocTester<PinballGame>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
game: PinballGameTest.create, gameBuilder: PinballGameTest.create,
gameBloc: () => gameBloc, blocBuilder: () => gameBloc,
); );
tester.testGameWidget( flameBlocTester.testGameWidget(
'lost adds BallLost to GameBloc', 'lost adds BallLost to GameBloc',
setUp: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = LaunchedBallController(ball);
@ -86,7 +86,7 @@ void main() {
); );
group('listenWhen', () { group('listenWhen', () {
tester.testGameWidget( flameBlocTester.testGameWidget(
'listens when a ball has been lost', 'listens when a ball has been lost',
setUp: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = LaunchedBallController(ball);
@ -107,7 +107,7 @@ void main() {
}, },
); );
tester.testGameWidget( flameBlocTester.testGameWidget(
'does not listen when a ball has not been lost', 'does not listen when a ball has not been lost',
setUp: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = LaunchedBallController(ball);
@ -130,7 +130,7 @@ void main() {
}); });
group('onNewState', () { group('onNewState', () {
tester.testGameWidget( flameBlocTester.testGameWidget(
'removes ball', 'removes ball',
setUp: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = LaunchedBallController(ball);
@ -147,7 +147,7 @@ void main() {
}, },
); );
tester.testGameWidget( flameBlocTester.testGameWidget(
'spawns a new ball when the ball is not the last one', 'spawns a new ball when the ball is not the last one',
setUp: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = LaunchedBallController(ball);
@ -168,7 +168,7 @@ void main() {
}, },
); );
tester.testGameWidget( flameBlocTester.testGameWidget(
'does not spawn a new ball is the last one', 'does not spawn a new ball is the last one',
setUp: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = LaunchedBallController(ball);

@ -1,7 +1,9 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
@ -9,6 +11,18 @@ import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) {
assert(
bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty,
'Bodies require fixtures to contact each other.',
);
final fixtureA = bodyA.body.fixtures.first;
final fixtureB = bodyB.body.fixtures.first;
final contact = Contact.init(fixtureA, 0, fixtureB, 0);
game.world.contactManager.contactListener?.beginContact(contact);
}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(PinballGameTest.create);
@ -30,13 +44,73 @@ void main() {
'a FlutterSignPost', 'a FlutterSignPost',
(game) async { (game) async {
await game.ready(); await game.ready();
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.descendants().whereType<FlutterSignPost>().length,
equals(1),
);
},
);
flameTester.test(
'a BigDashNestBumper',
(game) async {
await game.ready();
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect( expect(
game.descendants().whereType<FlutterSignPost>().length, flutterForest.descendants().whereType<BigDashNestBumper>().length,
equals(1), equals(1),
); );
}, },
); );
flameTester.test(
'two SmallDashNestBumper',
(game) async {
await game.ready();
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect(
flutterForest.descendants().whereType<SmallDashNestBumper>().length,
equals(2),
);
},
);
});
group('controller', () {
group('listenWhen', () {
final gameBloc = MockGameBloc();
final flameBlocTester = FlameBlocTester<TestGame, GameBloc>(
gameBuilder: TestGame.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'listens when a Bonus.dashNest is added',
verify: (game, tester) async {
final flutterForest = FlutterForest();
const state = GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
);
expect(
flutterForest.controller
.listenWhen(const GameState.initial(), state),
isTrue,
);
},
);
});
}); });
flameTester.test( flameTester.test(
@ -47,7 +121,7 @@ void main() {
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
final previousBalls = game.descendants().whereType<Ball>().length; final previousBalls = game.descendants().whereType<Ball>().length;
flutterForest.onNewState(MockGameState()); flutterForest.controller.onNewState(MockGameState());
await game.ready(); await game.ready();
expect( expect(
@ -57,14 +131,13 @@ void main() {
}, },
); );
group('listenWhen', () { group('bumpers', () {
final gameBloc = MockGameBloc(); late Ball ball;
final tester = flameBlocTester( late GameBloc gameBloc;
game: TestGame.new,
gameBloc: () => gameBloc,
);
setUp(() { setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
gameBloc = MockGameBloc();
whenListen( whenListen(
gameBloc, gameBloc,
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
@ -72,73 +145,167 @@ void main() {
); );
}); });
tester.testGameWidget( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
'listens when a Bonus.dashNest is added', gameBuilder: PinballGameTest.create,
verify: (game, tester) async { blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'add DashNestActivated event',
setUp: (game, tester) async {
await game.ready();
final flutterForest =
game.descendants().whereType<FlutterForest>().first;
await game.ensureAdd(ball);
final bumpers =
flutterForest.descendants().whereType<DashNestBumper>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
final controller = bumper.firstChild<DashNestBumperController>()!;
verify(
() => gameBloc.add(DashNestActivated(controller.id)),
).called(1);
}
},
);
flameBlocTester.testGameWidget(
'add Scored event',
setUp: (game, tester) async {
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
await game.ensureAdd(ball);
const state = GameState( final bumpers =
score: 0, flutterForest.descendants().whereType<DashNestBumper>();
balls: 3,
activatedBonusLetters: [], for (final bumper in bumpers) {
activatedDashNests: {}, beginContact(game, bumper, ball);
bonusHistory: [GameBonus.dashNest], final points = (bumper as ScorePoints).points;
); verify(
expect( () => gameBloc.add(Scored(points: points)),
flutterForest.listenWhen(const GameState.initial(), state), ).called(1);
isTrue, }
);
}, },
); );
}); });
}); });
group('DashNestBumperBallContactCallback', () { group('DashNestBumperController', () {
final gameBloc = MockGameBloc(); late DashNestBumper dashNestBumper;
final tester = flameBlocTester(
// TODO(alestiago): Use TestGame.new once a controller is implemented.
game: PinballGameTest.create,
gameBloc: () => gameBloc,
);
setUp(() { setUp(() {
whenListen( dashNestBumper = MockDashNestBumper();
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
}); });
final dashNestBumper = MockDashNestBumper(); group(
tester.testGameWidget( 'listensWhen',
'adds a DashNestActivated event with DashNestBumper.id', () {
setUp: (game, tester) async { late GameState previousState;
const id = '0'; late GameState newState;
when(() => dashNestBumper.id).thenReturn(id);
when(() => dashNestBumper.gameRef).thenReturn(game); setUp(
}, () {
verify: (game, tester) async { previousState = MockGameState();
final contactCallback = DashNestBumperBallContactCallback(); newState = MockGameState();
contactCallback.begin(dashNestBumper, MockBall(), MockContact()); },
);
test('listens when the id is added to activatedDashNests', () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({});
when(() => newState.activatedDashNests).thenReturn({id});
expect(controller.listenWhen(previousState, newState), isTrue);
});
test('listens when the id is removed from activatedDashNests', () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({id});
when(() => newState.activatedDashNests).thenReturn({});
expect(controller.listenWhen(previousState, newState), isTrue);
});
test("doesn't listen when the id is never in activatedDashNests", () {
final controller = DashNestBumperController(
dashNestBumper,
id: '',
);
verify( when(() => previousState.activatedDashNests).thenReturn({});
() => gameBloc.add(DashNestActivated(dashNestBumper.id)), when(() => newState.activatedDashNests).thenReturn({});
).called(1);
expect(controller.listenWhen(previousState, newState), isFalse);
});
test("doesn't listen when the id still in activatedDashNests", () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({id});
when(() => newState.activatedDashNests).thenReturn({id});
expect(controller.listenWhen(previousState, newState), isFalse);
});
}, },
); );
});
group('BigDashNestBumper', () { group(
test('has points', () { 'onNewState',
final dashNestBumper = BigDashNestBumper(id: ''); () {
expect(dashNestBumper.points, greaterThan(0)); late GameState state;
});
});
group('SmallDashNestBumper', () { setUp(() {
test('has points', () { state = MockGameState();
final dashNestBumper = SmallDashNestBumper(id: ''); });
expect(dashNestBumper.points, greaterThan(0));
}); test(
'activates the bumper when id in activatedDashNests',
() {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => state.activatedDashNests).thenReturn({id});
controller.onNewState(state);
verify(() => dashNestBumper.activate()).called(1);
},
);
test(
'deactivates the bumper when id not in activatedDashNests',
() {
final controller = DashNestBumperController(
dashNestBumper,
id: '',
);
when(() => state.activatedDashNests).thenReturn({});
controller.onNewState(state);
verify(() => dashNestBumper.deactivate()).called(1);
},
);
},
);
}); });
} }

@ -31,29 +31,33 @@ void main() {
when(() => fixture.filterData).thenReturn(filterData); when(() => fixture.filterData).thenReturn(filterData);
}); });
// TODO(alestiago): Make ContactCallback private and use `beginContact`
// instead.
group('SpaceshipExitHoleBallContactCallback', () { group('SpaceshipExitHoleBallContactCallback', () {
test('changes the ball priority on contact', () { setUp(() {
when(() => ball.priority).thenReturn(1);
when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board); when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board);
when(() => exitRailEnd.outsidePriority).thenReturn(0);
});
test('changes the ball priority on contact', () {
SpaceshipExitRailEndBallContactCallback().begin( SpaceshipExitRailEndBallContactCallback().begin(
exitRailEnd, exitRailEnd,
ball, ball,
MockContact(), MockContact(),
); );
verify(() => ball.priority = 1).called(1); verify(() => ball.sendTo(exitRailEnd.outsidePriority)).called(1);
}); });
test('reorders the game children', () { test('changes the ball layer on contact', () {
when(() => exitRailEnd.outsideLayer).thenReturn(Layer.board);
SpaceshipExitRailEndBallContactCallback().begin( SpaceshipExitRailEndBallContactCallback().begin(
exitRailEnd, exitRailEnd,
ball, ball,
MockContact(), MockContact(),
); );
verify(game.reorderChildren).called(1); verify(() => ball.layer = exitRailEnd.outsideLayer).called(1);
}); });
}); });
}); });

@ -1,21 +1,21 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame/src/game/flame_game.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
FlameTester<T> flameBlocTester<T extends Forge2DGame>({ class FlameBlocTester<T extends FlameGame, B extends Bloc<dynamic, dynamic>>
required T Function() game, extends FlameTester<T> {
required GameBloc Function() gameBloc, FlameBlocTester({
}) { required GameCreateFunction<T> gameBuilder,
return FlameTester<T>( required B Function() blocBuilder,
game, }) : super(
pumpWidget: (gameWidget, tester) async { gameBuilder,
await tester.pumpWidget( pumpWidget: (gameWidget, tester) async {
BlocProvider.value( await tester.pumpWidget(
value: gameBloc(), BlocProvider.value(
child: gameWidget, value: blocBuilder(),
), child: gameWidget,
); ),
}, );
); },
);
} }

@ -66,10 +66,6 @@ class MockFilter extends Mock implements Filter {}
class MockFixture extends Mock implements Fixture {} class MockFixture extends Mock implements Fixture {}
class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {}
class MockSpaceshipHole extends Mock implements SpaceshipHole {}
class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {} class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {}
class MockComponentSet extends Mock implements ComponentSet {} class MockComponentSet extends Mock implements ComponentSet {}

Loading…
Cancel
Save