Merge branch 'main' into feat/add-user-at-end-game

pull/98/head
RuiAlonso 4 years ago
commit 8305635677

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

@ -216,7 +216,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
}
/// {@template flipper_anchor}
/// [Anchor] positioned at the end of a [Flipper].
/// [JointAnchor] positioned at the end of a [Flipper].
///
/// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate}
@ -235,7 +235,7 @@ class FlipperAnchor extends JointAnchor {
}
/// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [Anchor] to achieve an arc motion.
/// Hinges one end of [Flipper] to a [FlipperAnchor] to achieve an arc motion.
/// {@endtemplate}
class FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def}

@ -1,7 +1,9 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:geometry/geometry.dart' show centroid;
import 'package:geometry/geometry.dart' as geometry show centroid;
import 'package:pinball/game/game.dart';
/// {@template sling_shot}
@ -31,54 +33,88 @@ class SlingShot extends BodyComponent with InitialPosition {
/// The size of the [SlingShot] body.
// TODO(alestiago): Use size from PositionedBodyComponent instead,
// once a sprite is given.
static final Vector2 size = Vector2(6, 8);
static final Vector2 size = Vector2(4, 10);
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
// TODO(alestiago): This magic number can be deduced by specifying the
// angle and using polar coordinate system to place the bottom right
// vertex.
// Something as: y = -size.y * math.cos(angle)
const additionalIncrement = 3;
final triangleVertices = _side.isLeft
? [
Vector2(0, 0),
Vector2(0, -size.y),
Vector2(
size.x,
-size.y - additionalIncrement,
),
]
: [
Vector2(size.x, 0),
Vector2(size.x, -size.y),
final fixturesDefs = <FixtureDef>[];
final direction = _side.direction;
const quarterPi = math.pi / 4;
final upperCircle = CircleShape()..radius = 1.45;
upperCircle.position.setValues(0, -upperCircle.radius / 2);
final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0;
fixturesDefs.add(upperCircleFixtureDef);
final lowerCircle = CircleShape()..radius = 1.45;
lowerCircle.position.setValues(
size.x * -direction,
-size.y,
);
final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0;
fixturesDefs.add(lowerCircleFixtureDef);
final wallFacingEdge = EdgeShape()
..set(
upperCircle.position +
Vector2(
upperCircle.radius * direction,
0,
-size.y - additionalIncrement,
),
];
final triangleCentroid = centroid(triangleVertices);
for (final vertex in triangleVertices) {
vertex.setFrom(vertex - triangleCentroid);
}
// TODO(alestiago): Use values from design.
Vector2(2.0 * direction, -size.y + 2),
);
final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0;
fixturesDefs.add(wallFacingLineFixtureDef);
final triangle = PolygonShape()..set(triangleVertices);
final triangleFixtureDef = FixtureDef(triangle)..friction = 0;
fixturesDef.add(triangleFixtureDef);
final bottomEdge = EdgeShape()
..set(
wallFacingEdge.vertex2,
lowerCircle.position +
Vector2(
lowerCircle.radius * math.cos(quarterPi) * direction,
-lowerCircle.radius * math.sin(quarterPi),
),
);
final bottomLineFixtureDef = FixtureDef(bottomEdge)..friction = 0;
fixturesDefs.add(bottomLineFixtureDef);
final kicker = EdgeShape()
final kickerEdge = EdgeShape()
..set(
triangleVertices.first,
triangleVertices.last,
upperCircle.position +
Vector2(
upperCircle.radius * math.cos(quarterPi) * -direction,
upperCircle.radius * math.sin(quarterPi),
),
lowerCircle.position +
Vector2(
lowerCircle.radius * math.cos(quarterPi) * -direction,
lowerCircle.radius * math.sin(quarterPi),
),
);
// TODO(alestiago): Play with restitution value once game is bundled.
final kickerFixtureDef = FixtureDef(kicker)
final kickerFixtureDef = FixtureDef(kickerEdge)
// TODO(alestiago): Play with restitution value once game is bundled.
..restitution = 10.0
..friction = 0;
fixturesDef.add(kickerFixtureDef);
fixturesDefs.add(kickerFixtureDef);
return fixturesDef;
// TODO(alestiago): Evaluate if there is value on centering the fixtures.
final centroid = geometry.centroid(
[
upperCircle.position + Vector2(0, -upperCircle.radius),
lowerCircle.position +
Vector2(
lowerCircle.radius * math.cos(quarterPi) * -direction,
-lowerCircle.radius * math.sin(quarterPi),
),
wallFacingEdge.vertex2,
],
);
for (final fixtureDef in fixturesDefs) {
fixtureDef.shape.moveBy(-centroid);
}
return fixturesDefs;
}
@override
@ -90,3 +126,17 @@ class SlingShot extends BodyComponent with InitialPosition {
return body;
}
}
// TODO(alestiago): Evaluate if there's value on generalising this to
// all shapes.
extension on Shape {
void moveBy(Vector2 offset) {
if (this is CircleShape) {
final circle = this as CircleShape;
circle.position.setFrom(circle.position + offset);
} else if (this is EdgeShape) {
final edge = this as EdgeShape;
edge.set(edge.vertex1 + offset, edge.vertex2 + offset);
}
}
}

