Merge branch 'main' into feat/leaderboard_screen

pull/51/head
RuiAlonso 4 years ago
commit 02dd698336

@ -8,27 +8,11 @@ import 'package:pinball/game/game.dart';
/// {@endtemplate} /// {@endtemplate}
class Baseboard extends BodyComponent { class Baseboard extends BodyComponent {
/// {@macro baseboard} /// {@macro baseboard}
Baseboard._({ Baseboard({
required Vector2 position,
required BoardSide side, required BoardSide side,
}) : _position = position,
_side = side;
/// A left positioned [Baseboard].
Baseboard.left({
required Vector2 position,
}) : this._(
position: position,
side: BoardSide.left,
);
/// A right positioned [Baseboard].
Baseboard.right({
required Vector2 position, required Vector2 position,
}) : this._( }) : _side = side,
position: position, _position = position;
side: BoardSide.right,
);
/// The width of the [Baseboard]. /// The width of the [Baseboard].
static const width = 10.0; static const width = 10.0;

@ -0,0 +1,83 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
/// {@template bottom_group}
/// Grouping of the board's bottom [Component]s.
///
/// The bottom [Component]s are the [Flipper]s and the [Baseboard]s.
/// {@endtemplate}
// TODO(alestiago): Consider renaming once entire Board is defined.
class BottomGroup extends Component {
/// {@macro bottom_group}
BottomGroup({
required this.position,
required this.spacing,
});
/// The amount of space between the line of symmetry.
final double spacing;
/// The position of this [BottomGroup].
final Vector2 position;
@override
Future<void> onLoad() async {
final spacing = this.spacing + Flipper.width / 2;
final rightSide = _BottomGroupSide(
side: BoardSide.right,
position: position + Vector2(spacing, 0),
);
final leftSide = _BottomGroupSide(
side: BoardSide.left,
position: position + Vector2(-spacing, 0),
);
await addAll([rightSide, leftSide]);
}
}
/// {@template bottom_group_side}
/// Group with one side of [BottomGroup]'s symmetric [Component]s.
///
/// For example, [Flipper]s are symmetric components.
/// {@endtemplate}
class _BottomGroupSide extends Component {
/// {@macro bottom_group_side}
_BottomGroupSide({
required BoardSide side,
required Vector2 position,
}) : _side = side,
_position = position;
final BoardSide _side;
final Vector2 _position;
@override
Future<void> onLoad() async {
final direction = _side.direction;
final flipper = Flipper.fromSide(
side: _side,
position: _position,
);
final baseboard = Baseboard(
side: _side,
position: _position +
Vector2(
(Flipper.width * direction) - direction,
Flipper.height,
),
);
final slingShot = SlingShot(
side: _side,
position: _position +
Vector2(
(Flipper.width) * direction,
Flipper.height + SlingShot.size.y,
),
);
await addAll([flipper, baseboard, slingShot]);
}
}

@ -3,7 +3,7 @@ import 'package:pinball/game/game.dart';
/// Indicates a side of the board. /// Indicates a side of the board.
/// ///
/// Usually used to position or mirror elements of a [PinballGame]; such as a /// Usually used to position or mirror elements of a [PinballGame]; such as a
/// [Flipper]. /// [Flipper] or [SlingShot].
enum BoardSide { enum BoardSide {
/// The left side of the board. /// The left side of the board.
left, left,
@ -19,4 +19,9 @@ extension BoardSideX on BoardSide {
/// Whether this side is [BoardSide.right]. /// Whether this side is [BoardSide.right].
bool get isRight => this == BoardSide.right; bool get isRight => this == BoardSide.right;
/// Direction of the [BoardSide].
///
/// Represents the path which the [BoardSide] moves along.
int get direction => isLeft ? -1 : 1;
} }

@ -1,10 +1,14 @@
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board.dart';
export 'board_side.dart'; export 'board_side.dart';
export 'bonus_word.dart'; export 'bonus_word.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'initial_position.dart';
export 'joint_anchor.dart'; export 'joint_anchor.dart';
export 'pathway.dart'; export 'pathway.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'round_bumper.dart';
export 'score_points.dart'; export 'score_points.dart';
export 'sling_shot.dart';
export 'wall.dart'; export 'wall.dart';

@ -8,42 +8,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
/// {@template flipper_group}
/// Loads a [Flipper.right] and a [Flipper.left].
/// {@endtemplate}
class FlipperGroup extends Component {
/// {@macro flipper_group}
FlipperGroup({
required this.position,
required this.spacing,
});
/// The amount of space between the [Flipper.right] and [Flipper.left].
final double spacing;
/// The position of this [FlipperGroup]
final Vector2 position;
@override
Future<void> onLoad() async {
final leftFlipper = Flipper.left(
position: Vector2(
position.x - (Flipper.width / 2) - (spacing / 2),
position.y,
),
);
await add(leftFlipper);
final rightFlipper = Flipper.right(
position: Vector2(
position.x + (Flipper.width / 2) + (spacing / 2),
position.y,
),
);
await add(rightFlipper);
}
}
/// {@template flipper} /// {@template flipper}
/// A bat, typically found in pairs at the bottom of the board. /// A bat, typically found in pairs at the bottom of the board.
/// ///
@ -58,8 +22,7 @@ class Flipper extends BodyComponent with KeyboardHandler {
}) : _position = position, }) : _position = position,
_keys = keys; _keys = keys;
/// A left positioned [Flipper]. Flipper._left({
Flipper.left({
required Vector2 position, required Vector2 position,
}) : this._( }) : this._(
position: position, position: position,
@ -70,8 +33,7 @@ class Flipper extends BodyComponent with KeyboardHandler {
], ],
); );
/// A right positioned [Flipper]. Flipper._right({
Flipper.right({
required Vector2 position, required Vector2 position,
}) : this._( }) : this._(
position: position, position: position,
@ -82,6 +44,22 @@ class Flipper extends BodyComponent with KeyboardHandler {
], ],
); );
/// Constructs a [Flipper] from a [BoardSide].
///
/// A [Flipper._right] and [Flipper._left] besides being mirrored
/// horizontally, also have different [LogicalKeyboardKey]s that control them.
factory Flipper.fromSide({
required BoardSide side,
required Vector2 position,
}) {
switch (side) {
case BoardSide.left:
return Flipper._left(position: position);
case BoardSide.right:
return Flipper._right(position: position);
}
}
/// Asset location of the sprite that renders with the [Flipper]. /// Asset location of the sprite that renders with the [Flipper].
/// ///
/// Sprite is preloaded by [PinballGameAssetsX]. /// Sprite is preloaded by [PinballGameAssetsX].

