fix: merge conflicts on test helper methods

pull/83/head
RuiAlonso 4 years ago
commit a5eb7ca118

@ -16,4 +16,4 @@ jobs:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with: with:
working_directory: packages/pinball_components working_directory: packages/pinball_components
coverage_excludes: "lib/src/generated/*.dart" coverage_excludes: "lib/gen/*.dart"

@ -12,19 +12,19 @@ const _attachedErrorMessage = "Can't add to attached Blueprints";
/// A [Blueprint] is a virtual way of grouping [Component]s /// A [Blueprint] is a virtual way of grouping [Component]s
/// that are related, but they need to be added directly on /// that are related, but they need to be added directly on
/// the [FlameGame] level. /// the [FlameGame] level.
abstract class Blueprint { abstract class Blueprint<T extends FlameGame> {
final List<Component> _components = []; final List<Component> _components = [];
bool _isAttached = false; bool _isAttached = false;
/// Called before the the [Component]s managed /// Called before the the [Component]s managed
/// by this blueprint is added to the [FlameGame] /// by this blueprint is added to the [FlameGame]
void build(); void build(T gameRef);
/// Attach the [Component]s built on [build] to the [game] /// Attach the [Component]s built on [build] to the [game]
/// instance /// instance
@mustCallSuper @mustCallSuper
Future<void> attach(FlameGame game) async { Future<void> attach(T game) async {
build(); build(game);
await game.addAll(_components); await game.addAll(_components);
_isAttached = true; _isAttached = true;
} }
@ -47,7 +47,7 @@ abstract class Blueprint {
/// A [Blueprint] that provides additional /// A [Blueprint] that provides additional
/// structures specific to flame_forge2d /// structures specific to flame_forge2d
abstract class Forge2DBlueprint extends Blueprint { abstract class Forge2DBlueprint extends Blueprint<Forge2DGame> {
final List<ContactCallback> _callbacks = []; final List<ContactCallback> _callbacks = [];
/// Adds a single [ContactCallback] to this blueprint /// Adds a single [ContactCallback] to this blueprint
@ -63,13 +63,11 @@ abstract class Forge2DBlueprint extends Blueprint {
} }
@override @override
Future<void> attach(FlameGame game) async { Future<void> attach(Forge2DGame game) async {
await super.attach(game); await super.attach(game);
assert(game is Forge2DGame, 'Forge2DBlueprint used outside a Forge2DGame');
for (final callback in _callbacks) { for (final callback in _callbacks) {
(game as Forge2DGame).addContactCallback(callback); game.addContactCallback(callback);
} }
} }

@ -1,61 +1,40 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template ball} /// {@template ball_blueprint}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the /// [Blueprint] which cretes a ball game object
/// [PinballGame].
/// {@endtemplate} /// {@endtemplate}
class Ball extends BodyComponent<PinballGame> with InitialPosition, Layered { class BallBlueprint extends Blueprint<PinballGame> {
/// {@macro ball} /// {@macro ball_blueprint}
Ball() { BallBlueprint({required this.position});
// 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]
final Vector2 size = Vector2.all(2);
@override /// The initial position of the [Ball]
Future<void> onLoad() async { final Vector2 position;
await super.onLoad();
final sprite = await gameRef.loadSprite(Assets.images.components.ball.path);
final tint = gameRef.theme.characterTheme.ballColor.withOpacity(0.5);
await add(
SpriteComponent(
sprite: sprite,
size: size,
anchor: Anchor.center,
)..tint(tint),
);
}
@override @override
Body createBody() { void build(PinballGame gameRef) {
final shape = CircleShape()..radius = size.x / 2; final baseColor = gameRef.theme.characterTheme.ballColor;
final ball = Ball(baseColor: baseColor)..add(BallController());
final fixtureDef = FixtureDef(shape)..density = 1;
final bodyDef = BodyDef() add(ball..initialPosition = position + Vector2(0, ball.size.y / 2));
..position = initialPosition
..userData = this
..type = BodyType.dynamic;
return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
}
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the
/// [PinballGame].
/// {@endtemplate}
class BallController extends Component with HasGameRef<PinballGame> {
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left. /// any are left.
/// ///
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall]. /// a [BottomWall].
void lost() { void lost() {
shouldRemove = true; parent?.shouldRemove = true;
final bloc = gameRef.read<GameBloc>()..add(const BallLost()); final bloc = gameRef.read<GameBloc>()..add(const BallLost());
@ -64,19 +43,13 @@ class Ball extends BodyComponent<PinballGame> with InitialPosition, Layered {
gameRef.spawnBall(); gameRef.spawnBall();
} }
} }
}
/// Immediatly and completly [stop]s the ball. /// Adds helper methods to the [Ball]
/// extension BallX on Ball {
/// The [Ball] will no longer be affected by any forces, including it's /// Returns the controller instance of the ball
/// weight and those emitted from collisions. // TODO(erickzanardo): Remove the need of an extension.
void stop() { BallController get controller {
body.setType(BodyType.static); return children.whereType<BallController>().first;
}
/// Allows the [Ball] to be affected by forces.
///
/// If previously [stop]ed, the previous ball's velocity is not kept.
void resume() {
body.setType(BodyType.dynamic);
} }
} }

@ -2,6 +2,7 @@ import 'dart:math' as math;
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template baseboard} /// {@template baseboard}
/// Straight, angled board piece to corral the [Ball] towards the [Flipper]s. /// Straight, angled board piece to corral the [Ball] towards the [Flipper]s.

@ -1,5 +1,6 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template board} /// {@template board}
/// The main flat surface of the [PinballGame], where the [Flipper]s, /// The main flat surface of the [PinballGame], where the [Flipper]s,
@ -127,7 +128,7 @@ class _BottomGroupSide extends Component {
Future<void> onLoad() async { Future<void> onLoad() async {
final direction = _side.direction; final direction = _side.direction;
final flipper = Flipper.fromSide( final flipper = Flipper(
side: _side, side: _side,
)..initialPosition = _position; )..initialPosition = _position;
final baseboard = Baseboard(side: _side) final baseboard = Baseboard(side: _side)

@ -8,6 +8,7 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template bonus_word} /// {@template bonus_word}
/// Loads all [BonusLetter]s to compose a [BonusWord]. /// Loads all [BonusLetter]s to compose a [BonusWord].

@ -4,12 +4,10 @@ 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 'jetpack_ramp.dart'; export 'jetpack_ramp.dart';
export 'joint_anchor.dart'; export 'joint_anchor.dart';
export 'kicker.dart'; export 'kicker.dart';
export 'launcher_ramp.dart'; export 'launcher_ramp.dart';
export 'layer.dart';
export 'pathway.dart'; export 'pathway.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'priority.dart'; export 'priority.dart';

@ -3,10 +3,20 @@ import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
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';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
const _leftFlipperKeys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
];
const _rightFlipperKeys = [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
];
/// {@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.
@ -15,43 +25,9 @@ import 'package:pinball/gen/assets.gen.dart';
/// {@endtemplate flipper} /// {@endtemplate flipper}
class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
/// {@macro flipper} /// {@macro flipper}
Flipper._({ Flipper({
required this.side, required this.side,
required List<LogicalKeyboardKey> keys, }) : _keys = side.isLeft ? _leftFlipperKeys : _rightFlipperKeys;
}) : _keys = keys;
Flipper._left()
: this._(
side: BoardSide.left,
keys: [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
],
);
Flipper._right()
: this._(
side: BoardSide.right,
keys: [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
],
);
/// 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,
}) {
switch (side) {
case BoardSide.left:
return Flipper._left();
case BoardSide.right:
return Flipper._right();
}
}
/// The size of the [Flipper]. /// The size of the [Flipper].
static final size = Vector2(12, 2.8); static final size = Vector2(12, 2.8);
@ -104,35 +80,29 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
/// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion.
Future<void> _anchorToJoint() async { Future<void> _anchorToJoint() async {
final anchor = FlipperAnchor(flipper: this); final anchor = _FlipperAnchor(flipper: this);
await add(anchor); await add(anchor);
final jointDef = FlipperAnchorRevoluteJointDef( final jointDef = _FlipperAnchorRevoluteJointDef(
flipper: this, flipper: this,
anchor: anchor, anchor: anchor,
); );
// TODO(alestiago): Remove casting once the following is closed: final joint = _FlipperJoint(jointDef)..create(world);
// https://github.com/flame-engine/forge2d/issues/36
final joint = world.createJoint(jointDef) as RevoluteJoint;
// FIXME(erickzanardo): when mounted the initial position is not fully // FIXME(erickzanardo): when mounted the initial position is not fully
// reached. // reached.
unawaited( unawaited(
mounted.whenComplete( mounted.whenComplete(joint.unlock),
() => FlipperAnchorRevoluteJointDef.unlock(joint, side),
),
); );
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
final isLeft = side.isLeft; final direction = side.direction;
final bigCircleShape = CircleShape()..radius = 1.75; final bigCircleShape = CircleShape()..radius = 1.75;
bigCircleShape.position.setValues( bigCircleShape.position.setValues(
isLeft ((size.x / 2) * direction) + (bigCircleShape.radius * -direction),
? -(size.x / 2) + bigCircleShape.radius
: (size.x / 2) - bigCircleShape.radius,
0, 0,
); );
final bigCircleFixtureDef = FixtureDef(bigCircleShape); final bigCircleFixtureDef = FixtureDef(bigCircleShape);
@ -140,15 +110,13 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
final smallCircleShape = CircleShape()..radius = 0.9; final smallCircleShape = CircleShape()..radius = 0.9;
smallCircleShape.position.setValues( smallCircleShape.position.setValues(
isLeft ((size.x / 2) * -direction) + (smallCircleShape.radius * direction),
? (size.x / 2) - smallCircleShape.radius
: -(size.x / 2) + smallCircleShape.radius,
0, 0,
); );
final smallCircleFixtureDef = FixtureDef(smallCircleShape); final smallCircleFixtureDef = FixtureDef(smallCircleShape);
fixturesDef.add(smallCircleFixtureDef); fixturesDef.add(smallCircleFixtureDef);
final trapeziumVertices = isLeft final trapeziumVertices = side.isLeft
? [ ? [
Vector2(bigCircleShape.position.x, bigCircleShape.radius), Vector2(bigCircleShape.position.x, bigCircleShape.radius),
Vector2(smallCircleShape.position.x, smallCircleShape.radius), Vector2(smallCircleShape.position.x, smallCircleShape.radius),
@ -173,7 +141,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
paint = Paint()..color = Colors.transparent; renderBody = false;
await Future.wait([ await Future.wait([
_loadSprite(), _loadSprite(),
_anchorToJoint(), _anchorToJoint(),
@ -214,61 +183,66 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
/// ///
/// The end of a [Flipper] depends on its [Flipper.side]. /// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate} /// {@endtemplate}
class FlipperAnchor extends JointAnchor { class _FlipperAnchor extends JointAnchor {
/// {@macro flipper_anchor} /// {@macro flipper_anchor}
FlipperAnchor({ _FlipperAnchor({
required Flipper flipper, required Flipper flipper,
}) { }) {
initialPosition = Vector2( initialPosition = Vector2(
flipper.side.isLeft flipper.body.position.x + ((Flipper.size.x * flipper.side.direction) / 2),
? flipper.body.position.x - Flipper.size.x / 2
: flipper.body.position.x + Flipper.size.x / 2,
flipper.body.position.y, flipper.body.position.y,
); );
} }
} }
/// {@template flipper_anchor_revolute_joint_def} /// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [FlipperAnchor] to achieve an arc motion. /// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion.
/// {@endtemplate} /// {@endtemplate}
class FlipperAnchorRevoluteJointDef extends RevoluteJointDef { class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def} /// {@macro flipper_anchor_revolute_joint_def}
FlipperAnchorRevoluteJointDef({ _FlipperAnchorRevoluteJointDef({
required Flipper flipper, required Flipper flipper,
required FlipperAnchor anchor, required _FlipperAnchor anchor,
}) { }) : side = flipper.side {
initialize( initialize(
flipper.body, flipper.body,
anchor.body, anchor.body,
anchor.body.position, anchor.body.position,
); );
enableLimit = true;
final angle = (flipper.side.isLeft ? _sweepingAngle : -_sweepingAngle) / 2; enableLimit = true;
final angle = (_sweepingAngle * -side.direction) / 2;
lowerAngle = upperAngle = angle; lowerAngle = upperAngle = angle;
} }
/// The total angle of the arc motion. /// The total angle of the arc motion.
static const _sweepingAngle = math.pi / 3.5; static const _sweepingAngle = math.pi / 3.5;
final BoardSide side;
}
class _FlipperJoint extends RevoluteJoint {
_FlipperJoint(_FlipperAnchorRevoluteJointDef def)
: side = def.side,
super(def);
final BoardSide side;
// TODO(alestiago): Remove once Forge2D supports custom joints.
void create(World world) {
world.joints.add(this);
bodyA.joints.add(this);
bodyB.joints.add(this);
}
/// Unlocks the [Flipper] from its resting position. /// Unlocks the [Flipper] from its resting position.
/// ///
/// The [Flipper] is locked when initialized in order to force it to be at /// The [Flipper] is locked when initialized in order to force it to be at
/// its resting position. /// its resting position.
// TODO(alestiago): consider refactor once the issue is solved: void unlock() {
// https://github.com/flame-engine/forge2d/issues/36 setLimits(
static void unlock(RevoluteJoint joint, BoardSide side) { lowerLimit * side.direction,
late final double upperLimit, lowerLimit; -upperLimit * side.direction,
switch (side) { );
case BoardSide.left:
lowerLimit = -joint.lowerLimit;
upperLimit = joint.upperLimit;
break;
case BoardSide.right:
lowerLimit = joint.lowerLimit;
upperLimit = -joint.upperLimit;
}
joint.setLimits(lowerLimit, upperLimit);
} }
} }

@ -4,6 +4,7 @@ import 'package:flame/components.dart';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template jetpack_ramp} /// {@template jetpack_ramp}
/// Represents the upper left blue ramp of the [Board]. /// Represents the upper left blue ramp of the [Board].

@ -1,5 +1,5 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template joint_anchor} /// {@template joint_anchor}
/// Non visual [BodyComponent] used to hold a [BodyType.dynamic] in [Joint]s /// Non visual [BodyComponent] used to hold a [BodyType.dynamic] in [Joint]s

@ -5,6 +5,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geometry/geometry.dart' as geometry show centroid; import 'package:geometry/geometry.dart' as geometry show centroid;
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template kicker} /// {@template kicker}
/// Triangular [BodyType.static] body that propels the [Ball] towards the /// Triangular [BodyType.static] body that propels the [Ball] towards the

@ -4,6 +4,7 @@ import 'package:flame/components.dart';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template launcher_ramp} /// {@template launcher_ramp}
/// The yellow left ramp, where the [Ball] goes through when launched from the /// The yellow left ramp, where the [Ball] goes through when launched from the

