feat: crossing upper ramps (#40)

* feat: added generic area and area callback for ramps crossing

* feat: added jetpack ramp (blue one) and own area, contact callback and maskbits

* feat: added sparky ramp (yellow one) and own area, contact callback and maskbits

* feat: included ramp components

* feat: added maskbits to ball for collisions

* feat: added paths to pinball game

* feat: added maskbits to paths

* fix: fixed collisions of a ball that only touch path entrance but doesn't get into

* fix: analysis warnings

* feat: ball default maskbits

* chore: refactor some names and vars

* test: tests for ramps and callbacks, and coverage

* test: pinball game check ramps are added

* test: tests for ramps check childrens

* test: fixing tests for ramps

* test: fix tests

* chore: increase sparky angle

* fix: placed plunge aligned with straight launcher path

* fix: fixed maskBits change for ball on crossing ramps and tests coverage

* doc: public member api docs

* chore: placed launcher ramp

* test: moved mock from crossing ramps to helpers file

* fix: build and dep where broken by forge2d/position_body_component

* Update lib/game/components/crossing_ramp.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Update lib/game/components/jetpack_ramp.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Update test/game/components/crossing_ramp_test.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Update test/game/components/jetpack_ramp_test.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Update test/game/components/sparky_ramp_test.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Update lib/game/components/jetpack_ramp.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* chore: fixed formatting

* chore: removed coverage tool

* Update lib/game/components/crossing_ramp.dart

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

* Update test/game/components/ball_test.dart

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

* Update lib/game/components/crossing_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 test/game/components/crossing_ramp_test.dart

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

* Update test/game/components/pathway_test.dart

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

* Update test/game/pinball_game_test.dart

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

* Update test/game/components/pathway_test.dart

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

* test: fix tests and groups

* chore: ramp area name changed to opening

* refactor: ball with mixin Layer for mask collisions

* chore: avoid foreach in a function literal

* refactor: hide maskbits and manage only with layer param

* chore: formatting file

* refactor: changed name for ramp area

* refactor: sparky+launcher into one path

* doc: doc layer for ball

* refactor: sparky to launcher

* feat: allow jetpack ramp to be over the board

* feat: refactor to allow jetpack ramp to be above board and launcher ramp

* test: coverage

* fix: fixed conflict with merge Component position

* chore: analysis fixes

* chore: doc and comments

* refactor: initial position to ramps and cleaned ramp callbacks

* refactor: improved ramp contact callback

* refactor: ball layer and ramp addAll components

* refactor: create fixtures for pathways and opening improved

* refactor: placed ramps on pinball game

* refactor: splitted layer from rampopening

* refactor: rampopening with layered mixin

* test: fixed all changes with tests

* test: fixed tests after Layer mixin changes

* chore: refactor names, test and doc

* chore: review docs and names

* fix: fixed tests and bug with initialposition collision

* chore: analysis error

* fix: fixed collision end from ramps

* test: coverage

* chore: fixed spaces between methods and other comments from pr

* chore: remove unnecessary layer set on Layered

* fix: removed unrelated files from pr

* chore: removed unused import

* refactor: ballsInside private and removed from tests

* chore: todo comment

* chore: removed unused import

* chore: removed podfile

* doc: changed Layered doc

* doc: changed Layered doc

* Update test/game/components/ramp_opening_test.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* Update lib/game/components/layer.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* docs: improved punctuation

Co-authored-by: Erick <erickzanardoo@gmail.com>
Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
Co-authored-by: Alejandro Santiago <dev@alestiago.com>
pull/64/head
Rui Miguel Alonso 3 years ago committed by GitHub
parent 81396d337f
commit 6867187659
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,9 +6,16 @@ import 'package:pinball/game/game.dart';
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the /// A solid, [BodyType.dynamic] sphere that rolls and bounces along the
/// [PinballGame]. /// [PinballGame].
/// {@endtemplate} /// {@endtemplate}
class Ball extends BodyComponent<PinballGame> with InitialPosition { class Ball extends BodyComponent<PinballGame> with InitialPosition, Layered {
/// {@macro ball} /// {@macro ball}
Ball(); Ball() {
// TODO(ruimiguel): while developing Ball can be launched by clicking mouse,
// and default layer is Layer.all. But on final game Ball will be always be
// be launched from Plunger and LauncherRamp will modify it to Layer.board.
// We need to see what happens if Ball appears from other place like nest
// bumper, it will need to explicit change layer to Layer.board then.
layer = Layer.board;
}
/// The size of the [Ball] /// The size of the [Ball]
final Vector2 size = Vector2.all(2); final Vector2 size = Vector2.all(2);

@ -22,7 +22,7 @@ class Baseboard extends BodyComponent with InitialPosition {
final BoardSide _side; final BoardSide _side;
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixtures = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
final circleShape1 = CircleShape()..radius = Baseboard.height / 2; final circleShape1 = CircleShape()..radius = Baseboard.height / 2;
circleShape1.position.setValues( circleShape1.position.setValues(
@ -30,7 +30,7 @@ class Baseboard extends BodyComponent with InitialPosition {
0, 0,
); );
final circle1FixtureDef = FixtureDef(circleShape1); final circle1FixtureDef = FixtureDef(circleShape1);
fixtures.add(circle1FixtureDef); fixturesDef.add(circle1FixtureDef);
final circleShape2 = CircleShape()..radius = Baseboard.height / 2; final circleShape2 = CircleShape()..radius = Baseboard.height / 2;
circleShape2.position.setValues( circleShape2.position.setValues(
@ -38,7 +38,7 @@ class Baseboard extends BodyComponent with InitialPosition {
0, 0,
); );
final circle2FixtureDef = FixtureDef(circleShape2); final circle2FixtureDef = FixtureDef(circleShape2);
fixtures.add(circle2FixtureDef); fixturesDef.add(circle2FixtureDef);
final rectangle = PolygonShape() final rectangle = PolygonShape()
..setAsBoxXY( ..setAsBoxXY(
@ -46,9 +46,9 @@ class Baseboard extends BodyComponent with InitialPosition {
Baseboard.height / 2, Baseboard.height / 2,
); );
final rectangleFixtureDef = FixtureDef(rectangle); final rectangleFixtureDef = FixtureDef(rectangle);
fixtures.add(rectangleFixtureDef); fixturesDef.add(rectangleFixtureDef);
return fixtures; return fixturesDef;
} }
@override @override

@ -5,9 +5,13 @@ export 'board_side.dart';
export 'bonus_word.dart'; export 'bonus_word.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'initial_position.dart'; export 'initial_position.dart';
export 'jetpack_ramp.dart';
export 'joint_anchor.dart'; export 'joint_anchor.dart';
export 'launcher_ramp.dart';
export 'layer.dart';
export 'pathway.dart'; export 'pathway.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'ramp_opening.dart';
export 'round_bumper.dart'; export 'round_bumper.dart';
export 'score_points.dart'; export 'score_points.dart';
export 'sling_shot.dart'; export 'sling_shot.dart';

@ -131,7 +131,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixtures = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
final isLeft = side.isLeft; final isLeft = side.isLeft;
final bigCircleShape = CircleShape()..radius = height / 2; final bigCircleShape = CircleShape()..radius = height / 2;
@ -142,7 +142,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
0, 0,
); );
final bigCircleFixtureDef = FixtureDef(bigCircleShape); final bigCircleFixtureDef = FixtureDef(bigCircleShape);
fixtures.add(bigCircleFixtureDef); fixturesDef.add(bigCircleFixtureDef);
final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2; final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2;
smallCircleShape.position.setValues( smallCircleShape.position.setValues(
@ -152,7 +152,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
0, 0,
); );
final smallCircleFixtureDef = FixtureDef(smallCircleShape); final smallCircleFixtureDef = FixtureDef(smallCircleShape);
fixtures.add(smallCircleFixtureDef); fixturesDef.add(smallCircleFixtureDef);
final trapeziumVertices = isLeft final trapeziumVertices = isLeft
? [ ? [
@ -171,9 +171,9 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
final trapeziumFixtureDef = FixtureDef(trapezium) final trapeziumFixtureDef = FixtureDef(trapezium)
..density = 50.0 // TODO(alestiago): Use a proper density. ..density = 50.0 // TODO(alestiago): Use a proper density.
..friction = .1; // TODO(alestiago): Use a proper friction. ..friction = .1; // TODO(alestiago): Use a proper friction.
fixtures.add(trapeziumFixtureDef); fixturesDef.add(trapeziumFixtureDef);
return fixtures; return fixturesDef;
} }
@override @override

@ -0,0 +1,87 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.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;
@override
Future<void> onLoad() async {
const layer = Layer.jetpack;
gameRef.addContactCallback(
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: 80,
radius: 200,
angle: 7 * math.pi / 6,
rotation: -math.pi / 18,
)
..initialPosition = position
..layer = layer;
final leftOpening = _JetpackRampOpening(
rotation: 15 * math.pi / 180,
)
..initialPosition = position + Vector2(-27, 21)
..layer = Layer.opening;
final rightOpening = _JetpackRampOpening(
rotation: -math.pi / 20,
)
..initialPosition = position + Vector2(-11.2, 22.5)
..layer = Layer.opening;
await addAll([
curvePath,
leftOpening,
rightOpening,
]);
}
}
/// {@template jetpack_ramp_opening}
/// [RampOpening] with [Layer.jetpack] to filter [Ball] collisions
/// inside [JetpackRamp].
/// {@endtemplate}
class _JetpackRampOpening extends RampOpening {
/// {@macro jetpack_ramp_opening}
_JetpackRampOpening({
required double rotation,
}) : _rotation = rotation,
super(
pathwayLayer: Layer.jetpack,
orientation: RampOrientation.down,
);
final double _rotation;
// TODO(ruialonso): Avoid magic number 3, should be propotional to
// [JetpackRamp].
static final Vector2 _size = Vector2(3, .1);
@override
Shape get shape => PolygonShape()
..setAsBox(
_size.x,
_size.y,
initialPosition,
_rotation,
);
}

@ -0,0 +1,90 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.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;
@override
Future<void> onLoad() async {
const layer = Layer.launcher;
gameRef.addContactCallback(
RampOpeningBallContactCallback<_LauncherRampOpening>(),
);
final straightPath = Pathway.straight(
color: const Color.fromARGB(255, 34, 255, 0),
start: Vector2(0, 0),
end: Vector2(0, 700),
width: 80,
)
..initialPosition = position
..layer = layer;
final curvedPath = Pathway.arc(
color: const Color.fromARGB(255, 251, 255, 0),
center: position + Vector2(-29, -8),
radius: 300,
angle: 10 * math.pi / 9,
width: 80,
)
..initialPosition = position + Vector2(-28.8, -6)
..layer = layer;
final leftOpening = _LauncherRampOpening(rotation: 13 * math.pi / 180)
..initialPosition = position + Vector2(-72.5, 12)
..layer = Layer.opening;
final rightOpening = _LauncherRampOpening(rotation: 0)
..initialPosition = position + Vector2(-46.8, 17)
..layer = Layer.opening;
await addAll([
straightPath,
curvedPath,
leftOpening,
rightOpening,
]);
}
}
/// {@template launcher_ramp_opening}
/// [RampOpening] with [Layer.launcher] to filter [Ball]s collisions
/// inside [LauncherRamp].
/// {@endtemplate}
class _LauncherRampOpening extends RampOpening {
/// {@macro launcher_ramp_opening}
_LauncherRampOpening({
required double rotation,
}) : _rotation = rotation,
super(
pathwayLayer: Layer.launcher,
orientation: RampOrientation.down,
);
final double _rotation;
// TODO(ruialonso): Avoid magic number 3, should be propotional to
// [JetpackRamp].
static final Vector2 _size = Vector2(3, .1);
@override
Shape get shape => PolygonShape()
..setAsBox(
_size.x,
_size.y,
initialPosition,
_rotation,
);
}

@ -0,0 +1,86 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
/// {@template layered}
/// Modifies maskBits and categoryBits of all the [BodyComponent]'s [Fixture]s
/// to specify what other [BodyComponent]s it can collide with.
///
/// [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).
/// {@endtemplate}
mixin Layered<T extends Forge2DGame> on BodyComponent<T> {
Layer _layer = Layer.all;
/// {@macro layered}
Layer get layer => _layer;
set layer(Layer value) {
_layer = value;
if (!isLoaded) {
// TODO(alestiago): Use loaded.whenComplete once provided.
mounted.whenComplete(_applyMaskBits);
} else {
_applyMaskBits();
}
}
void _applyMaskBits() {
for (final fixture in body.fixtures) {
fixture
..filterData.categoryBits = layer.maskBits
..filterData.maskBits = layer.maskBits;
}
}
}
/// The [Layer]s a [BodyComponent] can be in.
///
/// Each [Layer] is associated with a maskBits value to define possible
/// collisions within that plane.
///
/// Usually used with [Layered].
enum Layer {
/// Collide with all elements.
all,
/// Collide only with board elements (the ground level).
board,
/// Collide only with ramps opening elements.
opening,
/// Collide only with Jetpack group elements.
jetpack,
/// Collide only with Launcher group elements.
launcher,
}
/// {@template layer_mask_bits}
/// Specifies the maskBits of each [Layer].
///
/// Used by [Layered] to specify what other [BodyComponent]s it can collide
///
/// Note: the maximum value for maskBits is 2^16.
/// {@endtemplate}
@visibleForTesting
extension LayerMaskBits on Layer {
/// {@macro layer_mask_bits}
@visibleForTesting
int get maskBits {
// TODO(ruialonso): test bit groups once final design is implemented.
switch (this) {
case Layer.all:
return 0xFFFF;
case Layer.board:
return 0x0001;
case Layer.opening:
return 0x0007;
case Layer.jetpack:
return 0x0002;
case Layer.launcher:
return 0x0005;
}
}
}

@ -9,7 +9,7 @@ import 'package:pinball/game/game.dart';
/// ///
/// [BodyComponent]s such as a Ball can collide and move along a [Pathway]. /// [BodyComponent]s such as a Ball can collide and move along a [Pathway].
/// {@endtemplate} /// {@endtemplate}
class Pathway extends BodyComponent with InitialPosition { class Pathway extends BodyComponent with InitialPosition, Layered {
Pathway._({ Pathway._({
// TODO(ruialonso): remove color when assets added. // TODO(ruialonso): remove color when assets added.
Color? color, Color? color,
@ -146,20 +146,26 @@ class Pathway extends BodyComponent with InitialPosition {
final List<List<Vector2>> _paths; final List<List<Vector2>> _paths;
@override List<FixtureDef> _createFixtureDefs() {
Body createBody() { final fixturesDef = <FixtureDef>[];
final bodyDef = BodyDef()..position = initialPosition;
final body = world.createBody(bodyDef);
for (final path in _paths) { for (final path in _paths) {
final chain = ChainShape() final chain = ChainShape()
..createChain( ..createChain(
path.map(gameRef.screenToWorld).toList(), path.map(gameRef.screenToWorld).toList(),
); );
final fixtureDef = FixtureDef(chain); fixturesDef.add(FixtureDef(chain));
}
body.createFixture(fixtureDef); return fixturesDef;
} }
@override
Body createBody() {
final bodyDef = BodyDef()..position = initialPosition;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body; return body;
} }
} }

@ -0,0 +1,105 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
/// {@template ramp_orientation}
/// Determines if a ramp is facing [up] or [down] on the [Board].
/// {@endtemplate}
enum RampOrientation {
/// Facing up on the [Board].
up,
/// Facing down on the [Board].
down,
}
/// {@template ramp_opening}
/// [BodyComponent] located at the entrance and exit of a ramp.
///
/// [RampOpeningBallContactCallback] detects when a [Ball] passes
/// through this opening.
///
/// By default the base [layer] is set to [Layer.board].
/// {@endtemplate}
// TODO(ruialonso): Consider renaming the class.
abstract class RampOpening extends BodyComponent with InitialPosition, Layered {
/// {@macro ramp_opening}
RampOpening({
required Layer pathwayLayer,
required this.orientation,
}) : _pathwayLayer = pathwayLayer {
layer = Layer.board;
}
final Layer _pathwayLayer;
/// Mask of category bits for collision inside [Pathway].
Layer get pathwayLayer => _pathwayLayer;
/// The [Shape] of the [RampOpening].
Shape get shape;
/// {@macro ramp_orientation}
// TODO(ruimiguel): Try to remove the need of [RampOrientation] for collision
// calculations.
final RampOrientation orientation;
@override
Body createBody() {
final fixtureDef = FixtureDef(shape)..isSensor = true;
final bodyDef = BodyDef()
..userData = this
..position = initialPosition
..type = BodyType.static;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@template ramp_opening_ball_contact_callback}
/// Detects when a [Ball] enters or exits a [Pathway] ramp through a
/// [RampOpening].
///
/// Modifies [Ball]'s [Layer] accordingly depending on whether the [Ball] is
/// outside or inside a ramp.
/// {@endtemplate}
class RampOpeningBallContactCallback<Opening extends RampOpening>
extends ContactCallback<Ball, Opening> {
/// [Ball]s currently inside the ramp.
final _ballsInside = <Ball>{};
@override
void begin(Ball ball, Opening opening, Contact _) {
Layer layer;
if (!_ballsInside.contains(ball)) {
layer = opening.pathwayLayer;
_ballsInside.add(ball);
ball.layer = layer;
} else {
_ballsInside.remove(ball);
}
}
@override
void end(Ball ball, Opening opening, Contact _) {
if (!_ballsInside.contains(ball)) {
ball.layer = Layer.board;
} else {
// TODO(ruimiguel): change this code. Check what happens with ball that
// slightly touch Opening and goes out again. With InitialPosition change
// now doesn't work position.y comparison
final isBallOutsideOpening =
(opening.orientation == RampOrientation.down &&
ball.body.linearVelocity.y < 0) ||
(opening.orientation == RampOrientation.up &&
ball.body.linearVelocity.y > 0);
if (isBallOutsideOpening) {
ball.layer = Layer.board;
_ballsInside.remove(ball);
}
}
}
}

@ -34,7 +34,7 @@ class SlingShot extends BodyComponent with InitialPosition {
static final Vector2 size = Vector2(6, 8); static final Vector2 size = Vector2(6, 8);
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixtures = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
// TODO(alestiago): This magic number can be deduced by specifying the // TODO(alestiago): This magic number can be deduced by specifying the
// angle and using polar coordinate system to place the bottom right // angle and using polar coordinate system to place the bottom right
@ -65,7 +65,7 @@ class SlingShot extends BodyComponent with InitialPosition {
final triangle = PolygonShape()..set(triangleVertices); final triangle = PolygonShape()..set(triangleVertices);
final triangleFixtureDef = FixtureDef(triangle)..friction = 0; final triangleFixtureDef = FixtureDef(triangle)..friction = 0;
fixtures.add(triangleFixtureDef); fixturesDef.add(triangleFixtureDef);
final kicker = EdgeShape() final kicker = EdgeShape()
..set( ..set(
@ -76,9 +76,9 @@ class SlingShot extends BodyComponent with InitialPosition {
final kickerFixtureDef = FixtureDef(kicker) final kickerFixtureDef = FixtureDef(kicker)
..restitution = 10.0 ..restitution = 10.0
..friction = 0; ..friction = 0;
fixtures.add(kickerFixtureDef); fixturesDef.add(kickerFixtureDef);
return fixtures; return fixturesDef;
} }
@override @override

@ -27,6 +27,7 @@ class PinballGame extends Forge2DGame
await _addGameBoundaries(); await _addGameBoundaries();
unawaited(_addPlunger()); unawaited(_addPlunger());
unawaited(_addPaths());
// Corner wall above plunger so the ball deflects into the rest of the // Corner wall above plunger so the ball deflects into the rest of the
// board. // board.
@ -96,13 +97,34 @@ class PinballGame extends Forge2DGame
createBoundaries(this).forEach(add); createBoundaries(this).forEach(add);
} }
Future<void> _addPaths() async {
final jetpackRamp = JetpackRamp(
position: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2 - 150,
camera.viewport.effectiveSize.y / 2 - 250,
),
),
);
final launcherRamp = LauncherRamp(
position: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2 + 400,
camera.viewport.effectiveSize.y / 2 - 330,
),
),
);
await addAll([jetpackRamp, launcherRamp]);
}
Future<void> _addPlunger() async { Future<void> _addPlunger() async {
plunger = Plunger( plunger = Plunger(
compressionDistance: camera.viewport.effectiveSize.y / 12, compressionDistance: camera.viewport.effectiveSize.y / 12,
); );
plunger.initialPosition = screenToWorld( plunger.initialPosition = screenToWorld(
Vector2( Vector2(
camera.viewport.effectiveSize.x / 1.035, camera.viewport.effectiveSize.x / 2 + 450,
camera.viewport.effectiveSize.y - plunger.compressionDistance, camera.viewport.effectiveSize.y - plunger.compressionDistance,
), ),
); );

@ -70,6 +70,18 @@ void main() {
expect(fixture.shape.radius, equals(1)); expect(fixture.shape.radius, equals(1));
}, },
); );
flameTester.test(
'has Layer.all as default filter maskBits',
(game) async {
final ball = Ball();
await game.ensureAdd(ball);
await ball.mounted;
final fixture = ball.body.fixtures[0];
expect(fixture.filterData.maskBits, equals(Layer.board.maskBits));
},
);
}); });
group('lost', () { group('lost', () {

@ -0,0 +1,63 @@
// 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);
},
);
});
});
}