@ -5,12 +5,38 @@ import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
// TODO(erickzanardo): change this to use the layer class
// that will be introduced on the path PR
const _spaceShipBits = 0x0002;
const _spaceShipSize = 20.0;
/// A [Blueprint] which creates the spaceship feature.
class Spaceship extends Forge2DBlueprint {
/// Total size of the spaceship
static const radius = 10.0;
@override
void build() {
final position = Vector2(20, -24);
addAllContactCallback([
SpaceshipHoleBallContactCallback(),
SpaceshipEntranceBallContactCallback(),
]);
addAll([
SpaceshipSaucer()..initialPosition = position,
SpaceshipEntrance()..initialPosition = position,
SpaceshipBridge()..initialPosition = position,
SpaceshipBridgeTop()..initialPosition = position + Vector2(0, 5.5),
SpaceshipHole()..initialPosition = position - Vector2(5, 4),
SpaceshipHole()..initialPosition = position - Vector2(-5, 4),
SpaceshipWall()..initialPosition = position,
]);
}
}
/// {@template spaceship_saucer}
/// A [BodyComponent] for the base, or the saucer of the spaceship
@ -36,7 +62,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition {
await add(
SpriteComponent(
sprite: sprites.first,
size: Vector2.all(_spaceShipSize),
size: Vector2.all(Spaceship.radius * 2),
anchor: Anchor.center,
),
);
@ -44,9 +70,9 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition {
await add(
SpriteComponent(
sprite: sprites.last,
size: Vector2(_spaceShipSize + 0.5, _spaceShipSize / 2),
size: Vector2((Spaceship.radius * 2) + 0.5, Spaceship.radius),
anchor: Anchor.center,
position: Vector2(0, -(_spaceShipSize / 3.5)),
position: Vector2(0, -((Spaceship.radius * 2) / 3.5)),
),
);
@ -55,7 +81,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition {
@override
Body createBody() {
final circleShape = CircleShape()..radius = _spaceShipSize / 2;
final circleShape = CircleShape()..radius = Spaceship.radius;
final bodyDef = BodyDef()
..userData = this
@ -92,7 +118,7 @@ class SpaceshipBridgeTop extends BodyComponent with InitialPosition {
SpriteComponent(
sprite: sprite,
anchor: Anchor.center,
size: Vector2(_spaceShipSize / 2.5 - 1, _spaceShipSize / 5),
size: Vector2((Spaceship.radius * 2) / 2.5 - 1, Spaceship.radius / 2.5),
),
);
}
@ -134,7 +160,7 @@ class SpaceshipBridge extends BodyComponent with InitialPosition {
stepTime: 0.2,
textureSize: Vector2(160, 114),
),
size: Vector2.all(_spaceShipSize / 2.5),
size: Vector2.all((Spaceship.radius * 2) / 2.5),
anchor: Anchor.center,
),
);
@ -142,7 +168,7 @@ class SpaceshipBridge extends BodyComponent with InitialPosition {
@override
Body createBody() {
final circleShape = CircleShape()..radius = _spaceShipSize / 5;
final circleShape = CircleShape()..radius = Spaceship.radius / 2.5;
final bodyDef = BodyDef()
..userData = this
@ -171,16 +197,15 @@ class SpaceshipEntrance extends BodyComponent with InitialPosition {
@override
Body createBody() {
const radius = _spaceShipSize / 2;
final entranceShape = PolygonShape()
..setAsEdge(
Vector2(
radius * cos(20 * pi / 180),
radius * sin(20 * pi / 180),
Spaceship.radius * cos(20 * pi / 180),
Spaceship.radius * sin(20 * pi / 180),
),
Vector2(
radius * cos(340 * pi / 180),
radius * sin(340 * pi / 180),
Spaceship.radius * cos(340 * pi / 180),
Spaceship.radius * sin(340 * pi / 180),
),
);
@ -208,7 +233,7 @@ class SpaceshipHole extends BodyComponent with InitialPosition {
@override
Body createBody() {
renderBody = false;
final circleShape = CircleShape()..radius = _spaceShipSize / 80;
final circleShape = CircleShape()..radius = Spaceship.radius / 40;
final bodyDef = BodyDef()
..userData = this
@ -247,9 +272,9 @@ class SpaceshipWall extends BodyComponent with InitialPosition {
await add(
SpriteComponent(
sprite: sprite,
size: Vector2(_spaceShipSize, (_spaceShipSize / 2) + 1),
size: Vector2(Spaceship.radius * 2, Spaceship.radius + 1),
anchor: Anchor.center,
position: Vector2(-_spaceShipSize / 4, 0),
position: Vector2(-Spaceship.radius / 2, 0),
angle: 90 * pi / 180,
),
);
@ -259,15 +284,13 @@ class SpaceshipWall extends BodyComponent with InitialPosition {
Body createBody() {
renderBody = false;
const radius = _spaceShipSize / 2;
final wallShape = ChainShape()
..createChain(
[
for (var angle = 20; angle <= 340; angle++)
Vector2(
radius * cos(angle * pi / 180),
radius * sin(angle * pi / 180),
Spaceship.radius * cos(angle * pi / 180),
Spaceship.radius * sin(angle * pi / 180),
),
],
);

@ -4,6 +4,7 @@ import 'dart:async';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart';
@ -29,7 +30,7 @@ class PinballGame extends Forge2DGame
unawaited(_addPlunger());
unawaited(_addPaths());
unawaited(_addSpaceship());
unawaited(addFromBlueprint(Spaceship()));
// Corner wall above plunger so the ball deflects into the rest of the
// board.
@ -80,21 +81,6 @@ class PinballGame extends Forge2DGame
);
}
Future<void> _addSpaceship() async {
final position = Vector2(20, -24);
await addAll(
[
SpaceshipSaucer()..initialPosition = position,
SpaceshipEntrance()..initialPosition = position,
SpaceshipBridge()..initialPosition = position,
SpaceshipBridgeTop()..initialPosition = position + Vector2(0, 5.5),
SpaceshipHole()..initialPosition = position - Vector2(5, 4),
SpaceshipHole()..initialPosition = position - Vector2(-5, 4),
SpaceshipWall()..initialPosition = position,
],
);
}
void spawnBall() {
final ball = Ball();
add(
@ -107,8 +93,6 @@ class PinballGame extends Forge2DGame
addContactCallback(BallScorePointsCallback());
addContactCallback(BottomWallBallContactCallback());
addContactCallback(BonusLetterBallContactCallback());
addContactCallback(SpaceshipHoleBallContactCallback());
addContactCallback(SpaceshipEntranceBallContactCallback());
}
Future<void> _addGameBoundaries() async {

@ -0,0 +1,103 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
import '../helpers/helpers.dart';
class MyBlueprint extends Blueprint {
@override
void build() {
add(Component());
addAll([Component(), Component()]);
}
}
class MyForge2dBlueprint extends Forge2DBlueprint {
@override
void build() {
addContactCallback(MockContactCallback());
addAllContactCallback([MockContactCallback(), MockContactCallback()]);
}
}
void main() {
group('Blueprint', () {
test('components can be added to it', () {
final blueprint = MyBlueprint()..build();
expect(blueprint.components.length, equals(3));
});
test('adds the components to a game on attach', () {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
MyBlueprint().attach(mockGame);
verify(() => mockGame.addAll(any())).called(1);
});
test(
'throws assertion error when adding to an already attached blueprint',
() async {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
final blueprint = MyBlueprint();
await blueprint.attach(mockGame);
expect(() => blueprint.add(Component()), throwsAssertionError);
expect(() => blueprint.addAll([Component()]), throwsAssertionError);
},
);
});
group('Forge2DBlueprint', () {
setUpAll(() {
registerFallbackValue(SpaceshipHoleBallContactCallback());
});
test('callbacks can be added to it', () {
final blueprint = MyForge2dBlueprint()..build();
expect(blueprint.callbacks.length, equals(3));
});
test('adds the callbacks to a game on attach', () async {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
await MyForge2dBlueprint().attach(mockGame);
verify(() => mockGame.addContactCallback(any())).called(3);
});
test(
'throws assertion error when adding to an already attached blueprint',
() async {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
final blueprint = MyForge2dBlueprint();
await blueprint.attach(mockGame);
expect(
() => blueprint.addContactCallback(MockContactCallback()),
throwsAssertionError,
);
expect(
() => blueprint.addAllContactCallback([MockContactCallback()]),
throwsAssertionError,
);
},
);
test('throws assertion error when used on a non Forge2dGame', () {
expect(
() => MyForge2dBlueprint().attach(FlameGame()),
throwsAssertionError,
);
});
});
}

@ -75,8 +75,9 @@ void main() {
'has Layer.all as default filter maskBits',
(game) async {
final ball = Ball();
await game.ready();
await game.ensureAdd(ball);
await ball.mounted;
await game.ready();
final fixture = ball.body.fixtures[0];
expect(fixture.filterData.maskBits, equals(Layer.board.maskBits));

@ -7,6 +7,7 @@ import 'package:pinball/game/game.dart';
void main() {
group('SlingShot', () {
// TODO(alestiago): Include golden tests for left and right.
final flameTester = FlameTester(Forge2DGame.new);
flameTester.test(
@ -21,135 +22,48 @@ void main() {
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
expect(slingShot.body.bodyType, equals(BodyType.static));
},
);
});
group('first fixture', () {
flameTester.test(
'exists',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
expect(slingShot.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'shape is triangular',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
final fixture = slingShot.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.polygon));
expect((fixture.shape as PolygonShape).vertices.length, equals(3));
},
);
flameTester.test(
'triangular shapes are different '
'when side is left or right',
(game) async {
final leftSlingShot = SlingShot(
side: BoardSide.left,
);
final rightSlingShot = SlingShot(
side: BoardSide.right,
);
await game.ensureAdd(leftSlingShot);
await game.ensureAdd(rightSlingShot);
final rightShape =
rightSlingShot.body.fixtures[0].shape as PolygonShape;
final leftShape =
leftSlingShot.body.fixtures[0].shape as PolygonShape;
expect(rightShape.vertices, isNot(equals(leftShape.vertices)));
},
);
flameTester.test(
'has no friction',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
final fixture = slingShot.body.fixtures[0];
expect(fixture.friction, equals(0));
},
);
});
group('second fixture', () {
flameTester.test(
'exists',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
expect(slingShot.body.fixtures[1], isA<Fixture>());
},
);
flameTester.test(
'shape is edge',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
flameTester.test(
'body is static',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
final fixture = slingShot.body.fixtures[1];
expect(fixture.shape.shapeType, equals(ShapeType.edge));
},
);
expect(slingShot.body.bodyType, equals(BodyType.static));
},
);
flameTester.test(
'has restitution',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
flameTester.test(
'has restitution',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
final fixture = slingShot.body.fixtures[1];
expect(fixture.restitution, greaterThan(0));
},
);
final totalRestitution = slingShot.body.fixtures.fold<double>(
0,
(total, fixture) => total + fixture.restitution,
);
expect(totalRestitution, greaterThan(0));
},
);
flameTester.test(
'has no friction',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
flameTester.test(
'has no friction',
(game) async {
final slingShot = SlingShot(
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
final fixture = slingShot.body.fixtures[1];
expect(fixture.friction, equals(0));
},
);
});
final totalFriction = slingShot.body.fixtures.fold<double>(
0,
(total, fixture) => total + fixture.friction,
);
expect(totalFriction, equals(0));
},
);
});
}

@ -18,6 +18,9 @@ class MockBall extends Mock implements Ball {}
class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock
implements ContactCallback<Object, Object> {}
class MockRampOpening extends Mock implements RampOpening {}
class MockRampOpeningBallContactCallback extends Mock

Loading…
Cancel
Save