refactor: `SparkyComputer` behaviors and removed `ControlledBall` (#365)

* refactor: sparky behaviors and remove controlled ball

* fix: test typo

* fix: PR suggestions
pull/358/head
Allison Ryan 2 years ago committed by alestiago
parent 829dadd3d5
commit 3b5af49756

@ -24,11 +24,13 @@ class BallSpawningBehavior extends Component
final plunger = gameRef.descendants().whereType<Plunger>().single;
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
final characterTheme = readProvider<CharacterTheme>();
final ball = ControlledBall.launch(characterTheme: characterTheme)
final ball = Ball(assetPath: characterTheme.ball.keyName)
..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
)
..layer = Layer.launcher
..zIndex = ZIndexes.ballOnLaunchRamp;
canvas.add(ball);
}

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart';
export 'bottom_group.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart';
@ -12,4 +11,4 @@ export 'google_word/google_word.dart';
export 'launcher.dart';
export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart';
export 'sparky_scorch.dart';
export 'sparky_scorch/sparky_scorch.dart';

@ -1,66 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template controlled_ball}
/// A [Ball] with a [BallController] attached.
///
/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is
/// not over, a new [Ball] will be spawned.
/// {@endtemplate}
class ControlledBall extends Ball with Controls<BallController> {
/// A [Ball] that launches from the [Plunger].
ControlledBall.launch({
required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this);
layer = Layer.launcher;
zIndex = ZIndexes.ballOnLaunchRamp;
}
/// {@macro controlled_ball}
ControlledBall.bonus({
required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this);
zIndex = ZIndexes.ballOnBoard;
}
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super() {
controller = BallController(this);
zIndex = ZIndexes.ballOnBoard;
}
}
/// {@template ball_controller}
/// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate}
class BallController extends ComponentController<Ball>
with FlameBlocReader<GameBloc, GameState> {
/// {@macro ball_controller}
BallController(Ball ball) : super(ball);
/// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge
/// sequence runs, then boosts the ball out of the computer.
Future<void> turboCharge() async {
bloc.add(const SparkyTurboChargeActivated());
component.stop();
// TODO(alestiago): Refactor this hard coded duration once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1564
await Future<void>.delayed(
const Duration(milliseconds: 2583),
);
component.resume();
await component.add(
BallTurboChargingBehavior(impulse: Vector2(40, 110)),
);
}
}

@ -41,10 +41,13 @@ class FlutterForestBonusBehavior extends Component
if (signpost.bloc.isFullyProgressed()) {
bloc.add(const BonusActivated(GameBonus.dashNest));
final characterTheme = readProvider<CharacterTheme>();
canvas.add(
ControlledBall.bonus(
characterTheme: readProvider<CharacterTheme>(),
)..initialPosition = Vector2(29.2, -24.5),
Ball(
assetPath: characterTheme.ball.keyName,
)
..initialPosition = Vector2(29.2, -24.5)
..zIndex = ZIndexes.ballOnBoard,
);
animatronic.playing = true;
signpost.bloc.onProgressed();

@ -0,0 +1 @@
export 'sparky_computer_bonus_behavior.dart';

@ -0,0 +1,28 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.sparkyTurboCharge] when a [Ball] enters the
/// [SparkyComputer].
class SparkyComputerBonusBehavior extends Component
with ParentIsA<SparkyScorch>, FlameBlocReader<GameBloc, GameState> {
@override
void onMount() {
super.onMount();
final sparkyComputer = parent.firstChild<SparkyComputer>()!;
final animatronic = parent.firstChild<SparkyAnimatronic>()!;
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
sparkyComputer.bloc.stream.listen((state) async {
final listenWhen = state == SparkyComputerState.withBall;
if (!listenWhen) return;
bloc.add(const BonusActivated(GameBonus.sparkyTurboCharge));
animatronic.playing = true;
});
}
}

