refactor: Pathway to Shapes (#87)

* refactor: removed findNested extensions (#77)

* refactor: changed Pathway for Shapes

* refactor: renamed pathway to shape

* refactor: moved shapes to components package

* fix: fixed arc radius on shapes

* refactor: changed jetpack to shapes

* refactor: modified jetpack ramp to use shapes and blueprint

* refactor: launcher ramp

* test: removed unnecessary tests for ramps

* refactor: refactored baseboard with arcshapes

* chore: doc refactor

* test: coverage tests

* refactor: refactored launcher ramp

* test: tests for shapes

* test: added removed ellipse tests

* test: arcshape coverage

* test: unnecessary tests removed

* chore: params names

* chore: modified doc for Layered and added one test for nested

* test: changed tests names

* test: not layered nested children

* refactor: moved static param and made other private on ramps

* Update lib/game/components/jetpack_ramp.dart

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

* Update lib/game/components/jetpack_ramp.dart

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

* Update lib/game/components/launcher_ramp.dart

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

* chore: renamed straight path vars

* Update packages/pinball_components/lib/src/components/shapes/arc_shape.dart

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

* refactor: constructors with rotation instead of separated method

* test: refactored tests

* refactor: moved rotate to separate method

* refactor: rotation on shapes

Co-authored-by: RuiAlonso <rui.alonso@verygood.ventures>
Co-authored-by: Rui Miguel Alonso <ruiskas@gmail.com>
Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
pull/104/head
Alejandro Santiago 4 years ago committed by GitHub
parent ce8917e17d
commit 411b489e1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -46,25 +46,23 @@ class Baseboard extends BodyComponent with InitialPosition {
final outerEdgeShapeFixtureDef = FixtureDef(outerEdgeShape);
fixturesDef.add(outerEdgeShapeFixtureDef);
final upperArcFixtureDefs = Pathway.arc(
final upperArcShape = ArcShape(
center: Vector2(1.76 * direction, 3.25),
width: 0,
radius: 6.1,
arcRadius: 6.1,
angle: arcsAngle,
rotation: arcsRotation,
singleWall: true,
).createFixtureDefs();
fixturesDef.addAll(upperArcFixtureDefs);
);
final upperArcFixtureDefs = FixtureDef(upperArcShape);
fixturesDef.add(upperArcFixtureDefs);
final lowerArcFixtureDefs = Pathway.arc(
final lowerArcShape = ArcShape(
center: Vector2(1.85 * direction, -2.15),
width: 0,
radius: 4.5,
arcRadius: 4.5,
angle: arcsAngle,
rotation: arcsRotation,
singleWall: true,
).createFixtureDefs();
fixturesDef.addAll(lowerArcFixtureDefs);
);
final lowerArcFixtureDefs = FixtureDef(lowerArcShape);
fixturesDef.add(lowerArcFixtureDefs);
final bottomRectangle = PolygonShape()
..setAsBox(

@ -9,7 +9,6 @@ export 'jetpack_ramp.dart';
export 'joint_anchor.dart';
export 'kicker.dart';
export 'launcher_ramp.dart';
export 'pathway.dart';
export 'plunger.dart';
export 'ramp_opening.dart';
export 'score_points.dart';

@ -1,63 +1,102 @@
// ignore_for_file: public_member_api_docs, avoid_renaming_method_parameters
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template jetpack_ramp}
/// Represents the upper left blue ramp of the [Board].
/// {@endtemplate}
class JetpackRamp extends Component with HasGameRef<PinballGame> {
/// {@macro jetpack_ramp}
JetpackRamp({
required this.position,
});
/// The position of this [JetpackRamp].
final Vector2 position;
/// A [Blueprint] which creates the [JetpackRamp].
class Jetpack extends Forge2DBlueprint {
@override
Future<void> onLoad() async {
const layer = Layer.jetpack;
void build(_) {
final position = Vector2(
PinballGame.boardBounds.left + 40.5,
PinballGame.boardBounds.top - 31.5,
);
gameRef.addContactCallback(
addAllContactCallback([
RampOpeningBallContactCallback<_JetpackRampOpening>(),
);
]);
final curvePath = Pathway.arc(
// TODO(ruialonso): Remove color when not needed.
// TODO(ruialonso): Use a bezier curve once control points are defined.
color: const Color.fromARGB(255, 8, 218, 241),
center: position,
width: 5,
radius: 18,
angle: math.pi,
final rightOpening = _JetpackRampOpening(
rotation: math.pi,
)..layer = layer;
)
..initialPosition = position + Vector2(12.9, -20)
..layer = Layer.opening;
final leftOpening = _JetpackRampOpening(
outsideLayer: Layer.spaceship,
rotation: math.pi,
)
..initialPosition = position + Vector2(-2.5, -20.2)
..initialPosition = position + Vector2(-2.5, -20)
..layer = Layer.jetpack;
final rightOpening = _JetpackRampOpening(
rotation: math.pi,
)
..initialPosition = position + Vector2(12.9, -20.2)
..layer = Layer.opening;
final jetpackRamp = JetpackRamp()
..initialPosition = position + Vector2(5, -20.2)
..layer = Layer.jetpack;
await addAll([
curvePath,
leftOpening,
addAll([
rightOpening,
leftOpening,
jetpackRamp,
]);
}
}
/// {@template jetpack_ramp}
/// Represents the upper left blue ramp of the [Board].
/// {@endtemplate}
class JetpackRamp extends BodyComponent with InitialPosition, Layered {
JetpackRamp() : super(priority: 2) {
layer = Layer.jetpack;
paint = Paint()
..color = const Color.fromARGB(255, 8, 218, 241)
..style = PaintingStyle.stroke;
}
/// Radius of the external arc.
static const _externalRadius = 18.0;
/// Width between walls of the ramp.
static const width = 5.0;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final externalCurveShape = ArcShape(
center: initialPosition,
arcRadius: _externalRadius,
angle: math.pi,
rotation: math.pi,
);
final externalFixtureDef = FixtureDef(externalCurveShape);
fixturesDef.add(externalFixtureDef);
final internalCurveShape = externalCurveShape.copyWith(
arcRadius: _externalRadius - width,
);
final internalFixtureDef = FixtureDef(internalCurveShape);
fixturesDef.add(internalFixtureDef);
return fixturesDef;
}
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
}
/// {@template jetpack_ramp_opening}
/// [RampOpening] with [Layer.jetpack] to filter [Ball] collisions
/// inside [JetpackRamp].
@ -76,9 +115,7 @@ class _JetpackRampOpening extends RampOpening {
final double _rotation;
// TODO(ruialonso): Avoid magic number 3, should be propotional to
// [JetpackRamp].
static final Vector2 _size = Vector2(3, .1);
static final Vector2 _size = Vector2(JetpackRamp.width / 3, .1);
@override
Shape get shape => PolygonShape()

@ -1,67 +1,118 @@
// ignore_for_file: public_member_api_docs, avoid_renaming_method_parameters
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template launcher_ramp}
/// The yellow left ramp, where the [Ball] goes through when launched from the
/// [Plunger].
/// {@endtemplate}
class LauncherRamp extends Component with HasGameRef<PinballGame> {
/// {@macro launcher_ramp}
LauncherRamp({
required this.position,
});
/// The position of this [LauncherRamp].
final Vector2 position;
/// A [Blueprint] which creates the [LauncherRamp].
class Launcher extends Forge2DBlueprint {
@override
Future<void> onLoad() async {
const layer = Layer.launcher;
gameRef.addContactCallback(
RampOpeningBallContactCallback<_LauncherRampOpening>(),
void build(_) {
final position = Vector2(
PinballGame.boardBounds.right - 31.3,
PinballGame.boardBounds.bottom + 33,
);
final straightPath = Pathway.straight(
color: const Color.fromARGB(255, 34, 255, 0),
start: position + Vector2(-4.5, -10),
end: position + Vector2(-4.5, 117),
width: 4,
rotation: PinballGame.boardPerspectiveAngle,
)
..initialPosition = position
..layer = layer;
final curvedPath = Pathway.arc(
color: const Color.fromARGB(255, 251, 255, 0),
center: position + Vector2(-7, 87.2),
radius: 16.3,
angle: math.pi / 2,
width: 4,
rotation: 3 * math.pi / 2,
)..layer = layer;
addAllContactCallback([
RampOpeningBallContactCallback<_LauncherRampOpening>(),
]);
final leftOpening = _LauncherRampOpening(rotation: math.pi / 2)
..initialPosition = position + Vector2(-13.8, 66.7)
..initialPosition = position + Vector2(-11.8, 72.7)
..layer = Layer.opening;
final rightOpening = _LauncherRampOpening(rotation: 0)
..initialPosition = position + Vector2(-6.8, 59.4)
..initialPosition = position + Vector2(-5.4, 65.4)
..layer = Layer.opening;
await addAll([
straightPath,
curvedPath,
final launcherRamp = LauncherRamp()
..initialPosition = position + Vector2(1.7, 0)
..layer = Layer.launcher;
addAll([
leftOpening,
rightOpening,
launcherRamp,
]);
}
}
/// {@template launcher_ramp}
/// The yellow right ramp, where the [Ball] goes through when launched from the
/// [Plunger].
/// {@endtemplate}
class LauncherRamp extends BodyComponent with InitialPosition, Layered {
/// {@macro launcher_ramp}
LauncherRamp() : super(priority: 2) {
layer = Layer.launcher;
paint = Paint()
..color = const Color.fromARGB(255, 251, 255, 0)
..style = PaintingStyle.stroke;
}
/// Width between walls of the ramp.
static const width = 5.0;
/// Radius of the external arc at the top of the ramp.
static const _externalRadius = 16.3;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final startPosition = initialPosition + Vector2(0, 3);
final endPosition = initialPosition + Vector2(0, 130);
final rightStraightShape = EdgeShape()
..set(
startPosition..rotate(PinballGame.boardPerspectiveAngle),
endPosition..rotate(PinballGame.boardPerspectiveAngle),
);
final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef);
final leftStraightShape = EdgeShape()
..set(
startPosition - Vector2(width, 0),
endPosition - Vector2(width, 0),
);
final leftStraightFixtureDef = FixtureDef(leftStraightShape);
fixturesDef.add(leftStraightFixtureDef);
final externalCurveShape = ArcShape(
center: initialPosition + Vector2(-28.2, 132),
arcRadius: _externalRadius,
angle: math.pi / 2,
rotation: 3 * math.pi / 2,
);
final externalCurveFixtureDef = FixtureDef(externalCurveShape);
fixturesDef.add(externalCurveFixtureDef);
final internalCurveShape = externalCurveShape.copyWith(
arcRadius: _externalRadius - width,
);
final internalCurveFixtureDef = FixtureDef(internalCurveShape);
fixturesDef.add(internalCurveFixtureDef);
return fixturesDef;
}
@override
Body createBody() {
final bodyDef = BodyDef()
..userData = this
..position = initialPosition;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
}
/// {@template launcher_ramp_opening}
/// [RampOpening] with [Layer.launcher] to filter [Ball]s collisions
/// inside [LauncherRamp].
@ -78,9 +129,7 @@ class _LauncherRampOpening extends RampOpening {
final double _rotation;
// TODO(ruialonso): Avoid magic number 2.5, should be propotional to
// [JetpackRamp].
static final Vector2 _size = Vector2(2.5, .1);
static final Vector2 _size = Vector2(LauncherRamp.width / 3, .1);
@override
Shape get shape => PolygonShape()

@ -1,209 +0,0 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:geometry/geometry.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template pathway}
/// [Pathway] creates lines of various shapes.
///
/// [BodyComponent]s such as a Ball can collide and move along a [Pathway].
/// {@endtemplate}
class Pathway extends BodyComponent with InitialPosition, Layered {
Pathway._({
// TODO(ruialonso): remove color when assets added.
Color? color,
required List<List<Vector2>> paths,
}) : _paths = paths {
paint = Paint()
..color = color ?? const Color.fromARGB(0, 0, 0, 0)
..style = PaintingStyle.stroke;
}
/// Creates a uniform unidirectional (straight) [Pathway].
///
/// Does so with two [ChainShape] separated by a [width]. Can
/// be rotated by a given [rotation] in radians.
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.straight({
Color? color,
required Vector2 start,
required Vector2 end,
required double width,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final firstWall = [
start.clone(),
end.clone(),
].map((vector) => vector..rotate(rotation)).toList();
paths.add(firstWall);
if (!singleWall) {
final secondWall = [
start + Vector2(width, 0),
end + Vector2(width, 0),
].map((vector) => vector..rotate(rotation)).toList();
paths.add(secondWall);
}
return Pathway._(
color: color,
paths: paths,
);
}
/// Creates an arc [Pathway].
///
/// The [angle], in radians, specifies the size of the arc. For example, two
/// pi returns a complete circumference.
///
/// Does so with two [ChainShape] separated by a [width]. Which can be
/// rotated by a given [rotation] in radians.
///
/// The outer radius is specified by [radius], whilst the inner one is
/// equivalent to the [radius] minus the [width].
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.arc({
Color? color,
required Vector2 center,
required double width,
required double radius,
required double angle,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final outerWall = calculateArc(
center: center,
radius: radius,
angle: angle,
offsetAngle: rotation,
);
paths.add(outerWall);
if (!singleWall) {
final innerWall = calculateArc(
center: center,
radius: radius - width,
angle: angle,
offsetAngle: rotation,
);
paths.add(innerWall);
}
return Pathway._(
color: color,
paths: paths,
);
}
/// Creates a bezier curve [Pathway].
///
/// Does so with two [ChainShape] separated by a [width]. Which can be
/// rotated by a given [rotation] in radians.
///
/// First and last [controlPoints] set the beginning and end of the curve,
/// inner points between them set its final shape.
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.bezierCurve({
Color? color,
required List<Vector2> controlPoints,
required double width,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final firstWall = calculateBezierCurve(controlPoints: controlPoints)
.map((vector) => vector..rotate(rotation))
.toList();
paths.add(firstWall);
if (!singleWall) {
final secondWall = calculateBezierCurve(
controlPoints: controlPoints
.map((vector) => vector + Vector2(width, -width))
.toList(),
).map((vector) => vector..rotate(rotation)).toList();
paths.add(secondWall);
}
return Pathway._(
color: color,
paths: paths,
);
}
/// Creates an ellipse [Pathway].
///
/// Does so with two [ChainShape]s separated by a [width]. Can
/// be rotated by a given [rotation] in radians.
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.ellipse({
Color? color,
required Vector2 center,
required double width,
required double majorRadius,
required double minorRadius,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final outerWall = calculateEllipse(
center: center,
majorRadius: majorRadius,
minorRadius: minorRadius,
).map((vector) => vector..rotate(rotation)).toList();
paths.add(outerWall);
if (!singleWall) {
final innerWall = calculateEllipse(
center: center,
majorRadius: majorRadius - width,
minorRadius: minorRadius - width,
).map((vector) => vector..rotate(rotation)).toList();
paths.add(innerWall);
}
return Pathway._(
color: color,
paths: paths,
);
}
final List<List<Vector2>> _paths;
/// Constructs different [ChainShape]s to form the [Pathway] shape.
List<FixtureDef> createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
for (final path in _paths) {
final chain = ChainShape()..createChain(path);
fixturesDef.add(FixtureDef(chain));
}
return fixturesDef;
}
@override
Body createBody() {
final bodyDef = BodyDef()..position = initialPosition;
final body = world.createBody(bodyDef);
createFixtureDefs().forEach(body.createFixture);
return body;
}
}

@ -37,10 +37,10 @@ abstract class RampOpening extends BodyComponent with InitialPosition, Layered {
final Layer _pathwayLayer;
final Layer _outsideLayer;
/// Mask of category bits for collision inside [Pathway].
/// Mask of category bits for collision inside pathway.
Layer get pathwayLayer => _pathwayLayer;
/// Mask of category bits for collision outside [Pathway].
/// Mask of category bits for collision outside pathway.
Layer get outsideLayer => _outsideLayer;
/// The [Shape] of the [RampOpening].
@ -65,7 +65,7 @@ abstract class RampOpening extends BodyComponent with InitialPosition, Layered {
}
/// {@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 pathway ramp through a
/// [RampOpening].
///
/// Modifies [Ball]'s [Layer] accordingly depending on whether the [Ball] is

@ -81,23 +81,8 @@ class PinballGame extends Forge2DGame
}
Future<void> _addPaths() async {
final jetpackRamp = JetpackRamp(
position: Vector2(
PinballGame.boardBounds.left + 40.5,
PinballGame.boardBounds.top - 31.5,
),
);
final launcherRamp = LauncherRamp(
position: Vector2(
PinballGame.boardBounds.right - 30,
PinballGame.boardBounds.bottom + 40,
),
);
await addAll([
jetpackRamp,
launcherRamp,
]);
unawaited(addFromBlueprint(Jetpack()));
unawaited(addFromBlueprint(Launcher()));
}
void spawnBall() {

@ -2,3 +2,4 @@ export 'ball.dart';
export 'fire_effect.dart';
export 'initial_position.dart';
export 'layer.dart';
export 'shapes/shapes.dart';

@ -8,6 +8,9 @@ import 'package:flutter/material.dart';
/// [BodyComponent]s with compatible [Layer]s can collide with each other,
/// ignoring others. This compatibility depends on bit masking operation
/// between layers. For more information read: https://en.wikipedia.org/wiki/Mask_(computing).
///
/// A parent [Layered] have priority against its children's layer. Them won't be
/// changed but will be ignored.
/// {@endtemplate}
mixin Layered<T extends Forge2DGame> on BodyComponent<T> {
Layer _layer = Layer.all;

@ -0,0 +1,54 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:geometry/geometry.dart';
/// {@template arc_shape}
/// Creates an arc.
/// {@endtemplate}
class ArcShape extends ChainShape {
/// {@macro arc_shape}
ArcShape({
required this.center,
required this.arcRadius,
required this.angle,
this.rotation = 0,
}) {
createChain(
calculateArc(
center: center,
radius: arcRadius,
angle: angle,
offsetAngle: rotation,
),
);
}
/// The center of the arc.
final Vector2 center;
/// The radius of the arc.
// TODO(alestiago): Check if modifying the parent radius makes sense.
final double arcRadius;
/// Specifies the size of the arc, in radians.
///
/// For example, two pi returns a complete circumference.
final double angle;
/// Angle in radians to rotate the arc around its [center].
final double rotation;
ArcShape copyWith({
Vector2? center,
double? arcRadius,
double? angle,
double? rotation,
}) =>
ArcShape(
center: center ?? this.center,
arcRadius: arcRadius ?? this.arcRadius,
angle: angle ?? this.angle,
rotation: rotation ?? this.rotation,
);
}

@ -0,0 +1,28 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:geometry/geometry.dart';
/// {@template bezier_curve_shape}
/// Creates a bezier curve.
/// {@endtemplate}
class BezierCurveShape extends ChainShape {
/// {@macro bezier_curve_shape}
BezierCurveShape({
required this.controlPoints,
}) {
createChain(calculateBezierCurve(controlPoints: controlPoints));
}
/// Specifies the control points of the curve.
///
/// First and last [controlPoints] set the beginning and end of the curve,
/// inner points between them set its final shape.
final List<Vector2> controlPoints;
/// Rotates the bezier curve by a given [angle] in radians.
void rotate(double angle) {
vertices.map((vector) => vector..rotate(angle)).toList();
}
}

@ -0,0 +1,53 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:geometry/geometry.dart';
/// {@template ellipse_shape}
/// Creates an ellipse.
/// {@endtemplate}
class EllipseShape extends ChainShape {
/// {@macro ellipse_shape}
EllipseShape({
required this.center,
required this.majorRadius,
required this.minorRadius,
}) {
createChain(
calculateEllipse(
center: center,
majorRadius: majorRadius,
minorRadius: minorRadius,
),
);
}
/// The top left corner of the ellipse.
///
/// Where the initial painting begins.
// TODO(ruialonso): Change to use appropiate center.
final Vector2 center;
/// Major radius is specified by [majorRadius].
final double majorRadius;
/// Minor radius is specified by [minorRadius].
final double minorRadius;
/// Rotates the ellipse by a given [angle] in radians.
void rotate(double angle) {
vertices.map((vector) => vector..rotate(angle)).toList();
}
EllipseShape copyWith({
Vector2? center,
double? majorRadius,
double? minorRadius,
}) =>
EllipseShape(
center: center ?? this.center,
majorRadius: majorRadius ?? this.majorRadius,
minorRadius: minorRadius ?? this.minorRadius,
);
}

@ -0,0 +1,3 @@
export 'arc_shape.dart';
export 'bezier_curve_shape.dart';
export 'ellipse_shape.dart';

@ -11,6 +11,9 @@ dependencies:
flame_forge2d: ^0.9.0-releasecandidate.6
flutter:
sdk: flutter
geometry:
path: ../geometry
dev_dependencies:
flame_test: ^1.1.0

@ -6,7 +6,15 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
class TestBodyComponent extends BodyComponent with Layered {
class TestLayeredBodyComponent extends BodyComponent with Layered {
@override
Body createBody() {
final fixtureDef = FixtureDef(CircleShape());
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}
class TestBodyComponent extends BodyComponent {
@override
Body createBody() {
final fixtureDef = FixtureDef(CircleShape());
@ -38,7 +46,7 @@ void main() {
});
test('correctly sets and gets', () {
final component = TestBodyComponent()..layer = Layer.jetpack;
final component = TestLayeredBodyComponent()..layer = Layer.jetpack;
expect(component.layer, Layer.jetpack);
});
@ -46,7 +54,7 @@ void main() {
'layers correctly before being loaded',
(game) async {
const expectedLayer = Layer.jetpack;
final component = TestBodyComponent()..layer = expectedLayer;
final component = TestLayeredBodyComponent()..layer = expectedLayer;
await game.ensureAdd(component);
// TODO(alestiago): modify once component.loaded is available.
await component.mounted;
@ -63,7 +71,7 @@ void main() {
'when multiple different sets',
(game) async {
const expectedLayer = Layer.launcher;
final component = TestBodyComponent()..layer = Layer.jetpack;
final component = TestLayeredBodyComponent()..layer = Layer.jetpack;
expect(component.layer, isNot(equals(expectedLayer)));
component.layer = expectedLayer;
@ -83,7 +91,7 @@ void main() {
'layers correctly after being loaded',
(game) async {
const expectedLayer = Layer.jetpack;
final component = TestBodyComponent();
final component = TestLayeredBodyComponent();
await game.ensureAdd(component);
component.layer = expectedLayer;
_expectLayerOnFixtures(
@ -98,7 +106,7 @@ void main() {
'when multiple different sets',
(game) async {
const expectedLayer = Layer.launcher;
final component = TestBodyComponent();
final component = TestLayeredBodyComponent();
await game.ensureAdd(component);
component.layer = Layer.jetpack;
@ -116,11 +124,55 @@ void main() {
'defaults to Layer.all '
'when no layer is given',
(game) async {
final component = TestBodyComponent();
final component = TestLayeredBodyComponent();
await game.ensureAdd(component);
expect(component.layer, equals(Layer.all));
},
);
flameTester.test(
'nested Layered children will keep their layer',
(game) async {
const parentLayer = Layer.jetpack;
const childLayer = Layer.board;
final component = TestLayeredBodyComponent()..layer = parentLayer;
final childComponent = TestLayeredBodyComponent()..layer = childLayer;
await component.add(childComponent);
await game.ensureAdd(component);
expect(childLayer, isNot(equals(parentLayer)));
for (final child in component.children) {
expect((child as TestLayeredBodyComponent).layer, equals(childLayer));
}
},
);
flameTester.test(
'nested children will keep their layer',
(game) async {
const parentLayer = Layer.jetpack;
final component = TestLayeredBodyComponent()..layer = parentLayer;
final childComponent = TestBodyComponent();
await component.add(childComponent);
await game.ensureAdd(component);
for (final child in component.children) {
expect(
(child as TestBodyComponent)
.body
.fixtures
.first
.filterData
.maskBits,
equals(Filter().maskBits),
);
}
},
);
});
group('LayerMaskBits', () {

@ -0,0 +1,66 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/components/components.dart';
void main() {
group('ArcShape', () {
test('can be instantiated', () {
expect(
ArcShape(
center: Vector2.zero(),
arcRadius: 10,
angle: 2 * math.pi,
),
isNotNull,
);
});
group('copyWith', () {
test(
'copies correctly '
'when no argument specified', () {
final arcShape = ArcShape(
center: Vector2.zero(),
arcRadius: 10,
angle: 2 * math.pi,
);
final arcShapeCopied = arcShape.copyWith();
for (var index = 0; index < arcShape.vertices.length; index++) {
expect(
arcShape.vertices[index],
equals(arcShapeCopied.vertices[index]),
);
}
});
test(
'copies correctly '
'when all arguments specified', () {
final arcShapeExpected = ArcShape(
center: Vector2.all(10),
arcRadius: 15,
angle: 2 * math.pi,
);
final arcShapeCopied = ArcShape(
center: Vector2.zero(),
arcRadius: 10,
angle: math.pi,
).copyWith(
center: Vector2.all(10),
arcRadius: 15,
angle: 2 * math.pi,
);
for (var index = 0; index < arcShapeCopied.vertices.length; index++) {
expect(
arcShapeCopied.vertices[index],
equals(arcShapeExpected.vertices[index]),
);
}
});
});
});
}

@ -0,0 +1,51 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/components/components.dart';
void main() {
group('BezierCurveShape', () {
final controlPoints = [
Vector2(0, 0),
Vector2(10, 0),
Vector2(0, 10),
Vector2(10, 10),
];
test('can be instantiated', () {
expect(
BezierCurveShape(
controlPoints: controlPoints,
),
isNotNull,
);
});
group('rotate', () {
test('returns vertices rotated', () {
const rotationAngle = 2 * math.pi;
final controlPoints = [
Vector2(0, 0),
Vector2(10, 0),
Vector2(0, 10),
Vector2(10, 10),
];
final bezierCurveShape = BezierCurveShape(
controlPoints: controlPoints,
);
final bezierCurveShapeRotated = BezierCurveShape(
controlPoints: controlPoints,
)..rotate(rotationAngle);
for (var index = 0; index < bezierCurveShape.vertices.length; index++) {
expect(
bezierCurveShape.vertices[index]..rotate(rotationAngle),
equals(bezierCurveShapeRotated.vertices[index]),
);
}
});
});
});
}

@ -0,0 +1,83 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/src/components/components.dart';
void main() {
group('EllipseShape', () {
test('can be instantiated', () {
expect(
EllipseShape(
center: Vector2.zero(),
majorRadius: 10,
minorRadius: 8,
),
isNotNull,
);
});
group('rotate', () {
test('returns vertices rotated', () {
const rotationAngle = 2 * math.pi;
final ellipseShape = EllipseShape(
center: Vector2.zero(),
majorRadius: 10,
minorRadius: 8,
);
final ellipseShapeRotated = EllipseShape(
center: Vector2.zero(),
majorRadius: 10,
minorRadius: 8,
)..rotate(rotationAngle);
for (var index = 0; index < ellipseShape.vertices.length; index++) {
expect(
ellipseShape.vertices[index]..rotate(rotationAngle),
equals(ellipseShapeRotated.vertices[index]),
);
}
});
});
group('copyWith', () {
test('returns same shape when no properties are passed', () {
final ellipseShape = EllipseShape(
center: Vector2.zero(),
majorRadius: 10,
minorRadius: 8,
);
final ellipseShapeCopied = ellipseShape.copyWith();
for (var index = 0; index < ellipseShape.vertices.length; index++) {
expect(
ellipseShape.vertices[index],
equals(ellipseShapeCopied.vertices[index]),
);
}
});
test('returns object with updated properties when are passed', () {
final ellipseShapeExpected = EllipseShape(
center: Vector2.all(10),
majorRadius: 10,
minorRadius: 8,
);
final ellipseShapeCopied = EllipseShape(
center: Vector2.zero(),
majorRadius: 10,
minorRadius: 8,
).copyWith(center: Vector2.all(10));
for (var index = 0;
index < ellipseShapeCopied.vertices.length;
index++) {
expect(
ellipseShapeCopied.vertices[index],
equals(ellipseShapeExpected.vertices[index]),
);
}
});
});
});
}

@ -186,7 +186,7 @@ void main() {
test(
'copies correctly '
'when no arguement specified',
'when no argument specified',
() {
const gameState = GameState(
balls: 0,
@ -204,7 +204,7 @@ void main() {
test(
'copies correctly '
'when all arguements specified',
'when all arguments specified',
() {
const gameState = GameState(
score: 2,

@ -1,63 +0,0 @@
// 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/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('JetpackRamp', () {
flameTester.test(
'loads correctly',
(game) async {
final ramp = JetpackRamp(
position: Vector2.zero(),
);
await game.ready();
await game.ensureAdd(ramp);
expect(game.contains(ramp), isTrue);
},
);
group('children', () {
flameTester.test(
'has only one Pathway.arc',
(game) async {
final ramp = JetpackRamp(
position: Vector2.zero(),
);
await game.ready();
await game.ensureAdd(ramp);
expect(
() => ramp.children.singleWhere(
(component) => component is Pathway,
),
returnsNormally,
);
},
);
flameTester.test(
'has a two RampOpenings for the ramp',
(game) async {
final ramp = JetpackRamp(
position: Vector2.zero(),
);
await game.ready();
await game.ensureAdd(ramp);
final rampAreas = ramp.children.whereType<RampOpening>();
expect(rampAreas.length, 2);
},
);
});
});
}

@ -1,74 +0,0 @@
// 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/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('LauncherRamp', () {
flameTester.test(
'loads correctly',
(game) async {
final ramp = LauncherRamp(
position: Vector2.zero(),
);
await game.ready();
await game.ensureAdd(ramp);
expect(game.contains(ramp), isTrue);
},
);
group('constructor', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final ramp = LauncherRamp(
position: position,
);
await game.ensureAdd(ramp);
expect(ramp.position, equals(position));
},
);
});
group('children', () {
flameTester.test(
'has two Pathway',
(game) async {
final ramp = LauncherRamp(
position: Vector2.zero(),
);
await game.ready();
await game.ensureAdd(ramp);
final pathways = ramp.children.whereType<Pathway>().toList();
expect(pathways.length, 2);
},
);
flameTester.test(
'has a two RampOpenings for the ramp',
(game) async {
final ramp = LauncherRamp(
position: Vector2.zero(),
);
await game.ready();
await game.ensureAdd(ramp);
final rampAreas = ramp.children.whereType<RampOpening>().toList();
expect(rampAreas.length, 2);
},
);
});
});
}

@ -1,243 +0,0 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:math' as math;
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
group('Pathway', () {
const width = 50.0;
group('straight', () {
flameTester.test(
'loads correctly',
(game) async {
final pathway = Pathway.straight(
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ready();
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('color', () {
flameTester.test(
'has transparent color by default when no color is specified',
(game) async {
final pathway = Pathway.straight(
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ready();
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
expect(pathway.paint, isNotNull);
expect(
pathway.paint.color,
equals(Color.fromARGB(0, 0, 0, 0)),
);
},
);
flameTester.test(
'has a color when is specified',
(game) async {
const defaultColor = Colors.blue;
final pathway = Pathway.straight(
color: defaultColor,
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ready();
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
expect(pathway.paint, isNotNull);
expect(pathway.paint.color.value, equals(defaultColor.value));
},
);
});
group('body', () {
flameTester.test(
'is static',
(game) async {
final pathway = Pathway.straight(
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ready();
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
group('fixtures', () {
flameTester.test(
'has only one ChainShape when singleWall is true',
(game) async {
final pathway = Pathway.straight(
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
singleWall: true,
);
await game.ready();
await game.ensureAdd(pathway);
expect(pathway.body.fixtures.length, 1);
final fixture = pathway.body.fixtures[0];
expect(fixture, isA<Fixture>());
expect(fixture.shape.shapeType, equals(ShapeType.chain));
},
);
flameTester.test(
'has two ChainShape when singleWall is false (default)',
(game) async {
final pathway = Pathway.straight(
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ready();
await game.ensureAdd(pathway);
expect(pathway.body.fixtures.length, 2);
for (final fixture in pathway.body.fixtures) {
expect(fixture, isA<Fixture>());
expect(fixture.shape.shapeType, equals(ShapeType.chain));
}
},
);
});
});
group('arc', () {
flameTester.test(
'loads correctly',
(game) async {
final pathway = Pathway.arc(
center: Vector2.zero(),
width: width,
radius: 100,
angle: math.pi / 2,
);
await game.ready();
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final pathway = Pathway.arc(
center: Vector2.zero(),
width: width,
radius: 100,
angle: math.pi / 2,
);
await game.ready();
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
});
group('ellipse', () {
flameTester.test(
'loads correctly',
(game) async {
final pathway = Pathway.ellipse(
center: Vector2.zero(),
width: width,
majorRadius: 150,
minorRadius: 70,
);
await game.ready();
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final pathway = Pathway.ellipse(
center: Vector2.zero(),
width: width,
majorRadius: 150,
minorRadius: 70,
);
await game.ready();
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
});
group('bezier curve', () {
final controlPoints = [
Vector2(0, 0),
Vector2(50, 0),
Vector2(0, 50),
Vector2(50, 50),
];
flameTester.test(
'loads correctly',
(game) async {
final pathway = Pathway.bezierCurve(
controlPoints: controlPoints,
width: width,
);
await game.ready();
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final pathway = Pathway.bezierCurve(
controlPoints: controlPoints,
width: width,
);
await game.ready();
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
});
});
}

@ -62,32 +62,6 @@ void main() {
});
});
group('Paths', () {
flameTester.test(
'has only one JetpackRamp',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
(component) => component is JetpackRamp,
),
returnsNormally,
);
},
);
flameTester.test(
'has only one LauncherRamp',
(game) async {
await game.ready();
final rampAreas = game.children.whereType<LauncherRamp>().toList();
expect(rampAreas.length, 1);
},
);
});
debugModeFlameTester.test('adds a ball on tap up', (game) async {
await game.ready();

Loading…
Cancel
Save