@ -2,7 +2,7 @@ import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geometry/geometry.dart'; import 'package:geometry/geometry.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template pathway} /// {@template pathway}
/// [Pathway] creates lines of various shapes. /// [Pathway] creates lines of various shapes.
@ -144,6 +144,46 @@ class Pathway extends BodyComponent with InitialPosition, Layered {
); );
} }
/// Creates an ellipse [Pathway].
///
/// Does so with two [ChainShape]s separated by a [width]. Can
/// be rotated by a given [rotation] in radians.
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.ellipse({
Color? color,
required Vector2 center,
required double width,
required double majorRadius,
required double minorRadius,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final outerWall = calculateEllipse(
center: center,
majorRadius: majorRadius,
minorRadius: minorRadius,
).map((vector) => vector..rotate(rotation)).toList();
paths.add(outerWall);
if (!singleWall) {
final innerWall = calculateEllipse(
center: center,
majorRadius: majorRadius - width,
minorRadius: minorRadius - width,
).map((vector) => vector..rotate(rotation)).toList();
paths.add(innerWall);
}
return Pathway._(
color: color,
paths: paths,
);
}
final List<List<Vector2>> _paths; final List<List<Vector2>> _paths;
/// Constructs different [ChainShape]s to form the [Pathway] shape. /// Constructs different [ChainShape]s to form the [Pathway] shape.

@ -2,6 +2,7 @@ import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template plunger} /// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the /// [Plunger] serves as a spring, that shoots the ball on the right side of the