@ -1,9 +1,9 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/components.dart';
import 'package:pinball/game/components/sparky_scorch/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_scorch}
@ -33,51 +33,20 @@ class SparkyScorch extends Component {
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13.2, -49.9),
SparkyAnimatronic()..position = Vector2(-14, -58.2),
SparkyComputer(),
],
);
}
/// {@template sparky_computer_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SparkyComputer].
/// {@endtemplate}
class SparkyComputerSensor extends BodyComponent
with InitialPosition, ContactCallbacks {
/// {@macro sparky_computer_sensor}
SparkyComputerSensor()
: super(
renderBody: false,
children: [
ScoringContactBehavior(points: Points.twentyThousand),
SparkyComputer(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand)
..applyTo(['turbo_charge_sensor']),
],
),
SparkyComputerBonusBehavior(),
],
);
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
1,
0.1,
Vector2.zero(),
-0.18,
);
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.turboCharge();
gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
}
/// Creates [SparkyScorch] without any children.
///
/// This can be used for testing [SparkyScorch]'s behaviors in isolation.
@visibleForTesting
SparkyScorch.test();
}

@ -191,8 +191,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
if (info.raw.kind == PointerDeviceKind.mouse) {
final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()
..initialPosition = info.eventPosition.game;
final ball = Ball()..initialPosition = info.eventPosition.game;
canvas.add(ball);
}
}
@ -219,7 +218,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
void _turboChargeBall(Vector2 line) {
final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()..initialPosition = lineStart!;
final ball = Ball()..initialPosition = lineStart!;
final impulse = line * -1 * 10;
ball.add(BallTurboChargingBehavior(impulse: impulse));
canvas.add(ball);
@ -265,7 +264,7 @@ class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
void render(Canvas canvas) {
final debugText = [
'FPS: ${gameRef.fps().toStringAsFixed(1)}',
'BALLS: ${gameRef.descendants().whereType<ControlledBall>().length}',
'BALLS: ${gameRef.descendants().whereType<Ball>().length}',
].join(' | ');
final height = _debugTextPaint.measureTextHeight(debugText);

@ -36,5 +36,5 @@ export 'spaceship_rail.dart';
export 'spaceship_ramp/spaceship_ramp.dart';
export 'sparky_animatronic.dart';
export 'sparky_bumper/sparky_bumper.dart';
export 'sparky_computer.dart';
export 'sparky_computer/sparky_computer.dart';
export 'z_indexes.dart';

@ -0,0 +1 @@
export 'sparky_computer_sensor_ball_contact_behavior.dart';

@ -0,0 +1,35 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_computer_sensor_ball_contact_behavior}
/// When a [Ball] enters the [SparkyComputer] it is stopped for a period of time
/// before a [BallTurboChargingBehavior] is applied to it.
/// {@endtemplate}
class SparkyComputerSensorBallContactBehavior
extends ContactBehavior<SparkyComputer> {
@override
Future<void> beginContact(Object other, Contact contact) async {
super.beginContact(other, contact);
if (other is! Ball) return;
other.stop();
parent.bloc.onBallEntered();
await parent.add(
TimerComponent(
period: 1.5,
removeOnFinish: true,
onTick: () async {
other.resume();
await other.add(
BallTurboChargingBehavior(
impulse: Vector2(40, 110),
),
);
parent.bloc.onBallTurboCharged();
},
),
);
}
}

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'sparky_computer_state.dart';
class SparkyComputerCubit extends Cubit<SparkyComputerState> {
SparkyComputerCubit() : super(SparkyComputerState.withoutBall);
void onBallEntered() {
emit(SparkyComputerState.withBall);
}
void onBallTurboCharged() {
emit(SparkyComputerState.withoutBall);
}
}

@ -0,0 +1,8 @@
// ignore_for_file: public_member_api_docs
part of 'sparky_computer_cubit.dart';
enum SparkyComputerState {
withoutBall,
withBall,
}

@ -2,31 +2,52 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/sparky_computer/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/sparky_computer_cubit.dart';
/// {@template sparky_computer}
/// A computer owned by Sparky.
/// {@endtemplate}
class SparkyComputer extends Component {
class SparkyComputer extends BodyComponent {
/// {@macro sparky_computer}
SparkyComputer()
: super(
SparkyComputer({Iterable<Component>? children})
: bloc = SparkyComputerCubit(),
super(
renderBody: false,
children: [
_ComputerBase(),
SparkyComputerSensorBallContactBehavior()
..applyTo(['turbo_charge_sensor']),
_ComputerBaseSpriteComponent(),
_ComputerTopSpriteComponent(),
_ComputerGlowSpriteComponent(),
...?children,
],
);
}
class _ComputerBase extends BodyComponent with InitialPosition, ZIndex {
_ComputerBase()
: super(
renderBody: false,
children: [_ComputerBaseSpriteComponent()],
) {
zIndex = ZIndexes.computerBase;
/// Creates a [SparkyComputer] without any children.
///
/// This can be used for testing [SparkyComputer]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting
SparkyComputer.test({
required this.bloc,
Iterable<Component>? children,
}) : super(children: children);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
final SparkyComputerCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
List<FixtureDef> _createFixtureDefs() {
@ -45,30 +66,44 @@ class _ComputerBase extends BodyComponent with InitialPosition, ZIndex {
topEdge.vertex2,
Vector2(-9.4, -47.1),
);
final turboChargeSensor = PolygonShape()
..setAsBox(
1,
0.1,
Vector2(-13.2, -49.9),
-0.18,
);
return [
FixtureDef(leftEdge),
FixtureDef(topEdge),
FixtureDef(rightEdge),
FixtureDef(
turboChargeSensor,
isSensor: true,
userData: 'turbo_charge_sensor',
),
];
}
@override
Body createBody() {
final bodyDef = BodyDef(position: initialPosition);
final body = world.createBody(bodyDef);
final body = world.createBody(BodyDef());
_createFixtureDefs().forEach(body.createFixture);
return body;
}
}
class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef {
class _ComputerBaseSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_ComputerBaseSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-12.44, -48.15),
);
) {
zIndex = ZIndexes.computerBase;
}
@override
Future<void> onLoad() async {

@ -0,0 +1,141 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.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_components/pinball_components.dart';
import 'package:pinball_components/src/components/sparky_computer/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockSparkyComputerCubit extends Mock implements SparkyComputerCubit {}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SparkyComputerSensorBallContactBehavior',
() {
test('can be instantiated', () {
expect(
SparkyComputerSensorBallContactBehavior(),
isA<SparkyComputerSensorBallContactBehavior>(),
);
});
group('beginContact', () {
flameTester.test(
'stops a ball',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
final ball = _MockBall();
await behavior.beginContact(ball, _MockContact());
verify(ball.stop).called(1);
},
);
flameTester.test(
'emits onBallEntered when contacts with a ball',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
await behavior.beginContact(_MockBall(), _MockContact());
verify(sparkyComputer.bloc.onBallEntered).called(1);
},
);
flameTester.test(
'adds TimerComponent when contacts with a ball',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
await behavior.beginContact(_MockBall(), _MockContact());
await game.ready();
expect(
sparkyComputer.firstChild<TimerComponent>(),
isA<TimerComponent>(),
);
},
);
flameTester.test(
'TimerComponent resumes ball and calls onBallTurboCharged onTick',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
final ball = _MockBall();
await behavior.beginContact(ball, _MockContact());
await game.ready();
game.update(
sparkyComputer.firstChild<TimerComponent>()!.timer.limit,
);
await game.ready();
verify(ball.resume).called(1);
verify(sparkyComputer.bloc.onBallTurboCharged).called(1);
},
);
});
},
);
}

