refactor: splitting `Ball` business from UI logic (#86)

* feat: refactoring ball component to separate business from ui logic

* fix: tests

* fix: lint

* fix: test coverage

* Update lib/game/components/ball.dart

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

Co-authored-by: Alejandro Santiago <dev@alestiago.com>
pull/91/head
Erick 4 years ago committed by GitHub
parent 394acd1802
commit c8782cfbf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -16,4 +16,4 @@ jobs:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
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
/// that are related, but they need to be added directly on
/// the [FlameGame] level.
abstract class Blueprint {
abstract class Blueprint<T extends FlameGame> {
final List<Component> _components = [];
bool _isAttached = false;
/// Called before the the [Component]s managed
/// by this blueprint is added to the [FlameGame]
void build();
void build(T gameRef);
/// Attach the [Component]s built on [build] to the [game]
/// instance
@mustCallSuper
Future<void> attach(FlameGame game) async {
build();
Future<void> attach(T game) async {
build(game);
await game.addAll(_components);
_isAttached = true;
}
@ -47,7 +47,7 @@ abstract class Blueprint {
/// A [Blueprint] that provides additional
/// structures specific to flame_forge2d
abstract class Forge2DBlueprint extends Blueprint {
abstract class Forge2DBlueprint extends Blueprint<Forge2DGame> {
final List<ContactCallback> _callbacks = [];
/// Adds a single [ContactCallback] to this blueprint
@ -63,13 +63,11 @@ abstract class Forge2DBlueprint extends Blueprint {
}
@override
Future<void> attach(FlameGame game) async {
Future<void> attach(Forge2DGame game) async {
await super.attach(game);
assert(game is Forge2DGame, 'Forge2DBlueprint used outside a Forge2DGame');
for (final callback in _callbacks) {
(game as Forge2DGame).addContactCallback(callback);
game.addContactCallback(callback);
}
}

@ -1,61 +1,40 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the
/// [PinballGame].
/// {@template ball_blueprint}
/// [Blueprint] which cretes a ball game object
/// {@endtemplate}
class Ball extends BodyComponent<PinballGame> with InitialPosition, Layered {
/// {@macro ball}
Ball() {
// TODO(ruimiguel): while developing Ball can be launched by clicking mouse,
// and default layer is Layer.all. But on final game Ball will be always be
// be launched from Plunger and LauncherRamp will modify it to Layer.board.
// We need to see what happens if Ball appears from other place like nest
// bumper, it will need to explicit change layer to Layer.board then.
layer = Layer.board;
}
/// The size of the [Ball]
final Vector2 size = Vector2.all(2);
class BallBlueprint extends Blueprint<PinballGame> {
/// {@macro ball_blueprint}
BallBlueprint({required this.position});
@override
Future<void> onLoad() async {
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),
);
}
/// The initial position of the [Ball]
final Vector2 position;
@override
Body createBody() {
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(shape)..density = 1;
void build(PinballGame gameRef) {
final baseColor = gameRef.theme.characterTheme.ballColor;
final ball = Ball(baseColor: baseColor)..add(BallController());
final bodyDef = BodyDef()
..position = initialPosition
..userData = this
..type = BodyType.dynamic;
return world.createBody(bodyDef)..createFixture(fixtureDef);
add(ball..initialPosition = position + Vector2(0, ball.size.y / 2));
}
}
/// {@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
/// any are left.
///
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
void lost() {
shouldRemove = true;
parent?.shouldRemove = true;
final bloc = gameRef.read<GameBloc>()..add(const BallLost());
@ -64,19 +43,13 @@ class Ball extends BodyComponent<PinballGame> with InitialPosition, Layered {
gameRef.spawnBall();
}
}
}
/// 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);
/// Adds helper methods to the [Ball]
extension BallX on Ball {
/// Returns the controller instance of the ball
// TODO(erickzanardo): Remove the need of an extension.
BallController get controller {
return children.whereType<BallController>().first;
}
}

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

@ -1,5 +1,6 @@
import 'package:flame/components.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template board}
/// The main flat surface of the [PinballGame], where the [Flipper]s,

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

@ -4,12 +4,10 @@ export 'board.dart';
export 'board_side.dart';
export 'bonus_word.dart';
export 'flipper.dart';
export 'initial_position.dart';
export 'jetpack_ramp.dart';
export 'joint_anchor.dart';
export 'kicker.dart';
export 'launcher_ramp.dart';
export 'layer.dart';
export 'pathway.dart';
export 'plunger.dart';
export 'ramp_opening.dart';

@ -6,6 +6,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
const _leftFlipperKeys = [
LogicalKeyboardKey.arrowLeft,

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

@ -1,5 +1,5 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template joint_anchor}
/// 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:geometry/geometry.dart' as geometry show centroid;
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template kicker}
/// 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_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template launcher_ramp}
/// The yellow left ramp, where the [Ball] goes through when launched from the

@ -2,7 +2,7 @@ import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:geometry/geometry.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template pathway}
/// [Pathway] creates lines of various shapes.

@ -2,6 +2,7 @@ import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template plunger}
/// [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:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template ramp_orientation}
/// 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:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template round_bumper}
/// Circular body that repels a [Ball] on contact, increasing the score.

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

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

@ -4,6 +4,7 @@ import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/components/components.dart';
import 'package:pinball/game/pinball_game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template wall}
/// 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> {
@override
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/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' as components;
/// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame {
/// Pre load the initial assets of the game.
Future<void> preLoadAssets() async {
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.spaceship.androidTop.path),
images.load(Assets.images.components.spaceship.androidBottom.path),

@ -102,11 +102,7 @@ class PinballGame extends Forge2DGame
}
void spawnBall() {
final ball = Ball();
add(
ball
..initialPosition = plunger.body.position + Vector2(0, ball.size.y / 2),
);
addFromBlueprint(BallBlueprint(position: plunger.body.position));
}
}
@ -115,8 +111,6 @@ class DebugPinballGame extends PinballGame with TapDetector {
@override
void onTapUp(TapUpInfo info) {
add(
Ball()..initialPosition = info.eventPosition.game,
);
addFromBlueprint(BallBlueprint(position: info.eventPosition.game));
}
}

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

@ -1 +1,4 @@
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;
export 'gen/assets.gen.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}
/// Package with the UI game components for the Pinball Game
/// {@endtemplate}
class PinballComponents {
/// {@macro pinball_components}
const PinballComponents();
}
export 'components/components.dart';

@ -7,10 +7,24 @@ environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
flame: ^1.1.0-releasecandidate.6
flame_forge2d: ^0.9.0-releasecandidate.6
flutter:
sdk: flutter
dev_dependencies:
flame_test: ^1.1.0
flutter_test:
sdk: flutter
mocktail: ^0.2.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_test/flame_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 {
@override

@ -4,7 +4,7 @@ import 'dart:math' as math;
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
class TestBodyComponent extends BodyComponent with Layered {
@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"
source: hosted
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:
dependency: "direct main"
description:

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

@ -1,5 +1,4 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/flame/blueprint.dart';
@ -9,7 +8,7 @@ import '../helpers/helpers.dart';
class MyBlueprint extends Blueprint {
@override
void build() {
void build(_) {
add(Component());
addAll([Component(), Component()]);
}
@ -17,7 +16,7 @@ class MyBlueprint extends Blueprint {
class MyForge2dBlueprint extends Forge2DBlueprint {
@override
void build() {
void build(_) {
addContactCallback(MockContactCallback());
addAllContactCallback([MockContactCallback(), MockContactCallback()]);
}
@ -26,7 +25,7 @@ class MyForge2dBlueprint extends Forge2DBlueprint {
void main() {
group('Blueprint', () {
test('components can be added to it', () {
final blueprint = MyBlueprint()..build();
final blueprint = MyBlueprint()..build(MockPinballGame());
expect(blueprint.components.length, equals(3));
});
@ -59,7 +58,7 @@ void main() {
});
test('callbacks can be added to it', () {
final blueprint = MyForge2dBlueprint()..build();
final blueprint = MyForge2dBlueprint()..build(MockPinballGame());
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
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:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
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', () {
late GameBloc gameBloc;
@ -124,7 +31,7 @@ void main() {
(game, tester) async {
await game.ready();
game.children.whereType<Ball>().first.lost();
game.children.whereType<Ball>().first.controller.lost();
await tester.pump();
verify(() => gameBloc.add(const BallLost())).called(1);
@ -136,7 +43,7 @@ void main() {
(game, tester) async {
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
expect(
@ -162,7 +69,7 @@ void main() {
);
await game.ready();
game.children.whereType<Ball>().first.lost();
game.children.whereType<Ball>().first.controller.lost();
await tester.pump();
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_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
@ -81,7 +83,7 @@ void main() {
final flipper = Flipper(
side: BoardSide.left,
);
final ball = Ball();
final ball = Ball(baseColor: Colors.white);
await game.ready();
await game.ensureAddAll([flipper, ball]);

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

@ -1,9 +1,11 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.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 {}
@ -28,12 +30,16 @@ void main() {
late PinballGame game;
late GameBloc bloc;
late Ball ball;
late ComponentSet componentSet;
late BallController ballController;
late FakeScorePoints fakeScorePoints;
setUp(() {
game = MockPinballGame();
bloc = MockGameBloc();
ball = MockBall();
componentSet = MockComponentSet();
ballController = MockBallController();
fakeScorePoints = FakeScorePoints();
});
@ -45,7 +51,10 @@ void main() {
test(
'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);
BallScorePointsCallback().begin(

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

@ -17,11 +17,14 @@ void main() {
test(
'removes the ball on begin contact when the wall is a bottom one',
() {
final game = MockPinballGame();
final wall = MockBottomWall();
final ballController = MockBallController();
final ball = MockBall();
final componentSet = MockComponentSet();
when(() => ball.gameRef).thenReturn(game);
when(() => componentSet.whereType<BallController>())
.thenReturn([ballController]);
when(() => ball.children).thenReturn(componentSet);
BottomWallBallContactCallback()
// Remove once https://github.com/flame-engine/flame/pull/1415
@ -29,7 +32,7 @@ void main() {
..end(MockBall(), MockBottomWall(), 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:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../helpers/helpers.dart';

@ -1,3 +1,4 @@
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
@ -6,6 +7,7 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_components/pinball_components.dart';
class MockPinballGame extends Mock implements PinballGame {}
@ -17,6 +19,8 @@ class MockBody extends Mock implements Body {}
class MockBall extends Mock implements Ball {}
class MockBallController extends Mock implements BallController {}
class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock
@ -62,3 +66,5 @@ class MockFixture extends Mock implements Fixture {}
class MockSpaceshipEntrance extends Mock implements SpaceshipEntrance {}
class MockSpaceshipHole extends Mock implements SpaceshipHole {}
class MockComponentSet extends Mock implements ComponentSet {}

Loading…
Cancel
Save