@ -0,0 +1,74 @@
// 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);
},
);
});
});
}

@ -0,0 +1,144 @@
// ignore_for_file: cascade_invocations
import 'dart:math' as math;
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';
class TestBodyComponent extends BodyComponent with Layered {
@override
Body createBody() {
final fixtureDef = FixtureDef(CircleShape());
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}
void main() {
final flameTester = FlameTester(Forge2DGame.new);
group('Layered', () {
void _expectLayerOnFixtures({
required List<Fixture> fixtures,
required Layer layer,
}) {
expect(fixtures.length, greaterThan(0));
for (final fixture in fixtures) {
expect(
fixture.filterData.categoryBits,
equals(layer.maskBits),
);
expect(fixture.filterData.maskBits, equals(layer.maskBits));
}
}
flameTester.test('TestBodyComponent has fixtures', (game) async {
final component = TestBodyComponent();
await game.ensureAdd(component);
});
test('correctly sets and gets', () {
final component = TestBodyComponent()..layer = Layer.jetpack;
expect(component.layer, Layer.jetpack);
});
flameTester.test(
'layers correctly before being loaded',
(game) async {
const expectedLayer = Layer.jetpack;
final component = TestBodyComponent()..layer = expectedLayer;
await game.ensureAdd(component);
// TODO(alestiago): modify once component.loaded is available.
await component.mounted;
_expectLayerOnFixtures(
fixtures: component.body.fixtures,
layer: expectedLayer,
);
},
);
flameTester.test(
'layers correctly before being loaded '
'when multiple different sets',
(game) async {
const expectedLayer = Layer.launcher;
final component = TestBodyComponent()..layer = Layer.jetpack;
expect(component.layer, isNot(equals(expectedLayer)));
component.layer = expectedLayer;
await game.ensureAdd(component);
// TODO(alestiago): modify once component.loaded is available.
await component.mounted;
_expectLayerOnFixtures(
fixtures: component.body.fixtures,
layer: expectedLayer,
);
},
);
flameTester.test(
'layers correctly after being loaded',
(game) async {
const expectedLayer = Layer.jetpack;
final component = TestBodyComponent();
await game.ensureAdd(component);
component.layer = expectedLayer;
_expectLayerOnFixtures(
fixtures: component.body.fixtures,
layer: expectedLayer,
);
},
);
flameTester.test(
'layers correctly after being loaded '
'when multiple different sets',
(game) async {
const expectedLayer = Layer.launcher;
final component = TestBodyComponent();
await game.ensureAdd(component);
component.layer = Layer.jetpack;
expect(component.layer, isNot(equals(expectedLayer)));
component.layer = expectedLayer;
_expectLayerOnFixtures(
fixtures: component.body.fixtures,
layer: expectedLayer,
);
},
);
flameTester.test(
'defaults to Layer.all '
'when no layer is given',
(game) async {
final component = TestBodyComponent();
await game.ensureAdd(component);
expect(component.layer, equals(Layer.all));
},
);
});
group('LayerMaskBits', () {
test('all types are different', () {
for (final layer in Layer.values) {
for (final otherLayer in Layer.values) {
if (layer != otherLayer) {
expect(layer.maskBits, isNot(equals(otherLayer.maskBits)));
}
}
}
});
test('all maskBits are smaller than 2^16 ', () {
final maxMaskBitSize = math.pow(2, 16);
for (final layer in Layer.values) {
expect(layer.maskBits, isNot(greaterThan(maxMaskBitSize)));
}
});
});
}

@ -14,73 +14,73 @@ void main() {
const width = 50.0; const width = 50.0;
group('straight', () { group('straight', () {
group('color', () {
flameTester.test( flameTester.test(
'has transparent color by default when no color is specified', 'loads correctly',
(game) async { (game) async {
await game.ready();
final pathway = Pathway.straight( final pathway = Pathway.straight(
start: Vector2(10, 10), start: Vector2(10, 10),
end: Vector2(20, 20), end: Vector2(20, 20),
width: width, width: width,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue); expect(game.contains(pathway), isTrue);
expect(pathway.paint, isNotNull);
expect(
pathway.paint.color,
equals(Color.fromARGB(0, 0, 0, 0)),
);
}, },
); );
group('color', () {
flameTester.test( flameTester.test(
'has a color when is specified', 'has transparent color by default when no color is specified',
(game) async { (game) async {
await game.ready();
const defaultColor = Colors.blue;
final pathway = Pathway.straight( final pathway = Pathway.straight(
color: defaultColor,
start: Vector2(10, 10), start: Vector2(10, 10),
end: Vector2(20, 20), end: Vector2(20, 20),
width: width, width: width,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue); expect(game.contains(pathway), isTrue);
expect(pathway.paint, isNotNull); expect(pathway.paint, isNotNull);
expect(pathway.paint.color.value, equals(defaultColor.value)); expect(
pathway.paint.color,
equals(Color.fromARGB(0, 0, 0, 0)),
);
}, },
); );
});
flameTester.test( flameTester.test(
'loads correctly', 'has a color when is specified',
(game) async { (game) async {
await game.ready(); const defaultColor = Colors.blue;
final pathway = Pathway.straight( final pathway = Pathway.straight(
color: defaultColor,
start: Vector2(10, 10), start: Vector2(10, 10),
end: Vector2(20, 20), end: Vector2(20, 20),
width: width, width: width,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue); expect(game.contains(pathway), isTrue);
expect(pathway.paint, isNotNull);
expect(pathway.paint.color.value, equals(defaultColor.value));
}, },
); );
});
group('body', () { group('body', () {
flameTester.test( flameTester.test(
'is static', 'is static',
(game) async { (game) async {
await game.ready();
final pathway = Pathway.straight( final pathway = Pathway.straight(
start: Vector2(10, 10), start: Vector2(10, 10),
end: Vector2(20, 20), end: Vector2(20, 20),
width: width, width: width,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static)); expect(pathway.body.bodyType, equals(BodyType.static));
@ -92,13 +92,13 @@ void main() {
flameTester.test( flameTester.test(
'has only one ChainShape when singleWall is true', 'has only one ChainShape when singleWall is true',
(game) async { (game) async {
await game.ready();
final pathway = Pathway.straight( final pathway = Pathway.straight(
start: Vector2(10, 10), start: Vector2(10, 10),
end: Vector2(20, 20), end: Vector2(20, 20),
width: width, width: width,
singleWall: true, singleWall: true,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(pathway.body.fixtures.length, 1); expect(pathway.body.fixtures.length, 1);
@ -111,12 +111,12 @@ void main() {
flameTester.test( flameTester.test(
'has two ChainShape when singleWall is false (default)', 'has two ChainShape when singleWall is false (default)',
(game) async { (game) async {
await game.ready();
final pathway = Pathway.straight( final pathway = Pathway.straight(
start: Vector2(10, 10), start: Vector2(10, 10),
end: Vector2(20, 20), end: Vector2(20, 20),
width: width, width: width,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(pathway.body.fixtures.length, 2); expect(pathway.body.fixtures.length, 2);
@ -133,13 +133,13 @@ void main() {
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
await game.ready();
final pathway = Pathway.arc( final pathway = Pathway.arc(
center: Vector2.zero(), center: Vector2.zero(),
width: width, width: width,
radius: 100, radius: 100,
angle: math.pi / 2, angle: math.pi / 2,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue); expect(game.contains(pathway), isTrue);
@ -150,13 +150,13 @@ void main() {
flameTester.test( flameTester.test(
'is static', 'is static',
(game) async { (game) async {
await game.ready();
final pathway = Pathway.arc( final pathway = Pathway.arc(
center: Vector2.zero(), center: Vector2.zero(),
width: width, width: width,
radius: 100, radius: 100,
angle: math.pi / 2, angle: math.pi / 2,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static)); expect(pathway.body.bodyType, equals(BodyType.static));
@ -176,11 +176,11 @@ void main() {
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
await game.ready();
final pathway = Pathway.bezierCurve( final pathway = Pathway.bezierCurve(
controlPoints: controlPoints, controlPoints: controlPoints,
width: width, width: width,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue); expect(game.contains(pathway), isTrue);
@ -191,11 +191,11 @@ void main() {
flameTester.test( flameTester.test(
'is static', 'is static',
(game) async { (game) async {
await game.ready();
final pathway = Pathway.bezierCurve( final pathway = Pathway.bezierCurve(
controlPoints: controlPoints, controlPoints: controlPoints,
width: width, width: width,
); );
await game.ready();
await game.ensureAdd(pathway); await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static)); expect(pathway.body.bodyType, equals(BodyType.static));

@ -0,0 +1,249 @@
// 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:mockingjay/mockingjay.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
class TestRampOpening extends RampOpening {
TestRampOpening({
required RampOrientation orientation,
required Layer pathwayLayer,
}) : super(
pathwayLayer: pathwayLayer,
orientation: orientation,
);
@override
Shape get shape => PolygonShape()
..set([
Vector2(0, 0),
Vector2(0, 1),
Vector2(1, 1),
Vector2(1, 0),
]);
}
class TestRampOpeningBallContactCallback
extends RampOpeningBallContactCallback<TestRampOpening> {
TestRampOpeningBallContactCallback() : super();
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('RampOpening', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test(
'loads correctly',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
pathwayLayer: Layer.jetpack,
);
await game.ready();
await game.ensureAdd(ramp);
expect(game.contains(ramp), isTrue);
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
pathwayLayer: Layer.jetpack,
);
await game.ensureAdd(ramp);
expect(ramp.body.bodyType, equals(BodyType.static));
},
);
group('first fixture', () {
const pathwayLayer = Layer.jetpack;
const openingLayer = Layer.opening;
flameTester.test(
'exists',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
pathwayLayer: pathwayLayer,
)..layer = openingLayer;
await game.ensureAdd(ramp);
expect(ramp.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'shape is a polygon',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
pathwayLayer: pathwayLayer,
)..layer = openingLayer;
await game.ensureAdd(ramp);
final fixture = ramp.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.polygon));
},
);
flameTester.test(
'is sensor',
(game) async {
final ramp = TestRampOpening(
orientation: RampOrientation.down,
pathwayLayer: pathwayLayer,
)..layer = openingLayer;
await game.ensureAdd(ramp);
final fixture = ramp.body.fixtures[0];
expect(fixture.isSensor, isTrue);
},
);
});
});
});
group('RampOpeningBallContactCallback', () {
flameTester.test(
'changes ball layer '
'when a ball enters upwards into a downward ramp opening',
(game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.down,
pathwayLayer: Layer.jetpack,
);
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => body.position).thenReturn(Vector2.zero());
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
});
flameTester.test(
'changes ball layer '
'when a ball enters downwards into a upward ramp opening',
(game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.up,
pathwayLayer: Layer.jetpack,
);
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => body.position).thenReturn(Vector2.zero());
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
});
flameTester.test(
'changes ball layer '
'when a ball exits from a downward oriented ramp', (game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.down,
pathwayLayer: Layer.jetpack,
)..initialPosition = Vector2(0, 10);
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => body.position).thenReturn(Vector2.zero());
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board);
});
flameTester.test(
'changes ball layer '
'when a ball exits from a upward oriented ramp', (game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.up,
pathwayLayer: Layer.jetpack,
)..initialPosition = Vector2(0, 10);
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => body.position).thenReturn(Vector2.zero());
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board);
});
flameTester.test(
'change ball layer from pathwayLayer to Layer.board '
'when a ball enters and exits from ramp', (game) async {
final ball = MockBall();
final body = MockBody();
final area = TestRampOpening(
orientation: RampOrientation.down,
pathwayLayer: Layer.jetpack,
)..initialPosition = Vector2(0, 10);
final callback = TestRampOpeningBallContactCallback();
when(() => ball.body).thenReturn(body);
when(() => body.position).thenReturn(Vector2.zero());
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
when(() => ball.layer).thenReturn(Layer.board);
await game.ready();
await game.ensureAdd(area);
callback.begin(ball, area, MockContact());
verify(() => ball.layer = area.pathwayLayer).called(1);
callback.end(ball, area, MockContact());
verifyNever(() => ball.layer = Layer.board);
callback.begin(ball, area, MockContact());
verifyNever(() => ball.layer = area.pathwayLayer);
callback.end(ball, area, MockContact());
verify(() => ball.layer = Layer.board);
});
});
}

@ -63,6 +63,32 @@ 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 { debugModeFlameTester.test('adds a ball on tap up', (game) async {
await game.ready(); await game.ready();

@ -12,10 +12,17 @@ class MockWall extends Mock implements Wall {}
class MockBottomWall extends Mock implements BottomWall {} class MockBottomWall extends Mock implements BottomWall {}
class MockBody extends Mock implements Body {}
class MockBall extends Mock implements Ball {} class MockBall extends Mock implements Ball {}
class MockContact extends Mock implements Contact {} class MockContact extends Mock implements Contact {}
class MockRampOpening extends Mock implements RampOpening {}
class MockRampOpeningBallContactCallback extends Mock
implements RampOpeningBallContactCallback {}
class MockGameBloc extends Mock implements GameBloc {} class MockGameBloc extends Mock implements GameBloc {}
class MockGameState extends Mock implements GameState {} class MockGameState extends Mock implements GameState {}

Loading…
Cancel
Save