@ -0,0 +1,24 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'SparkyComputerCubit',
() {
blocTest<SparkyComputerCubit, SparkyComputerState>(
'onBallEntered emits withBall',
build: SparkyComputerCubit.new,
act: (bloc) => bloc.onBallEntered(),
expect: () => [SparkyComputerState.withBall],
);
blocTest<SparkyComputerCubit, SparkyComputerState>(
'onBallTurboCharged emits withoutBall',
build: SparkyComputerCubit.new,
act: (bloc) => bloc.onBallTurboCharged(),
expect: () => [SparkyComputerState.withoutBall],
);
},
);
}

@ -0,0 +1,93 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/sparky_computer/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
class _MockSparkyComputerCubit extends Mock implements SparkyComputerCubit {}
void main() {
group('SparkyComputer', () {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.sparky.computer.base.keyName,
Assets.images.sparky.computer.top.keyName,
Assets.images.sparky.computer.glow.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('loads correctly', (game) async {
final component = SparkyComputer();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(SparkyComputer());
await tester.pump();
game.camera
..followVector2(Vector2(0, -20))
..zoom = 7;
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('../golden/sparky-computer.png'),
);
},
);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
when(bloc.close).thenAnswer((_) async {});
final sparkyComputer = SparkyComputer.test(bloc: bloc);
await game.ensureAdd(sparkyComputer);
game.remove(sparkyComputer);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final sparkyComputer = SparkyComputer(
children: [component],
);
await game.ensureAdd(sparkyComputer);
expect(sparkyComputer.children, contains(component));
});
flameTester.test('a SparkyComputerSensorBallContactBehavior',
(game) async {
final sparkyComputer = SparkyComputer();
await game.ensureAdd(sparkyComputer);
expect(
sparkyComputer.children
.whereType<SparkyComputerSensorBallContactBehavior>()
.single,
isNotNull,
);
});
});
});
}