@ -2,6 +2,7 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template ramp_orientation} /// {@template ramp_orientation}
/// Determines if a ramp is facing [up] or [down] on the [Board]. /// Determines if a ramp is facing [up] or [down] on the [Board].

@ -1,5 +1,6 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template round_bumper} /// {@template round_bumper}
/// Circular body that repels a [Ball] on contact, increasing the score. /// Circular body that repels a [Ball] on contact, increasing the score.

@ -2,11 +2,12 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template score_points} /// {@template score_points}
/// Specifies the amount of points received on [Ball] collision. /// Specifies the amount of points received on [Ball] collision.
/// {@endtemplate} /// {@endtemplate}
mixin ScorePoints on BodyComponent { mixin ScorePoints<T extends Forge2DGame> on BodyComponent<T> {
/// {@macro score_points} /// {@macro score_points}
int get points; int get points;
@ -26,6 +27,8 @@ class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
ScorePoints scorePoints, ScorePoints scorePoints,
Contact _, Contact _,
) { ) {
ball.gameRef.read<GameBloc>().add(Scored(points: scorePoints.points)); ball.controller.gameRef.read<GameBloc>().add(
Scored(points: scorePoints.points),
);
} }
} }

@ -9,6 +9,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart'; import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
/// A [Blueprint] which creates the spaceship feature. /// A [Blueprint] which creates the spaceship feature.
class Spaceship extends Forge2DBlueprint { class Spaceship extends Forge2DBlueprint {
@ -16,7 +17,7 @@ class Spaceship extends Forge2DBlueprint {
static const radius = 10.0; static const radius = 10.0;
@override @override
void build() { void build(_) {
final position = Vector2( final position = Vector2(
PinballGame.boardBounds.left + radius + 15, PinballGame.boardBounds.left + radius + 15,
PinballGame.boardBounds.center.dy + 30, PinballGame.boardBounds.center.dy + 30,

@ -4,6 +4,7 @@ import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/components/components.dart'; import 'package:pinball/game/components/components.dart';
import 'package:pinball/game/pinball_game.dart'; import 'package:pinball/game/pinball_game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template wall} /// {@template wall}
/// A continuous generic and [BodyType.static] barrier that divides a game area. /// A continuous generic and [BodyType.static] barrier that divides a game area.
@ -77,6 +78,6 @@ class BottomWall extends Wall {
class BottomWallBallContactCallback extends ContactCallback<Ball, BottomWall> { class BottomWallBallContactCallback extends ContactCallback<Ball, BottomWall> {
@override @override
void begin(Ball ball, BottomWall wall, Contact contact) { void begin(Ball ball, BottomWall wall, Contact contact) {
ball.lost(); ball.controller.lost();
} }
} }

@ -1,12 +1,13 @@
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' as components;
/// Add methods to help loading and caching game assets. /// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame { extension PinballGameAssetsX on PinballGame {
/// Pre load the initial assets of the game. /// Pre load the initial assets of the game.
Future<void> preLoadAssets() async { Future<void> preLoadAssets() async {
await Future.wait([ await Future.wait([
images.load(Assets.images.components.ball.path), images.load(components.Assets.images.ball.keyName),
images.load(Assets.images.components.flipper.path), images.load(Assets.images.components.flipper.path),
images.load(Assets.images.components.spaceship.androidTop.path), images.load(Assets.images.components.spaceship.androidTop.path),
images.load(Assets.images.components.spaceship.androidBottom.path), images.load(Assets.images.components.spaceship.androidBottom.path),

@ -1,5 +1,4 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:async'; import 'dart:async';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
@ -103,11 +102,7 @@ class PinballGame extends Forge2DGame
} }
void spawnBall() { void spawnBall() {
final ball = Ball(); addFromBlueprint(BallBlueprint(position: plunger.body.position));
add(
ball
..initialPosition = plunger.body.position + Vector2(0, ball.size.y / 2),
);
} }
} }
@ -116,8 +111,6 @@ class DebugPinballGame extends PinballGame with TapDetector {
@override @override
void onTapUp(TapUpInfo info) { void onTapUp(TapUpInfo info) {
add( addFromBlueprint(BallBlueprint(position: info.eventPosition.game));
Ball()..initialPosition = info.eventPosition.game,
);
} }
} }

@ -15,8 +15,6 @@ class $AssetsImagesGen {
class $AssetsImagesComponentsGen { class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
AssetGenImage get ball =>
const AssetGenImage('assets/images/components/ball.png');
AssetGenImage get flipper => AssetGenImage get flipper =>
const AssetGenImage('assets/images/components/flipper.png'); const AssetGenImage('assets/images/components/flipper.png');
AssetGenImage get sauce => AssetGenImage get sauce =>

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/leaderboard/leaderboard.dart';
part 'leaderboard_event.dart'; part 'leaderboard_event.dart';
part 'leaderboard_state.dart'; part 'leaderboard_state.dart';
@ -30,10 +31,16 @@ class LeaderboardBloc extends Bloc<LeaderboardEvent, LeaderboardState> {
try { try {
final top10Leaderboard = final top10Leaderboard =
await _leaderboardRepository.fetchTop10Leaderboard(); await _leaderboardRepository.fetchTop10Leaderboard();
final leaderboardEntries = <LeaderboardEntry>[];
top10Leaderboard.asMap().forEach(
(index, value) => leaderboardEntries.add(value.toEntry(index + 1)),
);
emit( emit(
state.copyWith( state.copyWith(
status: LeaderboardStatus.success, status: LeaderboardStatus.success,
leaderboard: top10Leaderboard, leaderboard: leaderboardEntries,
), ),
); );
} catch (error) { } catch (error) {

@ -9,7 +9,7 @@ abstract class LeaderboardEvent extends Equatable {
} }
/// {@template top_10_fetched} /// {@template top_10_fetched}
/// Request the top 10 [LeaderboardEntry]s. /// Request the top 10 [LeaderboardEntryData]s.
/// {endtemplate} /// {endtemplate}
class Top10Fetched extends LeaderboardEvent { class Top10Fetched extends LeaderboardEvent {
/// {@macro top_10_fetched} /// {@macro top_10_fetched}
@ -20,7 +20,7 @@ class Top10Fetched extends LeaderboardEvent {
} }
/// {@template leaderboard_entry_added} /// {@template leaderboard_entry_added}
/// Writes a new [LeaderboardEntry]. /// Writes a new [LeaderboardEntryData].
/// ///
/// Should be added when a player finishes a game. /// Should be added when a player finishes a game.
/// {endtemplate} /// {endtemplate}
@ -28,8 +28,8 @@ class LeaderboardEntryAdded extends LeaderboardEvent {
/// {@macro leaderboard_entry_added} /// {@macro leaderboard_entry_added}
const LeaderboardEntryAdded({required this.entry}); const LeaderboardEntryAdded({required this.entry});
/// [LeaderboardEntry] to be written to the remote storage. /// [LeaderboardEntryData] to be written to the remote storage.
final LeaderboardEntry entry; final LeaderboardEntryData entry;
@override @override
List<Object?> get props => [entry]; List<Object?> get props => [entry];

@ -1 +1,2 @@
export 'bloc/leaderboard_bloc.dart'; export 'bloc/leaderboard_bloc.dart';
export 'models/leader_board_entry.dart';

@ -0,0 +1,80 @@
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template leaderboard_entry}
/// A model representing a leaderboard entry containing the ranking position,
/// player's initials, score, and chosen character.
///
/// {@endtemplate}
class LeaderboardEntry {
/// {@macro leaderboard_entry}
LeaderboardEntry({
required this.rank,
required this.playerInitials,
required this.score,
required this.character,
});
/// Ranking position for [LeaderboardEntry].
final String rank;
/// Player's chosen initials for [LeaderboardEntry].
final String playerInitials;
/// Score for [LeaderboardEntry].
final int score;
/// [CharacterTheme] for [LeaderboardEntry].
final AssetGenImage character;
}
/// Converts [LeaderboardEntryData] from repository to [LeaderboardEntry].
extension LeaderboardEntryDataX on LeaderboardEntryData {
/// Conversion method to [LeaderboardEntry]
LeaderboardEntry toEntry(int position) {
return LeaderboardEntry(
rank: position.toString(),
playerInitials: playerInitials,
score: score,
character: character.toTheme.characterAsset,
);
}
}
/// Converts [CharacterType] to [CharacterTheme] to show on UI character theme
/// from repository.
extension CharacterTypeX on CharacterType {
/// Conversion method to [CharacterTheme]
CharacterTheme get toTheme {
switch (this) {
case CharacterType.dash:
return const DashTheme();
case CharacterType.sparky:
return const SparkyTheme();
case CharacterType.android:
return const AndroidTheme();
case CharacterType.dino:
return const DinoTheme();
}
}
}
/// Converts [CharacterTheme] to [CharacterType] to persist at repository the
/// character theme from UI.
extension CharacterThemeX on CharacterTheme {
/// Conversion method to [CharacterType]
CharacterType get toType {
switch (runtimeType) {
case DashTheme:
return CharacterType.dash;
case SparkyTheme:
return CharacterType.sparky;
case AndroidTheme:
return CharacterType.android;
case DinoTheme:
return CharacterType.dino;
default:
return CharacterType.dash;
}
}
}

@ -23,10 +23,45 @@ List<Vector2> calculateArc({
final points = <Vector2>[]; final points = <Vector2>[];
for (var i = 0; i < precision; i++) { for (var i = 0; i < precision; i++) {
final xCoord = center.x + radius * math.cos((stepAngle * i) + offsetAngle); final x = center.x + radius * math.cos((stepAngle * i) + offsetAngle);
final yCoord = center.y - radius * math.sin((stepAngle * i) + offsetAngle); final y = center.y - radius * math.sin((stepAngle * i) + offsetAngle);
final point = Vector2(xCoord, yCoord); final point = Vector2(x, y);
points.add(point);
}
return points;
}
/// Calculates all [Vector2]s of an ellipse.
///
/// An ellipse can be achieved by specifying a [center], a [majorRadius] and a
/// [minorRadius].
///
/// The higher the [precision], the more [Vector2]s will be calculated;
/// achieving a more rounded ellipse.
///
/// For more information read: https://en.wikipedia.org/wiki/Ellipse.
List<Vector2> calculateEllipse({
required Vector2 center,
required double majorRadius,
required double minorRadius,
int precision = 100,
}) {
assert(
0 < minorRadius && minorRadius <= majorRadius,
'smallRadius ($minorRadius) and bigRadius ($majorRadius) must be in '
'range 0 < smallRadius <= bigRadius',
);
final stepAngle = 2 * math.pi / (precision - 1);
final points = <Vector2>[];
for (var i = 0; i < precision; i++) {
final x = center.x + minorRadius * math.cos(stepAngle * i);
final y = center.y - majorRadius * math.sin(stepAngle * i);
final point = Vector2(x, y);
points.add(point); points.add(point);
} }
@ -63,17 +98,15 @@ List<Vector2> calculateBezierCurve({
final points = <Vector2>[]; final points = <Vector2>[];
do { do {
var xCoord = 0.0; var x = 0.0;
var yCoord = 0.0; var y = 0.0;
for (var i = 0; i <= n; i++) { for (var i = 0; i <= n; i++) {
final point = controlPoints[i]; final point = controlPoints[i];
xCoord += x += binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.x;
binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.x; y += binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y;
yCoord +=
binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y;
} }
points.add(Vector2(xCoord, yCoord)); points.add(Vector2(x, y));
t = t + step; t = t + step;
} while (t <= 1); } while (t <= 1);

@ -33,6 +33,46 @@ void main() {
}); });
}); });
group('calculateEllipse', () {
test('returns by default 100 points as indicated by precision', () {
final points = calculateEllipse(
center: Vector2.zero(),
majorRadius: 100,
minorRadius: 50,
);
expect(points.length, 100);
});
test('returns as many points as indicated by precision', () {
final points = calculateEllipse(
center: Vector2.zero(),
majorRadius: 100,
minorRadius: 50,
precision: 50,
);
expect(points.length, 50);
});
test('fails if radius not in range', () {
expect(
() => calculateEllipse(
center: Vector2.zero(),
majorRadius: 100,
minorRadius: 150,
),
throwsA(isA<AssertionError>()),
);
expect(
() => calculateEllipse(
center: Vector2.zero(),
majorRadius: 100,
minorRadius: 0,
),
throwsA(isA<AssertionError>()),
);
});
});
group('calculateBezierCurve', () { group('calculateBezierCurve', () {
test('fails if step not in range', () { test('fails if step not in range', () {
expect( expect(

@ -83,9 +83,9 @@ class LeaderboardRepository {
final FirebaseFirestore _firebaseFirestore; final FirebaseFirestore _firebaseFirestore;
/// Acquires top 10 [LeaderboardEntry]s. /// Acquires top 10 [LeaderboardEntryData]s.
Future<List<LeaderboardEntry>> fetchTop10Leaderboard() async { Future<List<LeaderboardEntryData>> fetchTop10Leaderboard() async {
final leaderboardEntries = <LeaderboardEntry>[]; final leaderboardEntries = <LeaderboardEntryData>[];
late List<QueryDocumentSnapshot> documents; late List<QueryDocumentSnapshot> documents;
try { try {
@ -103,7 +103,7 @@ class LeaderboardRepository {
final data = document.data() as Map<String, dynamic>?; final data = document.data() as Map<String, dynamic>?;
if (data != null) { if (data != null) {
try { try {
leaderboardEntries.add(LeaderboardEntry.fromJson(data)); leaderboardEntries.add(LeaderboardEntryData.fromJson(data));
} catch (error, stackTrace) { } catch (error, stackTrace) {
throw LeaderboardDeserializationException(error, stackTrace); throw LeaderboardDeserializationException(error, stackTrace);
} }
@ -115,7 +115,9 @@ class LeaderboardRepository {
/// Adds player's score entry to the leaderboard and gets their /// Adds player's score entry to the leaderboard and gets their
/// [LeaderboardRanking]. /// [LeaderboardRanking].
Future<LeaderboardRanking> addLeaderboardEntry(LeaderboardEntry entry) async { Future<LeaderboardRanking> addLeaderboardEntry(
LeaderboardEntryData entry,
) async {
late DocumentReference entryReference; late DocumentReference entryReference;
try { try {
entryReference = await _firebaseFirestore entryReference = await _firebaseFirestore

@ -1,9 +1,9 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'leaderboard_entry.g.dart'; part 'leaderboard_entry_data.g.dart';
/// Google character type associated with a [LeaderboardEntry]. /// Google character type associated with a [LeaderboardEntryData].
enum CharacterType { enum CharacterType {
/// Dash character. /// Dash character.
dash, dash,
@ -18,7 +18,7 @@ enum CharacterType {
dino, dino,
} }
/// {@template leaderboard_entry} /// {@template leaderboard_entry_data}
/// A model representing a leaderboard entry containing the player's initials, /// A model representing a leaderboard entry containing the player's initials,
/// score, and chosen character. /// score, and chosen character.
/// ///
@ -34,42 +34,42 @@ enum CharacterType {
/// ``` /// ```
/// {@endtemplate} /// {@endtemplate}
@JsonSerializable() @JsonSerializable()
class LeaderboardEntry extends Equatable { class LeaderboardEntryData extends Equatable {
/// {@macro leaderboard_entry} /// {@macro leaderboard_entry_data}
const LeaderboardEntry({ const LeaderboardEntryData({
required this.playerInitials, required this.playerInitials,
required this.score, required this.score,
required this.character, required this.character,
}); });
/// Factory which converts a [Map] into a [LeaderboardEntry]. /// Factory which converts a [Map] into a [LeaderboardEntryData].
factory LeaderboardEntry.fromJson(Map<String, dynamic> json) { factory LeaderboardEntryData.fromJson(Map<String, dynamic> json) {
return _$LeaderboardEntryFromJson(json); return _$LeaderboardEntryFromJson(json);
} }
/// Converts the [LeaderboardEntry] to [Map]. /// Converts the [LeaderboardEntryData] to [Map].
Map<String, dynamic> toJson() => _$LeaderboardEntryToJson(this); Map<String, dynamic> toJson() => _$LeaderboardEntryToJson(this);
/// Player's chosen initials for [LeaderboardEntry]. /// Player's chosen initials for [LeaderboardEntryData].
/// ///
/// Example: 'ABC'. /// Example: 'ABC'.
@JsonKey(name: 'playerInitials') @JsonKey(name: 'playerInitials')
final String playerInitials; final String playerInitials;
/// Score for [LeaderboardEntry]. /// Score for [LeaderboardEntryData].
/// ///
/// Example: 1500. /// Example: 1500.
@JsonKey(name: 'score') @JsonKey(name: 'score')
final int score; final int score;
/// [CharacterType] for [LeaderboardEntry]. /// [CharacterType] for [LeaderboardEntryData].
/// ///
/// Example: [CharacterType.dash]. /// Example: [CharacterType.dash].
@JsonKey(name: 'character') @JsonKey(name: 'character')
final CharacterType character; final CharacterType character;
/// An empty [LeaderboardEntry] object. /// An empty [LeaderboardEntryData] object.
static const empty = LeaderboardEntry( static const empty = LeaderboardEntryData(
playerInitials: '', playerInitials: '',
score: 0, score: 0,
character: CharacterType.dash, character: CharacterType.dash,

@ -1,19 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'leaderboard_entry.dart'; part of 'leaderboard_entry_data.dart';
// ************************************************************************** // **************************************************************************
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
LeaderboardEntry _$LeaderboardEntryFromJson(Map<String, dynamic> json) => LeaderboardEntryData _$LeaderboardEntryFromJson(Map<String, dynamic> json) =>
LeaderboardEntry( LeaderboardEntryData(
playerInitials: json['playerInitials'] as String, playerInitials: json['playerInitials'] as String,
score: json['score'] as int, score: json['score'] as int,
character: $enumDecode(_$CharacterTypeEnumMap, json['character']), character: $enumDecode(_$CharacterTypeEnumMap, json['character']),
); );
Map<String, dynamic> _$LeaderboardEntryToJson(LeaderboardEntry instance) => Map<String, dynamic> _$LeaderboardEntryToJson(LeaderboardEntryData instance) =>
<String, dynamic>{ <String, dynamic>{
'playerInitials': instance.playerInitials, 'playerInitials': instance.playerInitials,
'score': instance.score, 'score': instance.score,

@ -2,17 +2,17 @@ import 'package:equatable/equatable.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
/// {@template leaderboard_ranking} /// {@template leaderboard_ranking}
/// Contains [ranking] for a single [LeaderboardEntry] and the number of players /// Contains [ranking] for a single [LeaderboardEntryData] and the number of
/// the [ranking] is [outOf]. /// players the [ranking] is [outOf].
/// {@endtemplate} /// {@endtemplate}
class LeaderboardRanking extends Equatable { class LeaderboardRanking extends Equatable {
/// {@macro leaderboard_ranking} /// {@macro leaderboard_ranking}
const LeaderboardRanking({required this.ranking, required this.outOf}); const LeaderboardRanking({required this.ranking, required this.outOf});
/// Place ranking by score for a [LeaderboardEntry]. /// Place ranking by score for a [LeaderboardEntryData].
final int ranking; final int ranking;
/// Number of [LeaderboardEntry]s at the time of score entry. /// Number of [LeaderboardEntryData]s at the time of score entry.
final int outOf; final int outOf;
@override @override

@ -1,2 +1,2 @@
export 'leaderboard_entry.dart'; export 'leaderboard_entry_data.dart';
export 'leaderboard_ranking.dart'; export 'leaderboard_ranking.dart';

@ -57,7 +57,7 @@ void main() {
final top10Leaderboard = top10Scores final top10Leaderboard = top10Scores
.map( .map(
(score) => LeaderboardEntry( (score) => LeaderboardEntryData(
playerInitials: 'user$score', playerInitials: 'user$score',
score: score, score: score,
character: CharacterType.dash, character: CharacterType.dash,
@ -144,7 +144,7 @@ void main() {
entryScore, entryScore,
1000, 1000,
]; ];
final leaderboardEntry = LeaderboardEntry( final leaderboardEntry = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: entryScore, score: entryScore,
character: CharacterType.dash, character: CharacterType.dash,

@ -9,21 +9,21 @@ void main() {
'character': 'dash', 'character': 'dash',
}; };
const leaderboardEntry = LeaderboardEntry( const leaderboardEntry = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 1500, score: 1500,
character: CharacterType.dash, character: CharacterType.dash,
); );
test('can be instantiated', () { test('can be instantiated', () {
const leaderboardEntry = LeaderboardEntry.empty; const leaderboardEntry = LeaderboardEntryData.empty;
expect(leaderboardEntry, isNotNull); expect(leaderboardEntry, isNotNull);
}); });
test('supports value equality.', () { test('supports value equality.', () {
const leaderboardEntry = LeaderboardEntry.empty; const leaderboardEntry = LeaderboardEntryData.empty;
const leaderboardEntry2 = LeaderboardEntry.empty; const leaderboardEntry2 = LeaderboardEntryData.empty;
expect(leaderboardEntry, equals(leaderboardEntry2)); expect(leaderboardEntry, equals(leaderboardEntry2));
}); });
@ -33,7 +33,7 @@ void main() {
}); });
test('can be obtained from json', () { test('can be obtained from json', () {
final leaderboardEntryFrom = LeaderboardEntry.fromJson(data); final leaderboardEntryFrom = LeaderboardEntryData.fromJson(data);
expect(leaderboardEntry, equals(leaderboardEntryFrom)); expect(leaderboardEntry, equals(leaderboardEntryFrom));
}); });

@ -1 +1,4 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml include: package:very_good_analysis/analysis_options.2.4.0.yaml
analyzer:
exclude:
- lib/**/*.gen.dart

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

@ -0,0 +1,68 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
const $AssetsImagesGen();
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
}
class Assets {
Assets._();
static const $AssetsImagesGen images = $AssetsImagesGen();
}
class AssetGenImage extends AssetImage {
const AssetGenImage(String assetName)
: super(assetName, package: 'pinball_components');
Image image({
Key? key,
ImageFrameBuilder? frameBuilder,
ImageLoadingBuilder? loadingBuilder,
ImageErrorWidgetBuilder? errorBuilder,
String? semanticLabel,
bool excludeFromSemantics = false,
double? width,
double? height,
Color? color,
BlendMode? colorBlendMode,
BoxFit? fit,
AlignmentGeometry alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect? centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = false,
bool isAntiAlias = false,
FilterQuality filterQuality = FilterQuality.low,
}) {
return Image(
key: key,
image: this,
frameBuilder: frameBuilder,
loadingBuilder: loadingBuilder,
errorBuilder: errorBuilder,
semanticLabel: semanticLabel,
excludeFromSemantics: excludeFromSemantics,
width: width,
height: height,
color: color,
colorBlendMode: colorBlendMode,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
matchTextDirection: matchTextDirection,
gaplessPlayback: gaplessPlayback,
isAntiAlias: isAntiAlias,
filterQuality: filterQuality,
);
}
String get path => assetName;
}

@ -1,3 +1,4 @@
library pinball_components; library pinball_components;
export 'gen/assets.gen.dart';
export 'src/pinball_components.dart'; export 'src/pinball_components.dart';

@ -0,0 +1,72 @@
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around
/// {@endtemplate}
class Ball<T extends Forge2DGame> extends BodyComponent<T>
with Layered, InitialPosition {
/// {@macro ball_body}
Ball({
required this.baseColor,
}) {
// 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]
final Vector2 size = Vector2.all(2);
/// The base [Color] used to tint this [Ball]
final Color baseColor;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(Assets.images.ball.keyName);
final tint = baseColor.withOpacity(0.5);
await add(
SpriteComponent(
sprite: sprite,
size: size,
anchor: Anchor.center,
)..tint(tint),
);
}
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(shape)..density = 1;
final bodyDef = BodyDef()
..position = initialPosition
..userData = this
..type = BodyType.dynamic;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
/// Immediatly and completly [stop]s the ball.
///
/// The [Ball] will no longer be affected by any forces, including it's
/// weight and those emitted from collisions.
void stop() {
body.setType(BodyType.static);
}
/// Allows the [Ball] to be affected by forces.
///
/// If previously [stop]ed, the previous ball's velocity is not kept.
void resume() {
body.setType(BodyType.dynamic);
}
}

@ -0,0 +1,3 @@
export 'ball.dart';
export 'initial_position.dart';
export 'layer.dart';

@ -1,7 +1 @@
/// {@template pinball_components} export 'components/components.dart';
/// Package with the UI game components for the Pinball Game
/// {@endtemplate}
class PinballComponents {
/// {@macro pinball_components}
const PinballComponents();
}

@ -7,10 +7,24 @@ environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.16.0 <3.0.0"
dependencies: dependencies:
flame: ^1.1.0-releasecandidate.6
flame_forge2d: ^0.9.0-releasecandidate.6
flutter: flutter:
sdk: flutter sdk: flutter
dev_dependencies: dev_dependencies:
flame_test: ^1.1.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mocktail: ^0.2.0
very_good_analysis: ^2.4.0 very_good_analysis: ^2.4.0
flutter:
generate: true
assets:
- assets/images/
flutter_gen:
line_length: 80
assets:
package_parameter_enabled: true

@ -0,0 +1,7 @@
import 'package:flame_forge2d/flame_forge2d.dart';
class TestGame extends Forge2DGame {
TestGame() {
images.prefix = '';
}
}

@ -0,0 +1,162 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('Ball', () {
flameTester.test(
'loads correctly',
(game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ready();
await game.ensureAdd(ball);
expect(game.contains(ball), isTrue);
},
);
group('body', () {
flameTester.test(
'is dynamic',
(game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
expect(ball.body.bodyType, equals(BodyType.dynamic));
},
);
group('can be moved', () {
flameTester.test('by its weight', (game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
game.update(1);
expect(ball.body.position, isNot(equals(ball.initialPosition)));
});
flameTester.test('by applying velocity', (game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
ball.body.gravityScale = 0;
ball.body.linearVelocity.setValues(10, 10);
game.update(1);
expect(ball.body.position, isNot(equals(ball.initialPosition)));
});
});
});
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
expect(ball.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'is dense',
(game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
expect(fixture.density, greaterThan(0));
},
);
flameTester.test(
'shape is circular',
(game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(1));
},
);
flameTester.test(
'has Layer.all as default filter maskBits',
(game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ready();
await game.ensureAdd(ball);
await game.ready();
final fixture = ball.body.fixtures[0];
expect(fixture.filterData.maskBits, equals(Layer.board.maskBits));
},
);
});
group('stop', () {
group("can't be moved", () {
flameTester.test('by its weight', (game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
ball.stop();
game.update(1);
expect(ball.body.position, equals(ball.initialPosition));
});
});
flameTester.test('by applying velocity', (game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
ball.stop();
ball.body.linearVelocity.setValues(10, 10);
game.update(1);
expect(ball.body.position, equals(ball.initialPosition));
});
});
group('resume', () {
group('can move', () {
flameTester.test(
'by its weight when previously stopped',
(game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
ball.stop();
ball.resume();
game.update(1);
expect(ball.body.position, isNot(equals(ball.initialPosition)));
},
);
flameTester.test(
'by applying velocity when previously stopped',
(game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
ball.stop();
ball.resume();
ball.body.gravityScale = 0;
ball.body.linearVelocity.setValues(10, 10);
game.update(1);
expect(ball.body.position, isNot(equals(ball.initialPosition)));
},
);
});
});
});
}

@ -3,7 +3,7 @@
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_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart';
class TestBodyComponent extends BodyComponent with InitialPosition { class TestBodyComponent extends BodyComponent with InitialPosition {
@override @override

@ -4,7 +4,7 @@ import 'dart:math' as math;
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_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart';
class TestBodyComponent extends BodyComponent with Layered { class TestBodyComponent extends BodyComponent with Layered {
@override @override

@ -1,11 +0,0 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('PinballComponents', () {
test('can be instantiated', () {
expect(PinballComponents(), isNotNull);
});
});
}

@ -392,6 +392,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
pinball_components:
dependency: "direct main"
description:
path: "packages/pinball_components"
relative: true
source: path
version: "1.0.0+1"
pinball_theme: pinball_theme:
dependency: "direct main" dependency: "direct main"
description: description:

@ -23,6 +23,8 @@ dependencies:
intl: ^0.17.0 intl: ^0.17.0
leaderboard_repository: leaderboard_repository:
path: packages/leaderboard_repository path: packages/leaderboard_repository
pinball_components:
path: packages/pinball_components
pinball_theme: pinball_theme:
path: packages/pinball_theme path: packages/pinball_theme

@ -1,5 +1,4 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/flame/blueprint.dart'; import 'package:pinball/flame/blueprint.dart';
@ -9,7 +8,7 @@ import '../helpers/helpers.dart';
class MyBlueprint extends Blueprint { class MyBlueprint extends Blueprint {
@override @override
void build() { void build(_) {
add(Component()); add(Component());
addAll([Component(), Component()]); addAll([Component(), Component()]);
} }
@ -17,7 +16,7 @@ class MyBlueprint extends Blueprint {
class MyForge2dBlueprint extends Forge2DBlueprint { class MyForge2dBlueprint extends Forge2DBlueprint {
@override @override
void build() { void build(_) {
addContactCallback(MockContactCallback()); addContactCallback(MockContactCallback());
addAllContactCallback([MockContactCallback(), MockContactCallback()]); addAllContactCallback([MockContactCallback(), MockContactCallback()]);
} }
@ -26,7 +25,7 @@ class MyForge2dBlueprint extends Forge2DBlueprint {
void main() { void main() {
group('Blueprint', () { group('Blueprint', () {
test('components can be added to it', () { test('components can be added to it', () {
final blueprint = MyBlueprint()..build(); final blueprint = MyBlueprint()..build(MockPinballGame());
expect(blueprint.components.length, equals(3)); expect(blueprint.components.length, equals(3));
}); });
@ -59,7 +58,7 @@ void main() {
}); });
test('callbacks can be added to it', () { test('callbacks can be added to it', () {
final blueprint = MyForge2dBlueprint()..build(); final blueprint = MyForge2dBlueprint()..build(MockPinballGame());
expect(blueprint.callbacks.length, equals(3)); expect(blueprint.callbacks.length, equals(3));
}); });
@ -92,12 +91,5 @@ void main() {
); );
}, },
); );
test('throws assertion error when used on a non Forge2dGame', () {
expect(
() => MyForge2dBlueprint().attach(FlameGame()),
throwsAssertionError,
);
});
}); });
} }

@ -1,110 +1,17 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
group('Ball', () { group('Ball', () {
flameTester.test(
'loads correctly',
(game) async {
final ball = Ball();
await game.ready();
await game.ensureAdd(ball);
expect(game.contains(ball), isTrue);
},
);
group('body', () {
flameTester.test(
'is dynamic',
(game) async {
final ball = Ball();
await game.ensureAdd(ball);
expect(ball.body.bodyType, equals(BodyType.dynamic));
},
);
group('can be moved', () {
flameTester.test('by its weight', (game) async {
final ball = Ball();
await game.ensureAdd(ball);
game.update(1);
expect(ball.body.position, isNot(equals(ball.initialPosition)));
});
flameTester.test('by applying velocity', (game) async {
final ball = Ball();
await game.ensureAdd(ball);
ball.body.gravityScale = 0;
ball.body.linearVelocity.setValues(10, 10);
game.update(1);
expect(ball.body.position, isNot(equals(ball.initialPosition)));
});
});
});
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final ball = Ball();
await game.ensureAdd(ball);
expect(ball.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'is dense',
(game) async {
final ball = Ball();
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
expect(fixture.density, greaterThan(0));
},
);
flameTester.test(
'shape is circular',
(game) async {
final ball = Ball();
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(1));
},
);
flameTester.test(
'has Layer.all as default filter maskBits',
(game) async {
final ball = Ball();
await game.ready();
await game.ensureAdd(ball);
await game.ready();
final fixture = ball.body.fixtures[0];
expect(fixture.filterData.maskBits, equals(Layer.board.maskBits));
},
);
});
group('lost', () { group('lost', () {
late GameBloc gameBloc; late GameBloc gameBloc;
@ -124,7 +31,7 @@ void main() {
(game, tester) async { (game, tester) async {
await game.ready(); await game.ready();
game.children.whereType<Ball>().first.lost(); game.children.whereType<Ball>().first.controller.lost();
await tester.pump(); await tester.pump();
verify(() => gameBloc.add(const BallLost())).called(1); verify(() => gameBloc.add(const BallLost())).called(1);
@ -136,7 +43,7 @@ void main() {
(game, tester) async { (game, tester) async {
await game.ready(); await game.ready();
game.children.whereType<Ball>().first.lost(); game.children.whereType<Ball>().first.controller.lost();
await game.ready(); // Making sure that all additions are done await game.ready(); // Making sure that all additions are done
expect( expect(
@ -162,7 +69,7 @@ void main() {
); );
await game.ready(); await game.ready();
game.children.whereType<Ball>().first.lost(); game.children.whereType<Ball>().first.controller.lost();
await tester.pump(); await tester.pump();
expect( expect(
@ -172,60 +79,5 @@ void main() {
}, },
); );
}); });
group('stop', () {
group("can't be moved", () {
flameTester.test('by its weight', (game) async {
final ball = Ball();
await game.ensureAdd(ball);
ball.stop();
game.update(1);
expect(ball.body.position, equals(ball.initialPosition));
});
});
flameTester.test('by applying velocity', (game) async {
final ball = Ball();
await game.ensureAdd(ball);
ball.stop();
ball.body.linearVelocity.setValues(10, 10);
game.update(1);
expect(ball.body.position, equals(ball.initialPosition));
});
});
group('resume', () {
group('can move', () {
flameTester.test(
'by its weight when previously stopped',
(game) async {
final ball = Ball();
await game.ensureAdd(ball);
ball.stop();
ball.resume();
game.update(1);
expect(ball.body.position, isNot(equals(ball.initialPosition)));
},
);
flameTester.test(
'by applying velocity when previously stopped',
(game) async {
final ball = Ball();
await game.ensureAdd(ball);
ball.stop();
ball.resume();
ball.body.gravityScale = 0;
ball.body.linearVelocity.setValues(10, 10);
game.update(1);
expect(ball.body.position, isNot(equals(ball.initialPosition)));
},
);
});
});
}); });
} }

@ -4,9 +4,11 @@ import 'dart:collection';
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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -17,13 +19,14 @@ void main() {
group( group(
'Flipper', 'Flipper',
() { () {
// TODO(alestiago): Add golden tests.
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final leftFlipper = Flipper.fromSide( final leftFlipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
final rightFlipper = Flipper.fromSide( final rightFlipper = Flipper(
side: BoardSide.right, side: BoardSide.right,
); );
await game.ready(); await game.ready();
@ -36,13 +39,13 @@ void main() {
group('constructor', () { group('constructor', () {
test('sets BoardSide', () { test('sets BoardSide', () {
final leftFlipper = Flipper.fromSide( final leftFlipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
expect(leftFlipper.side, equals(leftFlipper.side)); expect(leftFlipper.side, equals(leftFlipper.side));
final rightFlipper = Flipper.fromSide( final rightFlipper = Flipper(
side: BoardSide.right, side: BoardSide.right,
); );
expect(rightFlipper.side, equals(rightFlipper.side)); expect(rightFlipper.side, equals(rightFlipper.side));
@ -53,7 +56,7 @@ void main() {
flameTester.test( flameTester.test(
'is dynamic', 'is dynamic',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
@ -65,7 +68,7 @@ void main() {
flameTester.test( flameTester.test(
'ignores gravity', 'ignores gravity',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
@ -77,10 +80,10 @@ void main() {
flameTester.test( flameTester.test(
'has greater mass than Ball', 'has greater mass than Ball',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
final ball = Ball(); final ball = Ball(baseColor: Colors.white);
await game.ready(); await game.ready();
await game.ensureAddAll([flipper, ball]); await game.ensureAddAll([flipper, ball]);
@ -97,7 +100,7 @@ void main() {
flameTester.test( flameTester.test(
'has three', 'has three',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
@ -109,7 +112,7 @@ void main() {
flameTester.test( flameTester.test(
'has density', 'has density',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
@ -139,7 +142,7 @@ void main() {
late Flipper flipper; late Flipper flipper;
setUp(() { setUp(() {
flipper = Flipper.fromSide( flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
}); });
@ -205,7 +208,7 @@ void main() {
late Flipper flipper; late Flipper flipper;
setUp(() { setUp(() {
flipper = Flipper.fromSide( flipper = Flipper(
side: BoardSide.right, side: BoardSide.right,
); );
}); });
@ -269,159 +272,4 @@ void main() {
}); });
}, },
); );
group('FlipperAnchor', () {
flameTester.test(
'position is at the left of the left Flipper',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.left,
);
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
expect(flipperAnchor.body.position.x, equals(-Flipper.size.x / 2));
},
);
flameTester.test(
'position is at the right of the right Flipper',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.right,
);
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
expect(flipperAnchor.body.position.x, equals(Flipper.size.x / 2));
},
);
});
group('FlipperAnchorRevoluteJointDef', () {
group('initializes with', () {
flameTester.test(
'limits enabled',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.left,
);
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
expect(jointDef.enableLimit, isTrue);
},
);
group('equal upper and lower limits', () {
flameTester.test(
'when Flipper is left',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.left,
);
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
expect(jointDef.lowerAngle, equals(jointDef.upperAngle));
},
);
flameTester.test(
'when Flipper is right',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.right,
);
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
expect(jointDef.lowerAngle, equals(jointDef.upperAngle));
},
);
});
});
group(
'unlocks',
() {
flameTester.test(
'when Flipper is left',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.left,
);
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
final joint = game.world.createJoint(jointDef) as RevoluteJoint;
FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side);
expect(
joint.upperLimit,
isNot(equals(joint.lowerLimit)),
);
},
);
flameTester.test(
'when Flipper is right',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.right,
);
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
final joint = game.world.createJoint(jointDef) as RevoluteJoint;
FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side);
expect(
joint.upperLimit,
isNot(equals(joint.lowerLimit)),
);
},
);
},
);
});
} }

@ -165,6 +165,42 @@ void main() {
}); });
}); });
group('ellipse', () {
flameTester.test(
'loads correctly',
(game) async {
final pathway = Pathway.ellipse(
center: Vector2.zero(),
width: width,
majorRadius: 150,
minorRadius: 70,
);
await game.ready();
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final pathway = Pathway.ellipse(
center: Vector2.zero(),
width: width,
majorRadius: 150,
minorRadius: 70,
);
await game.ready();
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
});
group('bezier curve', () { group('bezier curve', () {
final controlPoints = [ final controlPoints = [
Vector2(0, 0), Vector2(0, 0),

@ -4,6 +4,7 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart'; import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';

@ -1,9 +1,11 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
class MockBall extends Mock implements Ball {} import '../../helpers/helpers.dart';
class MockGameBloc extends Mock implements GameBloc {} class MockGameBloc extends Mock implements GameBloc {}
@ -28,12 +30,16 @@ void main() {
late PinballGame game; late PinballGame game;
late GameBloc bloc; late GameBloc bloc;
late Ball ball; late Ball ball;
late ComponentSet componentSet;
late BallController ballController;
late FakeScorePoints fakeScorePoints; late FakeScorePoints fakeScorePoints;
setUp(() { setUp(() {
game = MockPinballGame(); game = MockPinballGame();
bloc = MockGameBloc(); bloc = MockGameBloc();
ball = MockBall(); ball = MockBall();
componentSet = MockComponentSet();
ballController = MockBallController();
fakeScorePoints = FakeScorePoints(); fakeScorePoints = FakeScorePoints();
}); });
@ -45,7 +51,10 @@ void main() {
test( test(
'emits Scored event with points', 'emits Scored event with points',
() { () {
when<PinballGame>(() => ball.gameRef).thenReturn(game); when(() => componentSet.whereType<BallController>())
.thenReturn([ballController]);
when(() => ball.children).thenReturn(componentSet);
when<Forge2DGame>(() => ballController.gameRef).thenReturn(game);
when<GameBloc>(game.read).thenReturn(bloc); when<GameBloc>(game.read).thenReturn(bloc);
BallScorePointsCallback().begin( BallScorePointsCallback().begin(

@ -2,6 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';

@ -17,11 +17,14 @@ void main() {
test( test(
'removes the ball on begin contact when the wall is a bottom one', 'removes the ball on begin contact when the wall is a bottom one',
() { () {
final game = MockPinballGame();
final wall = MockBottomWall(); final wall = MockBottomWall();
final ballController = MockBallController();
final ball = MockBall(); final ball = MockBall();
final componentSet = MockComponentSet();
when(() => ball.gameRef).thenReturn(game); when(() => componentSet.whereType<BallController>())
.thenReturn([ballController]);
when(() => ball.children).thenReturn(componentSet);
BottomWallBallContactCallback() BottomWallBallContactCallback()
// Remove once https://github.com/flame-engine/flame/pull/1415 // Remove once https://github.com/flame-engine/flame/pull/1415
@ -29,7 +32,7 @@ void main() {
..end(MockBall(), MockBottomWall(), MockContact()) ..end(MockBall(), MockBottomWall(), MockContact())
..begin(ball, wall, MockContact()); ..begin(ball, wall, MockContact());
verify(ball.lost).called(1); verify(ballController.lost).called(1);
}, },
); );
}); });

@ -5,6 +5,7 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../helpers/helpers.dart'; import '../helpers/helpers.dart';

@ -3,9 +3,11 @@ import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/theme/theme.dart'; import 'package:pinball/theme/theme.dart';
import 'package:pinball_components/pinball_components.dart';
class MockPinballGame extends Mock implements PinballGame {} class MockPinballGame extends Mock implements PinballGame {}
@ -17,6 +19,8 @@ class MockBody extends Mock implements Body {}
class MockBall extends Mock implements Ball {} class MockBall extends Mock implements Ball {}
class MockBallController extends Mock implements BallController {}
class MockContact extends Mock implements Contact {} class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock class MockContactCallback extends Mock
@ -33,6 +37,8 @@ class MockGameState extends Mock implements GameState {}
class MockThemeCubit extends Mock implements ThemeCubit {} class MockThemeCubit extends Mock implements ThemeCubit {}
class MockLeaderboardRepository extends Mock implements LeaderboardRepository {}
class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
@ -62,3 +68,5 @@ class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {}
class MockSpaceshipHole extends Mock implements SpaceshipHole {} class MockSpaceshipHole extends Mock implements SpaceshipHole {}
class MockComponent extends Mock implements Component {} class MockComponent extends Mock implements Component {}
class MockComponentSet extends Mock implements ComponentSet {}

@ -5,8 +5,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball_theme/pinball_theme.dart';
class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} import '../../helpers/helpers.dart';
void main() { void main() {
group('LeaderboardBloc', () { group('LeaderboardBloc', () {
@ -42,7 +43,7 @@ void main() {
final top10Leaderboard = top10Scores final top10Leaderboard = top10Scores
.map( .map(
(score) => LeaderboardEntry( (score) => LeaderboardEntryData(
playerInitials: 'user$score', playerInitials: 'user$score',
score: score, score: score,
character: CharacterType.dash, character: CharacterType.dash,
@ -101,7 +102,7 @@ void main() {
}); });
group('LeaderboardEntryAdded', () { group('LeaderboardEntryAdded', () {
final leaderboardEntry = LeaderboardEntry( final leaderboardEntry = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 1500, score: 1500,
character: CharacterType.dash, character: CharacterType.dash,
@ -163,4 +164,40 @@ void main() {
); );
}); });
}); });
group('CharacterTypeX', () {
test('converts CharacterType.android to AndroidTheme', () {
expect(CharacterType.android.toTheme, equals(AndroidTheme()));
});
test('converts CharacterType.dash to DashTheme', () {
expect(CharacterType.dash.toTheme, equals(DashTheme()));
});
test('converts CharacterType.dino to DinoTheme', () {
expect(CharacterType.dino.toTheme, equals(DinoTheme()));
});
test('converts CharacterType.sparky to SparkyTheme', () {
expect(CharacterType.sparky.toTheme, equals(SparkyTheme()));
});
});
group('CharacterThemeX', () {
test('converts AndroidTheme to CharacterType.android', () {
expect(AndroidTheme().toType, equals(CharacterType.android));
});
test('converts DashTheme to CharacterType.dash', () {
expect(DashTheme().toType, equals(CharacterType.dash));
});
test('converts DinoTheme to CharacterType.dino', () {
expect(DinoTheme().toType, equals(CharacterType.dino));
});
test('converts SparkyTheme to CharacterType.sparky', () {
expect(SparkyTheme().toType, equals(CharacterType.sparky));
});
});
} }

@ -20,7 +20,7 @@ void main() {
}); });
group('LeaderboardEntryAdded', () { group('LeaderboardEntryAdded', () {
const leaderboardEntry = LeaderboardEntry( const leaderboardEntry = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 1500, score: 1500,
character: CharacterType.dash, character: CharacterType.dash,

@ -3,6 +3,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() { void main() {
group('LeaderboardState', () { group('LeaderboardState', () {
@ -25,10 +26,11 @@ void main() {
}); });
group('copyWith', () { group('copyWith', () {
const leaderboardEntry = LeaderboardEntry( final leaderboardEntry = LeaderboardEntry(
rank: '1',
playerInitials: 'ABC', playerInitials: 'ABC',
score: 1500, score: 1500,
character: CharacterType.dash, character: DashTheme().characterAsset,
); );
test( test(
@ -51,7 +53,7 @@ void main() {
final otherLeaderboardState = LeaderboardState( final otherLeaderboardState = LeaderboardState(
status: LeaderboardStatus.success, status: LeaderboardStatus.success,
ranking: LeaderboardRanking(ranking: 0, outOf: 0), ranking: LeaderboardRanking(ranking: 0, outOf: 0),
leaderboard: const [leaderboardEntry], leaderboard: [leaderboardEntry],
); );
expect(leaderboardState, isNot(equals(otherLeaderboardState))); expect(leaderboardState, isNot(equals(otherLeaderboardState)));

Loading…
Cancel
Save