@ -0,0 +1,17 @@
import 'package:flame_forge2d/flame_forge2d.dart';
/// Forces a given [BodyComponent] to position their [body] to an
/// [initialPosition].
mixin InitialPosition<T extends Forge2DGame> on BodyComponent<T> {
/// The initial position of the [body].
late final Vector2 initialPosition;
@override
void onMount() {
super.onMount();
assert(
body.position == initialPosition,
'Body position is not equal to initial position.',
);
}
}

@ -0,0 +1,41 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
/// {@template round_bumper}
/// Circular body that repels a [Ball] on contact, increasing the score.
/// {@endtemplate}
class RoundBumper extends BodyComponent with ScorePoints {
/// {@macro round_bumper}
RoundBumper({
required Vector2 position,
required double radius,
required int points,
}) : _position = position,
_radius = radius,
_points = points;
/// The position of the [RoundBumper] body.
final Vector2 _position;
/// The radius of the [RoundBumper].
final double _radius;
/// Points awarded from hitting this [RoundBumper].
final int _points;
@override
int get points => _points;
@override
Body createBody() {
final shape = CircleShape()..radius = _radius;
final fixtureDef = FixtureDef(shape)..restitution = 1;
final bodyDef = BodyDef()
..position = _position
..type = BodyType.static;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -0,0 +1,97 @@
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:pinball/game/game.dart';
/// {@template sling_shot}
/// Triangular [BodyType.static] body that propels the [Ball] towards the
/// opposite side.
///
/// [SlingShot]s are usually positioned above each [Flipper].
/// {@endtemplate sling_shot}
class SlingShot extends BodyComponent {
/// {@macro sling_shot}
SlingShot({
required Vector2 position,
required BoardSide side,
}) : _position = position,
_side = side {
// TODO(alestiago): Use sprite instead of color when provided.
paint = Paint()
..color = const Color(0xFF00FF00)
..style = PaintingStyle.fill;
}
/// The initial position of the [SlingShot] body.
final Vector2 _position;
/// Whether the [SlingShot] is on the left or right side of the board.
///
/// A [SlingShot] with [BoardSide.left] propels the [Ball] to the right,
/// whereas a [SlingShot] with [BoardSide.right] propels the [Ball] to the
/// left.
final BoardSide _side;
/// 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);
List<FixtureDef> _createFixtureDefs() {
final fixtures = <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),
Vector2(
0,
-size.y - additionalIncrement,
),
];
final triangleCentroid = centroid(triangleVertices);
for (final vertex in triangleVertices) {
vertex.setFrom(vertex - triangleCentroid);
}
final triangle = PolygonShape()..set(triangleVertices);
final triangleFixtureDef = FixtureDef(triangle)..friction = 0;
fixtures.add(triangleFixtureDef);
final kicker = EdgeShape()
..set(
triangleVertices.first,
triangleVertices.last,
);
// TODO(alestiago): Play with restitution value once game is bundled.
final kickerFixtureDef = FixtureDef(kicker)
..restitution = 10.0
..friction = 0;
fixtures.add(kickerFixtureDef);
return fixtures;
}
@override
Body createBody() {
final bodyDef = BodyDef()..position = _position;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
}

@ -26,7 +26,7 @@ class Wall extends BodyComponent {
final fixtureDef = FixtureDef(shape) final fixtureDef = FixtureDef(shape)
..restitution = 0.1 ..restitution = 0.1
..friction = 0.3; ..friction = 0;
final bodyDef = BodyDef() final bodyDef = BodyDef()
..userData = this ..userData = this

@ -48,41 +48,33 @@ class PinballGame extends Forge2DGame
), ),
); );
unawaited(_addFlippers());
unawaited(_addBonusWord()); unawaited(_addBonusWord());
} unawaited(
add(
Future<void> _addBonusWord() async { BottomGroup(
await add(
BonusWord(
position: screenToWorld( position: screenToWorld(
Vector2( Vector2(
camera.viewport.effectiveSize.x / 2, camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y - 50, camera.viewport.effectiveSize.y / 1.25,
), ),
), ),
spacing: 2,
),
), ),
); );
} }
Future<void> _addFlippers() async { Future<void> _addBonusWord() async {
final flippersPosition = screenToWorld( await add(
BonusWord(
position: screenToWorld(
Vector2( Vector2(
camera.viewport.effectiveSize.x / 2, camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y / 1.1, camera.viewport.effectiveSize.y - 50,
), ),
);
unawaited(
add(
FlipperGroup(
position: flippersPosition,
spacing: 2,
), ),
), ),
); );
unawaited(_addBaseboards());
} }
void spawnBall() { void spawnBall() {
@ -115,31 +107,6 @@ class PinballGame extends Forge2DGame
), ),
); );
} }
Future<void> _addBaseboards() async {
final spaceBetweenBaseboards = camera.viewport.effectiveSize.x / 2;
final baseboardY = camera.viewport.effectiveSize.y / 1.12;
final leftBaseboard = Baseboard.left(
position: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2 - (spaceBetweenBaseboards / 2),
baseboardY,
),
),
);
await add(leftBaseboard);
final rightBaseboard = Baseboard.right(
position: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2 + (spaceBetweenBaseboards / 2),
baseboardY,
),
),
);
await add(rightBaseboard);
}
} }
class DebugPinballGame extends PinballGame with TapDetector { class DebugPinballGame extends PinballGame with TapDetector {

@ -18,6 +18,7 @@ void main() {
'loads correctly', 'loads correctly',
(game) async { (game) async {
final ball = Ball(position: Vector2.zero()); final ball = Ball(position: Vector2.zero());
await game.ready();
await game.ensureAdd(ball); await game.ensureAdd(ball);
expect(game.contains(ball), isTrue); expect(game.contains(ball), isTrue);

@ -14,8 +14,14 @@ void main() {
'loads correctly', 'loads correctly',
(game) async { (game) async {
await game.ready(); await game.ready();
final leftBaseboard = Baseboard.left(position: Vector2.zero()); final leftBaseboard = Baseboard(
final rightBaseboard = Baseboard.right(position: Vector2.zero()); position: Vector2.zero(),
side: BoardSide.left,
);
final rightBaseboard = Baseboard(
position: Vector2.zero(),
side: BoardSide.right,
);
await game.ensureAddAll([leftBaseboard, rightBaseboard]); await game.ensureAddAll([leftBaseboard, rightBaseboard]);
expect(game.contains(leftBaseboard), isTrue); expect(game.contains(leftBaseboard), isTrue);
@ -28,7 +34,10 @@ void main() {
'positions correctly', 'positions correctly',
(game) async { (game) async {
final position = Vector2.all(10); final position = Vector2.all(10);
final baseboard = Baseboard.left(position: position); final baseboard = Baseboard(
position: position,
side: BoardSide.left,
);
await game.ensureAdd(baseboard); await game.ensureAdd(baseboard);
game.contains(baseboard); game.contains(baseboard);
@ -39,7 +48,10 @@ void main() {
flameTester.test( flameTester.test(
'is static', 'is static',
(game) async { (game) async {
final baseboard = Baseboard.left(position: Vector2.zero()); final baseboard = Baseboard(
position: Vector2.zero(),
side: BoardSide.left,
);
await game.ensureAdd(baseboard); await game.ensureAdd(baseboard);
expect(baseboard.body.bodyType, equals(BodyType.static)); expect(baseboard.body.bodyType, equals(BodyType.static));
@ -49,8 +61,14 @@ void main() {
flameTester.test( flameTester.test(
'is at an angle', 'is at an angle',
(game) async { (game) async {
final leftBaseboard = Baseboard.left(position: Vector2.zero()); final leftBaseboard = Baseboard(
final rightBaseboard = Baseboard.right(position: Vector2.zero()); position: Vector2.zero(),
side: BoardSide.left,
);
final rightBaseboard = Baseboard(
position: Vector2.zero(),
side: BoardSide.right,
);
await game.ensureAddAll([leftBaseboard, rightBaseboard]); await game.ensureAddAll([leftBaseboard, rightBaseboard]);
expect(leftBaseboard.body.angle, isNegative); expect(leftBaseboard.body.angle, isNegative);
@ -63,7 +81,10 @@ void main() {
flameTester.test( flameTester.test(
'has three', 'has three',
(game) async { (game) async {
final baseboard = Baseboard.left(position: Vector2.zero()); final baseboard = Baseboard(
position: Vector2.zero(),
side: BoardSide.left,
);
await game.ensureAdd(baseboard); await game.ensureAdd(baseboard);
expect(baseboard.body.fixtures.length, equals(3)); expect(baseboard.body.fixtures.length, equals(3));

@ -23,5 +23,12 @@ void main() {
expect(side.isLeft, isFalse); expect(side.isLeft, isFalse);
expect(side.isRight, isTrue); expect(side.isRight, isTrue);
}); });
test('direction is correct', () {
const side = BoardSide.left;
expect(side.direction, equals(-1));
const side2 = BoardSide.right;
expect(side2.direction, equals(1));
});
}); });
} }

@ -0,0 +1,80 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
group('BottomGroup', () {
flameTester.test(
'loads correctly',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
await game.ready();
await game.ensureAdd(bottomGroup);
expect(game.contains(bottomGroup), isTrue);
},
);
group('children', () {
flameTester.test(
'has one left flipper',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
await game.ready();
await game.ensureAdd(bottomGroup);
final leftFlippers = bottomGroup.findNestedChildren<Flipper>(
condition: (flipper) => flipper.side.isLeft,
);
expect(leftFlippers.length, equals(1));
},
);
flameTester.test(
'has one right flipper',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
await game.ready();
await game.ensureAdd(bottomGroup);
final rightFlippers = bottomGroup.findNestedChildren<Flipper>(
condition: (flipper) => flipper.side.isRight,
);
expect(rightFlippers.length, equals(1));
},
);
flameTester.test(
'has two Baseboards',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
await game.ready();
await game.ensureAdd(bottomGroup);
final baseboards = bottomGroup.findNestedChildren<Baseboard>();
expect(baseboards.length, equals(2));
},
);
flameTester.test(
'has two SlingShots',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
await game.ready();
await game.ensureAdd(bottomGroup);
final slingShots = bottomGroup.findNestedChildren<SlingShot>();
expect(slingShots.length, equals(2));
},
);
});
});
}

@ -2,7 +2,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -15,111 +14,21 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(PinballGameTest.create);
group('FlipperGroup', () { group(
'Flipper',
() {
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final flipperGroup = FlipperGroup( final leftFlipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(), position: Vector2.zero(),
spacing: 0,
);
await game.ensureAdd(flipperGroup);
expect(game.contains(flipperGroup), isTrue);
},
);
group('constructor', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final flipperGroup = FlipperGroup(
position: position,
spacing: 0,
);
await game.ensureAdd(flipperGroup);
expect(flipperGroup.position, equals(position));
},
); );
}); final rightFlipper = Flipper.fromSide(
side: BoardSide.right,
group('children', () {
bool Function(Component) flipperSelector(BoardSide side) =>
(component) => component is Flipper && component.side == side;
flameTester.test(
'has only one left Flipper',
(game) async {
final flipperGroup = FlipperGroup(
position: Vector2.zero(), position: Vector2.zero(),
spacing: 0,
);
await game.ensureAdd(flipperGroup);
expect(
() => flipperGroup.children.singleWhere(
flipperSelector(BoardSide.left),
),
returnsNormally,
);
},
);
flameTester.test(
'has only one right Flipper',
(game) async {
final flipperGroup = FlipperGroup(
position: Vector2.zero(),
spacing: 0,
);
await game.ensureAdd(flipperGroup);
expect(
() => flipperGroup.children.singleWhere(
flipperSelector(BoardSide.right),
),
returnsNormally,
);
},
);
flameTester.test(
'spaced correctly',
(game) async {
final flipperGroup = FlipperGroup(
position: Vector2.zero(),
spacing: 2,
); );
await game.ready(); await game.ready();
await game.ensureAdd(flipperGroup);
final leftFlipper = flipperGroup.children.singleWhere(
flipperSelector(BoardSide.left),
) as Flipper;
final rightFlipper = flipperGroup.children.singleWhere(
flipperSelector(BoardSide.right),
) as Flipper;
expect(
leftFlipper.body.position.x + Flipper.width + flipperGroup.spacing,
equals(rightFlipper.body.position.x),
);
},
);
});
});
group(
'Flipper',
() {
flameTester.test(
'loads correctly',
(game) async {
final leftFlipper = Flipper.left(position: Vector2.zero());
final rightFlipper = Flipper.right(position: Vector2.zero());
await game.ready();
await game.ensureAddAll([leftFlipper, rightFlipper]); await game.ensureAddAll([leftFlipper, rightFlipper]);
expect(game.contains(leftFlipper), isTrue); expect(game.contains(leftFlipper), isTrue);
@ -129,10 +38,17 @@ void main() {
group('constructor', () { group('constructor', () {
test('sets BoardSide', () { test('sets BoardSide', () {
final leftFlipper = Flipper.left(position: Vector2.zero()); final leftFlipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
expect(leftFlipper.side, equals(leftFlipper.side)); expect(leftFlipper.side, equals(leftFlipper.side));
final rightFlipper = Flipper.right(position: Vector2.zero()); final rightFlipper = Flipper.fromSide(
side: BoardSide.right,
position: Vector2.zero(),
);
expect(rightFlipper.side, equals(rightFlipper.side)); expect(rightFlipper.side, equals(rightFlipper.side));
}); });
}); });
@ -142,7 +58,10 @@ void main() {
'positions correctly', 'positions correctly',
(game) async { (game) async {
final position = Vector2.all(10); final position = Vector2.all(10);
final flipper = Flipper.left(position: position); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: position,
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
game.contains(flipper); game.contains(flipper);
@ -153,7 +72,10 @@ void main() {
flameTester.test( flameTester.test(
'is dynamic', 'is dynamic',
(game) async { (game) async {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
expect(flipper.body.bodyType, equals(BodyType.dynamic)); expect(flipper.body.bodyType, equals(BodyType.dynamic));
@ -163,7 +85,10 @@ void main() {
flameTester.test( flameTester.test(
'ignores gravity', 'ignores gravity',
(game) async { (game) async {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
expect(flipper.body.gravityScale, isZero); expect(flipper.body.gravityScale, isZero);
@ -173,7 +98,10 @@ void main() {
flameTester.test( flameTester.test(
'has greater mass than Ball', 'has greater mass than Ball',
(game) async { (game) async {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
final ball = Ball(position: Vector2.zero()); final ball = Ball(position: Vector2.zero());
await game.ready(); await game.ready();
@ -191,7 +119,10 @@ void main() {
flameTester.test( flameTester.test(
'has three', 'has three',
(game) async { (game) async {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
expect(flipper.body.fixtures.length, equals(3)); expect(flipper.body.fixtures.length, equals(3));
@ -201,7 +132,10 @@ void main() {
flameTester.test( flameTester.test(
'has density', 'has density',
(game) async { (game) async {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
final fixtures = flipper.body.fixtures; final fixtures = flipper.body.fixtures;
@ -229,7 +163,10 @@ void main() {
late Flipper flipper; late Flipper flipper;
setUp(() { setUp(() {
flipper = Flipper.left(position: Vector2.zero()); flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
}); });
testRawKeyDownEvents(leftKeys, (event) { testRawKeyDownEvents(leftKeys, (event) {
@ -293,7 +230,10 @@ void main() {
late Flipper flipper; late Flipper flipper;
setUp(() { setUp(() {
flipper = Flipper.right(position: Vector2.zero()); flipper = Flipper.fromSide(
side: BoardSide.right,
position: Vector2.zero(),
);
}); });
testRawKeyDownEvents(rightKeys, (event) { testRawKeyDownEvents(rightKeys, (event) {
@ -360,7 +300,10 @@ void main() {
flameTester.test( flameTester.test(
'position is at the left of the left Flipper', 'position is at the left of the left Flipper',
(game) async { (game) async {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper); final flipperAnchor = FlipperAnchor(flipper: flipper);
@ -373,7 +316,10 @@ void main() {
flameTester.test( flameTester.test(
'position is at the right of the right Flipper', 'position is at the right of the right Flipper',
(game) async { (game) async {
final flipper = Flipper.right(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.right,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper); final flipperAnchor = FlipperAnchor(flipper: flipper);
@ -389,7 +335,10 @@ void main() {
flameTester.test( flameTester.test(
'limits enabled', 'limits enabled',
(game) async { (game) async {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper); final flipperAnchor = FlipperAnchor(flipper: flipper);
@ -408,7 +357,10 @@ void main() {
flameTester.test( flameTester.test(
'when Flipper is left', 'when Flipper is left',
(game) async { (game) async {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper); final flipperAnchor = FlipperAnchor(flipper: flipper);
@ -426,7 +378,10 @@ void main() {
flameTester.test( flameTester.test(
'when Flipper is right', 'when Flipper is right',
(game) async { (game) async {
final flipper = Flipper.right(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.right,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper); final flipperAnchor = FlipperAnchor(flipper: flipper);
@ -449,7 +404,10 @@ void main() {
flameTester.test( flameTester.test(
'when Flipper is left', 'when Flipper is left',
(game) async { (game) async {
final flipper = Flipper.left(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.left,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper); final flipperAnchor = FlipperAnchor(flipper: flipper);
@ -473,7 +431,10 @@ void main() {
flameTester.test( flameTester.test(
'when Flipper is right', 'when Flipper is right',
(game) async { (game) async {
final flipper = Flipper.right(position: Vector2.zero()); final flipper = Flipper.fromSide(
side: BoardSide.right,
position: Vector2.zero(),
);
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper); final flipperAnchor = FlipperAnchor(flipper: flipper);

@ -0,0 +1,66 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
class TestBodyComponent extends BodyComponent with InitialPosition {
@override
Body createBody() {
return world.createBody(BodyDef());
}
}
class TestPositionedBodyComponent extends BodyComponent with InitialPosition {
@override
Body createBody() {
return world.createBody(BodyDef()..position = initialPosition);
}
}
void main() {
final flameTester = FlameTester(Forge2DGame.new);
group('InitialPosition', () {
test('correctly sets and gets', () {
final component = TestBodyComponent()..initialPosition = Vector2(1, 2);
expect(component.initialPosition, Vector2(1, 2));
});
test('can only be set once', () {
final component = TestBodyComponent()..initialPosition = Vector2(1, 2);
expect(
() => component.initialPosition = Vector2(3, 4),
throwsA(isA<Error>()),
);
});
flameTester.test(
'returns normally '
'when the body sets the position to initial position',
(game) async {
final component = TestPositionedBodyComponent()
..initialPosition = Vector2.zero();
await expectLater(
() async => game.ensureAdd(component),
returnsNormally,
);
},
);
flameTester.test(
'throws AssertionError '
'when not setting initialPosition to body',
(game) async {
final component = TestBodyComponent()..initialPosition = Vector2.zero();
await game.ensureAdd(component);
await expectLater(
() async => game.ensureAdd(component),
throwsAssertionError,
);
},
);
});
}

@ -0,0 +1,124 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RoundBumper', () {
final flameTester = FlameTester(Forge2DGame.new);
const radius = 1.0;
const points = 1;
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final roundBumper = RoundBumper(
position: Vector2.zero(),
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
expect(game.contains(roundBumper), isTrue);
},
);
flameTester.test(
'has points',
(game) async {
final roundBumper = RoundBumper(
position: Vector2.zero(),
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
expect(roundBumper.points, equals(points));
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final roundBumper = RoundBumper(
position: position,
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
game.contains(roundBumper);
expect(roundBumper.body.position, equals(position));
},
);
flameTester.test(
'is static',
(game) async {
final roundBumper = RoundBumper(
position: Vector2.zero(),
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
expect(roundBumper.body.bodyType, equals(BodyType.static));
},
);
});
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final roundBumper = RoundBumper(
position: Vector2.zero(),
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
expect(roundBumper.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'has restitution',
(game) async {
final roundBumper = RoundBumper(
position: Vector2.zero(),
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
final fixture = roundBumper.body.fixtures[0];
expect(fixture.restitution, greaterThan(0));
},
);
flameTester.test(
'shape is circular',
(game) async {
final roundBumper = RoundBumper(
position: Vector2.zero(),
radius: radius,
points: points,
);
await game.ensureAdd(roundBumper);
final fixture = roundBumper.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(1));
},
);
});
});
}

@ -0,0 +1,180 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
group('SlingShot', () {
final flameTester = FlameTester(Forge2DGame.new);
flameTester.test(
'loads correctly',
(game) async {
final slingShot = SlingShot(
position: Vector2.zero(),
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
expect(game.contains(slingShot), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final slingShot = SlingShot(
position: position,
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
expect(slingShot.body.position, equals(position));
},
);
flameTester.test(
'is static',
(game) async {
final slingShot = SlingShot(
position: Vector2.zero(),
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(
position: Vector2.zero(),
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
expect(slingShot.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'shape is triangular',
(game) async {
final slingShot = SlingShot(
position: Vector2.zero(),
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(
position: Vector2.zero(),
side: BoardSide.left,
);
final rightSlingShot = SlingShot(
position: Vector2.zero(),
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(
position: Vector2.zero(),
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(
position: Vector2.zero(),
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
expect(slingShot.body.fixtures[1], isA<Fixture>());
},
);
flameTester.test(
'shape is edge',
(game) async {
final slingShot = SlingShot(
position: Vector2.zero(),
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
final fixture = slingShot.body.fixtures[1];
expect(fixture.shape.shapeType, equals(ShapeType.edge));
},
);
flameTester.test(
'has restitution',
(game) async {
final slingShot = SlingShot(
position: Vector2.zero(),
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
final fixture = slingShot.body.fixtures[1];
expect(fixture.restitution, greaterThan(0));
},
);
flameTester.test(
'has no friction',
(game) async {
final slingShot = SlingShot(
position: Vector2.zero(),
side: BoardSide.left,
);
await game.ensureAdd(slingShot);
final fixture = slingShot.body.fixtures[1];
expect(fixture.friction, equals(0));
},
);
});
});
}

@ -106,7 +106,7 @@ void main() {
); );
flameTester.test( flameTester.test(
'has friction', 'has no friction',
(game) async { (game) async {
final wall = Wall( final wall = Wall(
start: Vector2.zero(), start: Vector2.zero(),
@ -115,7 +115,7 @@ void main() {
await game.ensureAdd(wall); await game.ensureAdd(wall);
final fixture = wall.body.fixtures[0]; final fixture = wall.body.fixtures[0];
expect(fixture.friction, greaterThan(0)); expect(fixture.friction, equals(0));
}, },
); );
}); });

@ -54,22 +54,13 @@ void main() {
}, },
); );
flameTester.test('has only one FlipperGroup', (game) async { flameTester.test('has only one BottomGroup', (game) async {
await game.ready(); await game.ready();
expect( expect(
game.children.whereType<FlipperGroup>().length, game.children.whereType<BottomGroup>().length,
equals(1), equals(1),
); );
}); });
flameTester.test(
'has two Baseboards',
(game) async {
await game.ready();
final baseboards = game.children.whereType<Baseboard>();
expect(baseboards.length, 2);
},
);
}); });
debugModeFlameTester.test('adds a ball on tap up', (game) async { debugModeFlameTester.test('adds a ball on tap up', (game) async {

@ -1,3 +1,4 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
@ -20,3 +21,41 @@ extension DebugPinballGameTest on DebugPinballGame {
), ),
); );
} }
extension ComponentX on Component {
T findNestedChild<T extends Component>({
bool Function(T)? condition,
}) {
T? nestedChild;
propagateToChildren<T>((child) {
final foundChild = (condition ?? (_) => true)(child);
if (foundChild) {
nestedChild = child;
}
return !foundChild;
});
if (nestedChild == null) {
throw Exception('No child of type $T found.');
} else {
return nestedChild!;
}
}
List<T> findNestedChildren<T extends Component>({
bool Function(T)? condition,
}) {
final nestedChildren = <T>[];
propagateToChildren<T>((child) {
final foundChild = (condition ?? (_) => true)(child);
if (foundChild) {
nestedChildren.add(child);
}
return true;
});
return nestedChildren;
}
}

Loading…
Cancel
Save