@ -1,45 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('SparkyComputer', () {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.sparky.computer.base.keyName,
Assets.images.sparky.computer.top.keyName,
Assets.images.sparky.computer.glow.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('loads correctly', (game) async {
final component = SparkyComputer();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game.ensureAdd(SparkyComputer());
await tester.pump();
game.camera
..followVector2(Vector2(0, -20))
..zoom = 7;
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/sparky-computer.png'),
);
},
);
});
}

@ -1,71 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.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 'package:pinball_theme/pinball_theme.dart' as theme;
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.load(theme.Assets.images.dash.ball.keyName);
}
Future<void> pump(Ball child, {required GameBloc gameBloc}) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
children: [child],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockBall extends Mock implements Ball {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('BallController', () {
late Ball ball;
late GameBloc gameBloc;
setUp(() {
ball = Ball();
gameBloc = _MockGameBloc();
});
final flameBlocTester = FlameTester(_TestGame.new);
test('can be instantiated', () {
expect(
BallController(_MockBall()),
isA<BallController>(),
);
});
flameBlocTester.testGameWidget(
'turboCharge adds TurboChargeActivated',
setUp: (game, tester) async {
await game.onLoad();
final controller = BallController(ball);
await ball.add(controller);
await game.pump(ball, gameBloc: gameBloc);
await controller.turboCharge();
},
verify: (game, tester) async {
verify(() => gameBloc.add(const SparkyTurboChargeActivated()))
.called(1);
},
);
});
}

@ -0,0 +1,86 @@
// ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.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/components/sparky_scorch/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.sparky.computer.top.keyName,
Assets.images.sparky.computer.base.keyName,
Assets.images.sparky.computer.glow.keyName,
Assets.images.sparky.animatronic.keyName,
Assets.images.sparky.bumper.a.lit.keyName,
Assets.images.sparky.bumper.a.dimmed.keyName,
Assets.images.sparky.bumper.b.lit.keyName,
Assets.images.sparky.bumper.b.dimmed.keyName,
Assets.images.sparky.bumper.c.lit.keyName,
Assets.images.sparky.bumper.c.dimmed.keyName,
]);
}
Future<void> pump(
SparkyScorch child, {
required GameBloc gameBloc,
}) async {
// Not needed once https://github.com/flame-engine/flame/issues/1607
// is fixed
await onLoad();
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
children: [child],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('SparkyComputerBonusBehavior', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
final flameTester = FlameTester(_TestGame.new);
flameTester.testGameWidget(
'adds GameBonus.sparkyTurboCharge to the game and plays animatronic '
'when SparkyComputerState.withBall is emitted',
setUp: (game, tester) async {
final behavior = SparkyComputerBonusBehavior();
final parent = SparkyScorch.test();
final sparkyComputer = SparkyComputer();
final animatronic = SparkyAnimatronic();
await parent.addAll([
sparkyComputer,
animatronic,
]);
await game.pump(parent, gameBloc: gameBloc);
await parent.ensureAdd(behavior);
sparkyComputer.bloc.onBallEntered();
await tester.pump();
verify(
() => gameBloc.add(const BonusActivated(GameBonus.sparkyTurboCharge)),
).called(1);
expect(animatronic.playing, isTrue);
},
);
});
}

