feat: improved extra ball logic (#151)

pull/162/head
Alejandro Santiago 4 years ago committed by GitHub
parent 254c38d2a4
commit 655007b2d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -33,7 +33,7 @@ abstract class ComponentController<T extends Component> extends Component {
/// Mixin that attaches a single [ComponentController] to a [Component].
mixin Controls<T extends ComponentController> on Component {
/// The [ComponentController] attached to this [Component].
late final T controller;
late T controller;
@override
@mustCallSuper

@ -19,10 +19,8 @@ class GameBloc extends Bloc<GameEvent, GameState> {
static const bonusWordScore = 10000;
void _onBallLost(BallLost event, Emitter emit) {
if (state.balls > 0) {
emit(state.copyWith(balls: state.balls - 1));
}
}
void _onScored(Scored event, Emitter emit) {
if (!state.isGameOver) {
@ -36,7 +34,8 @@ class GameBloc extends Bloc<GameEvent, GameState> {
event.letterIndex,
];
if (newBonusLetters.length == bonusWord.length) {
final achievedBonus = newBonusLetters.length == bonusWord.length;
if (achievedBonus) {
emit(
state.copyWith(
activatedBonusLetters: [],
@ -55,15 +54,16 @@ class GameBloc extends Bloc<GameEvent, GameState> {
}
void _onDashNestActivated(DashNestActivated event, Emitter emit) {
const nestsRequiredForBonus = 3;
final newNests = {
...state.activatedDashNests,
event.nestId,
};
if (newNests.length == nestsRequiredForBonus) {
final achievedBonus = newNests.length == 3;
if (achievedBonus) {
emit(
state.copyWith(
balls: state.balls + 1,
activatedDashNests: {},
bonusHistory: [
...state.bonusHistory,

@ -5,11 +5,10 @@ part of 'game_bloc.dart';
/// Defines bonuses that a player can gain during a PinballGame.
enum GameBonus {
/// Bonus achieved when the user activate all of the bonus
/// letters on the board, forming the bonus word
/// letters on the board, forming the bonus word.
word,
/// Bonus achieved when the user activates all of the Dash
/// nests on the board, adding a new ball to the board.
/// Bonus achieved when the user activates all dash nest bumpers.
dashNest,
}

@ -1,5 +1,4 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.dart';
@ -18,7 +17,7 @@ class ControlledBall extends Ball with Controls<BallController> {
ControlledBall.launch({
required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) {
controller = LaunchedBallController(this);
controller = BallController(this);
}
/// {@template bonus_ball}
@ -29,74 +28,43 @@ class ControlledBall extends Ball with Controls<BallController> {
ControlledBall.bonus({
required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) {
controller = BonusBallController(this);
controller = BallController(this);
}
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = BonusBallController(this);
controller = DebugBallController(this);
}
}
/// {@template ball_controller}
/// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate}
abstract class BallController extends ComponentController<Ball> {
class BallController extends ComponentController<Ball>
with HasGameRef<PinballGame> {
/// {@macro ball_controller}
BallController(Ball ball) : super(ball);
/// Removes the [Ball] from a [PinballGame].
///
/// {@template ball_controller_lost}
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
/// {@endtemplate}
void lost();
}
/// {@template bonus_ball_controller}
/// {@macro ball_controller}
///
/// A [BonusBallController] doesn't change the [GameState.balls] count.
/// {@endtemplate}
class BonusBallController extends BallController {
/// {@macro bonus_ball_controller}
BonusBallController(Ball<Forge2DGame> component) : super(component);
@override
void lost() {
component.shouldRemove = true;
}
}
/// {@template launched_ball_controller}
/// {@macro ball_controller}
///
/// A [LaunchedBallController] changes the [GameState.balls] count.
/// {@endtemplate}
class LaunchedBallController extends BallController
with HasGameRef<PinballGame>, BlocComponent<GameBloc, GameState> {
/// {@macro launched_ball_controller}
LaunchedBallController(Ball<Forge2DGame> ball) : super(ball);
@override
bool listenWhen(GameState? previousState, GameState newState) {
return (previousState?.balls ?? 0) > newState.balls;
void onRemove() {
super.onRemove();
gameRef.read<GameBloc>().add(const BallLost());
}
}
@override
void onNewState(GameState state) {
super.onNewState(state);
component.shouldRemove = true;
if (state.balls > 0) gameRef.spawnBall();
}
/// {@macro ball_controller}
class DebugBallController extends BallController {
/// {@macro ball_controller}
DebugBallController(Ball<Forge2DGame> component) : super(component);
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left.
///
/// {@macro ball_controller_lost}
@override
void lost() {
gameRef.read<GameBloc>().add(const BallLost());
}
void onRemove() {}
}

@ -71,12 +71,12 @@ class BottomWall extends Wall {
}
/// {@template bottom_wall_ball_contact_callback}
/// Listens when a [Ball] falls into a [BottomWall].
/// Listens when a [ControlledBall] falls into a [BottomWall].
/// {@endtemplate}
class BottomWallBallContactCallback extends ContactCallback<Ball, BottomWall> {
class BottomWallBallContactCallback
extends ContactCallback<ControlledBall, BottomWall> {
@override
void begin(Ball ball, BottomWall wall, Contact contact) {
// TODO(alestiago): replace with .firstChild when available.
ball.children.whereType<BallController>().first.lost();
void begin(ControlledBall ball, BottomWall wall, Contact contact) {
ball.controller.lost();
}
}

@ -5,6 +5,7 @@ import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart';
@ -12,29 +13,38 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class PinballGame extends Forge2DGame
with FlameBloc, HasKeyboardHandlerComponents {
PinballGame({required this.theme, required this.audio}) {
with
FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController> {
PinballGame({
required this.theme,
required this.audio,
}) {
images.prefix = '';
controller = _GameBallsController(this);
}
final PinballTheme theme;
final PinballAudio audio;
@override
void onAttach() {
super.onAttach();
spawnBall();
}
@override
Future<void> onLoad() async {
_addContactCallbacks();
// Fix camera on the center of the board.
camera
..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16;
await _addGameBoundaries();
unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp()));
unawaited(_addPlunger());
final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19);
await add(plunger);
unawaited(add(Board()));
unawaited(addFromBlueprint(DinoWalls()));
unawaited(_addBonusWord());
@ -52,10 +62,8 @@ class PinballGame extends Forge2DGame
),
);
// Fix camera on the center of the board.
camera
..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16;
controller.attachTo(plunger);
await super.onLoad();
}
void _addContactCallbacks() {
@ -69,12 +77,6 @@ class PinballGame extends Forge2DGame
createBoundaries(this).forEach(add);
}
Future<void> _addPlunger() async {
final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19);
await add(plunger);
}
Future<void> _addBonusWord() async {
await add(
BonusWord(
@ -85,13 +87,49 @@ class PinballGame extends Forge2DGame
),
);
}
}
class _GameBallsController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
_GameBallsController(PinballGame game) : super(game);
late final Plunger _plunger;
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
final canBallRespawn = newState.balls > 0;
return noBallsLeft && canBallRespawn;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
_spawnBall();
}
@override
Future<void> onLoad() async {
await super.onLoad();
_spawnBall();
}
Future<void> spawnBall() async {
// TODO(alestiago): Remove once this logic is moved to controller.
void _spawnBall() {
final ball = ControlledBall.launch(
theme: theme,
)..initialPosition = Vector2(38, -19 + Ball.size.y);
await add(ball);
theme: gameRef.theme,
)..initialPosition = Vector2(
_plunger.body.position.x,
_plunger.body.position.y + Ball.size.y,
);
component.add(ball);
}
/// Attaches the controller to the plunger.
// TODO(alestiago): Remove this method and use onLoad instead.
// ignore: use_setters_to_change_properties
void attachTo(Plunger plunger) {
_plunger = plunger;
}
}
@ -102,7 +140,9 @@ class DebugPinballGame extends PinballGame with TapDetector {
}) : super(
theme: theme,
audio: audio,
);
) {
controller = _DebugGameBallsController(this);
}
@override
Future<void> onLoad() async {
@ -134,3 +174,19 @@ class DebugPinballGame extends PinballGame with TapDetector {
);
}
}
class _DebugGameBallsController extends _GameBallsController {
_DebugGameBallsController(PinballGame game) : super(game);
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component
.descendants()
.whereType<ControlledBall>()
.where((ball) => ball.controller is! DebugBallController)
.isEmpty;
final canBallRespawn = newState.balls > 0;
return noBallsLeft && canBallRespawn;
}
}

@ -12,13 +12,10 @@ void main() {
group('LostBall', () {
blocTest<GameBloc, GameState>(
"doesn't decrease ball "
'when no balls left',
'decreases number of balls',
build: GameBloc.new,
act: (bloc) {
for (var i = 0; i <= bloc.state.balls; i++) {
bloc.add(const BallLost());
}
},
expect: () => [
const GameState(
@ -28,20 +25,6 @@ void main() {
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 1,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
],
);
});
@ -230,7 +213,7 @@ void main() {
),
GameState(
score: 0,
balls: 3,
balls: 4,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],

@ -13,42 +13,12 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('BonusBallController', () {
late Ball ball;
setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
});
test('can be instantiated', () {
expect(
BonusBallController(ball),
isA<BonusBallController>(),
);
});
flameTester.test(
'lost removes ball',
(game) async {
await game.add(ball);
final controller = BonusBallController(ball);
await ball.ensureAdd(controller);
controller.lost();
await game.ready();
expect(game.contains(ball), isFalse);
},
);
});
group('LaunchedBallController', () {
group('BallController', () {
test('can be instantiated', () {
expect(
LaunchedBallController(MockBall()),
isA<LaunchedBallController>(),
BallController(MockBall()),
isA<BallController>(),
);
});
@ -74,7 +44,7 @@ void main() {
flameBlocTester.testGameWidget(
'lost adds BallLost to GameBloc',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
final controller = BallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
@ -84,114 +54,6 @@ void main() {
verify(() => gameBloc.add(const BallLost())).called(1);
},
);
group('listenWhen', () {
flameBlocTester.testGameWidget(
'listens when a ball has been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(2);
expect(controller.listenWhen(previousState, newState), isTrue);
},
);
flameBlocTester.testGameWidget(
'does not listen when a ball has not been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(3);
expect(controller.listenWhen(previousState, newState), isFalse);
},
);
});
group('onNewState', () {
flameBlocTester.testGameWidget(
'removes ball',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(1);
controller.onNewState(state);
await game.ready();
},
verify: (game, tester) async {
expect(game.contains(ball), isFalse);
},
);
flameBlocTester.testGameWidget(
'spawns a new ball when the ball is not the last one',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(1);
final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state);
await game.ready();
final currentBalls = game.descendants().whereType<Ball>().toList();
expect(currentBalls.contains(ball), isFalse);
expect(currentBalls.length, equals(previousBalls.length));
},
);
flameBlocTester.testGameWidget(
'does not spawn a new ball is the last one',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(0);
final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state);
await game.ready();
final currentBalls = game.descendants().whereType<Ball>();
expect(currentBalls.contains(ball), isFalse);
expect(
currentBalls.length,
equals((previousBalls..remove(ball)).length),
);
},
);
});
});
});
}

@ -1,7 +1,6 @@
// 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/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@ -11,18 +10,6 @@ import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) {
assert(
bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty,
'Bodies require fixtures to contact each other.',
);
final fixtureA = bodyA.body.fixtures.first;
final fixtureB = bodyB.body.fixtures.first;
final contact = Contact.init(fixtureA, 0, fixtureB, 0);
game.world.contactManager.contactListener?.beginContact(contact);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
@ -92,7 +79,7 @@ void main() {
);
flameBlocTester.testGameWidget(
'listens when a Bonus.dashNest is added',
'listens when a Bonus.dashNest and a bonusBall is added',
verify: (game, tester) async {
final flutterForest = FlutterForest();
@ -103,6 +90,7 @@ void main() {
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
);
expect(
flutterForest.controller
.listenWhen(const GameState.initial(), state),

@ -3,40 +3,15 @@
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 '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('Wall', () {
group('BottomWallBallContactCallback', () {
test(
'removes the ball on begin contact when the wall is a bottom one',
() {
final wall = MockBottomWall();
final ballController = MockBallController();
final ball = MockBall();
final componentSet = MockComponentSet();
when(() => componentSet.whereType<BallController>())
.thenReturn([ballController]);
when(() => ball.children).thenReturn(componentSet);
BottomWallBallContactCallback()
// Remove once https://github.com/flame-engine/flame/pull/1415
// is merged
..end(MockBall(), MockBottomWall(), MockContact())
..begin(ball, wall, MockContact());
verify(ballController.lost).called(1);
},
);
});
flameTester.test(
'loads correctly',
(game) async {
@ -123,4 +98,67 @@ void main() {
);
});
});
group(
'BottomWall',
() {
group('removes ball on contact', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'when ball is launch',
setUp: (game, tester) async {
final ball = ControlledBall.launch(theme: game.theme);
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
flameBlocTester.testGameWidget(
'when ball is bonus',
setUp: (game, tester) async {
final ball = ControlledBall.bonus(theme: game.theme);
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
flameTester.test(
'when ball is debug',
(game) async {
final ball = ControlledBall.debug();
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
});
},
);
}

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@ -10,11 +11,11 @@ import 'package:pinball_components/pinball_components.dart';
import '../helpers/helpers.dart';
void main() {
group('PinballGame', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.new);
final debugModeFlameTester = FlameTester(DebugPinballGameTest.new);
group('PinballGame', () {
// TODO(alestiago): test if [PinballGame] registers
// [BallScorePointsCallback] once the following issue is resolved:
// https://github.com/flame-engine/flame/issues/1416
@ -60,8 +61,106 @@ void main() {
equals(1),
);
});
group('controller', () {
// TODO(alestiago): Write test to be controller agnostic.
group('listenWhen', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'listens when all balls are lost and there are more than 0 balls',
setUp: (game, tester) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(2);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
await game.ready();
expect(
game.controller.listenWhen(MockGameState(), newState),
isTrue,
);
},
);
flameTester.test(
"doesn't listen when some balls are left",
(game) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(1);
expect(
game.descendants().whereType<Ball>().length,
greaterThan(0),
);
expect(
game.controller.listenWhen(MockGameState(), newState),
isFalse,
);
},
);
flameBlocTester.test(
"doesn't listen when no balls left",
(game) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(0);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
await game.ready();
expect(
game.descendants().whereType<Ball>().isEmpty,
isTrue,
);
expect(
game.controller.listenWhen(MockGameState(), newState),
isFalse,
);
},
);
});
group(
'onNewState',
() {
flameTester.test(
'spawns a ball',
(game) async {
await game.ready();
final previousBalls =
game.descendants().whereType<Ball>().toList();
game.controller.onNewState(MockGameState());
await game.ready();
final currentBalls =
game.descendants().whereType<Ball>().toList();
expect(
currentBalls.length,
equals(previousBalls.length + 1),
);
},
);
},
);
});
});
});
group('DebugPinballGame', () {
debugModeFlameTester.test('adds a ball on tap up', (game) async {
await game.ready();
@ -71,12 +170,46 @@ void main() {
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
final previousBalls = game.descendants().whereType<Ball>().toList();
game.onTapUp(tapUpEvent);
await game.ready();
expect(
game.children.whereType<Ball>().length,
equals(1),
equals(previousBalls.length + 1),
);
});
group('controller', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final debugModeFlameBlocTester =
FlameBlocTester<DebugPinballGame, GameBloc>(
gameBuilder: DebugPinballGameTest.new,
blocBuilder: () => gameBloc,
);
debugModeFlameBlocTester.testGameWidget(
'ignores debug balls',
setUp: (game, tester) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(1);
await game.ready();
game.children.removeWhere((component) => component is Ball);
await game.ready();
await game.ensureAdd(ControlledBall.debug());
expect(
game.controller.listenWhen(MockGameState(), newState),
isTrue,
);
},
);
});
});

@ -1,3 +1,5 @@
// ignore_for_file: must_call_super
import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart';

@ -0,0 +1,13 @@
import 'package:flame_forge2d/flame_forge2d.dart';
void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) {
assert(
bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty,
'Bodies require fixtures to contact each other.',
);
final fixtureA = bodyA.body.fixtures.first;
final fixtureB = bodyB.body.fixtures.first;
final contact = Contact.init(fixtureA, 0, fixtureB, 0);
game.world.contactManager.contactListener?.beginContact(contact);
}

@ -7,6 +7,7 @@
export 'builders.dart';
export 'extensions.dart';
export 'fakes.dart';
export 'forge2d.dart';
export 'key_testers.dart';
export 'mocks.dart';
export 'navigator.dart';

@ -21,6 +21,8 @@ class MockBody extends Mock implements Body {}
class MockBall extends Mock implements Ball {}
class MockControlledBall extends Mock implements ControlledBall {}
class MockBallController extends Mock implements BallController {}
class MockContact extends Mock implements Contact {}

Loading…
Cancel
Save