@ -1,10 +1,11 @@
// ignore_for_file: cascade_invocations
import 'package:flame_bloc/flame_bloc.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/behaviors/behaviors.dart';
import 'package:pinball/game/components/sparky_scorch/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
@ -25,13 +26,16 @@ class _TestGame extends Forge2DGame {
Assets.images.sparky.bumper.c.dimmed.keyName,
]);
}
}
class _MockControlledBall extends Mock implements ControlledBall {}
class _MockBallController extends Mock implements BallController {}
class _MockContact extends Mock implements Contact {}
Future<void> pump(SparkyScorch child) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [child],
),
);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -41,15 +45,18 @@ void main() {
group('SparkyScorch', () {
flameTester.test('loads correctly', (game) async {
final component = SparkyScorch();
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
await game.pump(component);
expect(
game.descendants().whereType<SparkyScorch>().length,
equals(1),
);
});
group('loads', () {
flameTester.test(
'a SparkyComputer',
(game) async {
await game.ensureAdd(SparkyScorch());
await game.pump(SparkyScorch());
expect(
game.descendants().whereType<SparkyComputer>().length,
equals(1),
@ -60,7 +67,7 @@ void main() {
flameTester.test(
'a SparkyAnimatronic',
(game) async {
await game.ensureAdd(SparkyScorch());
await game.pump(SparkyScorch());
expect(
game.descendants().whereType<SparkyAnimatronic>().length,
equals(1),
@ -71,7 +78,7 @@ void main() {
flameTester.test(
'three SparkyBumper',
(game) async {
await game.ensureAdd(SparkyScorch());
await game.pump(SparkyScorch());
expect(
game.descendants().whereType<SparkyBumper>().length,
equals(3),
@ -82,7 +89,7 @@ void main() {
flameTester.test(
'three SparkyBumpers with BumperNoiseBehavior',
(game) async {
await game.ensureAdd(SparkyScorch());
await game.pump(SparkyScorch());
final bumpers = game.descendants().whereType<SparkyBumper>();
for (final bumper in bumpers) {
expect(
@ -93,41 +100,30 @@ void main() {
},
);
});
});
group('SparkyComputerSensor', () {
flameTester.test('calls turboCharge', (game) async {
final sensor = SparkyComputerSensor();
final ball = _MockControlledBall();
final controller = _MockBallController();
when(() => ball.controller).thenReturn(controller);
when(controller.turboCharge).thenAnswer((_) async {});
await game.ensureAddAll([
sensor,
SparkyAnimatronic(),
]);
sensor.beginContact(ball, _MockContact());
verify(() => ball.controller.turboCharge()).called(1);
});
group('adds', () {
flameTester.test(
'ScoringContactBehavior to SparkyComputer',
(game) async {
await game.pump(SparkyScorch());
flameTester.test('plays SparkyAnimatronic', (game) async {
final sensor = SparkyComputerSensor();
final sparkyAnimatronic = SparkyAnimatronic();
final ball = _MockControlledBall();
final controller = _MockBallController();
when(() => ball.controller).thenReturn(controller);
when(controller.turboCharge).thenAnswer((_) async {});
await game.ensureAddAll([
sensor,
sparkyAnimatronic,
]);
final sparkyComputer =
game.descendants().whereType<SparkyComputer>().single;
expect(
sparkyComputer.firstChild<ScoringContactBehavior>(),
isNotNull,
);
},
);
expect(sparkyAnimatronic.playing, isFalse);
sensor.beginContact(ball, _MockContact());
expect(sparkyAnimatronic.playing, isTrue);
flameTester.test('a SparkyComputerBonusBehavior', (game) async {
final sparkyScorch = SparkyScorch();
await game.pump(sparkyScorch);
expect(
sparkyScorch.children.whereType<SparkyComputerBonusBehavior>().single,
isNotNull,
);
});
});
});
}

@ -409,14 +409,12 @@ void main() {
when(() => tapUpEvent.raw).thenReturn(raw);
await game.ready();
final previousBalls =
game.descendants().whereType<ControlledBall>().toList();
final previousBalls = game.descendants().whereType<Ball>().toList();
game.onTapUp(0, tapUpEvent);
await game.ready();
final currentBalls =
game.descendants().whereType<ControlledBall>().toList();
final currentBalls = game.descendants().whereType<Ball>().toList();
expect(
currentBalls.length,
@ -475,14 +473,13 @@ void main() {
game.lineEnd = endPosition;
await game.ready();
final previousBalls =
game.descendants().whereType<ControlledBall>().toList();
final previousBalls = game.descendants().whereType<Ball>().toList();
game.onPanEnd(_MockDragEndInfo());
await game.ready();
expect(
game.descendants().whereType<ControlledBall>().length,
game.descendants().whereType<Ball>().length,
equals(previousBalls.length + 1),
);
},

Loading…
Cancel
Save