Merge branch 'main' into fix/outer-bottom-boundary-rendering

pull/350/head
Alejandro Santiago 3 years ago committed by GitHub
commit 947bfcd3ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,33 @@
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';
/// Spawns a new [Ball] into the game when all balls are lost and still
/// [GameStatus.playing].
class BallSpawningBehavior extends Component
with ParentIsA<PinballGame>, BlocComponent<GameBloc, GameState> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
if (!newState.status.isPlaying) return false;
final startedGame = previousState?.status.isWaiting ?? true;
final lostRound =
(previousState?.rounds ?? newState.rounds + 1) > newState.rounds;
return startedGame || lostRound;
}
@override
void onNewState(GameState state) {
final plunger = parent.descendants().whereType<Plunger>().single;
final canvas = parent.descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.launch(characterTheme: parent.characterTheme)
..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
canvas.add(ball);
}
}

@ -1,3 +1,4 @@
export 'bumper_noisy_behavior.dart';
export 'ball_spawning_behavior.dart';
export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart';
export 'scoring_behavior.dart';

@ -6,7 +6,7 @@ import 'package:pinball/game/pinball_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> {
class BumperNoiseBehavior extends ContactBehavior with HasGameRef<PinballGame> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);

@ -27,6 +27,7 @@ enum GameStatus {
}
extension GameStatusX on GameStatus {
bool get isWaiting => this == GameStatus.waiting;
bool get isPlaying => this == GameStatus.playing;
bool get isGameOver => this == GameStatus.gameOver;
}

@ -35,19 +35,19 @@ class AndroidAcres extends Component {
AndroidBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-25, 1.3),
AndroidBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-32.8, -9.2),
AndroidBumper.cow(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.5, -13.8),
AndroidSpaceshipBonusBehavior(),

@ -5,7 +5,7 @@ export 'controlled_ball.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart';
export 'drain.dart';
export 'drain/drain.dart';
export 'flutter_forest/flutter_forest.dart';
export 'game_bloc_status_listener.dart';
export 'google_word/google_word.dart';

@ -22,9 +22,7 @@ class ControlledBall extends Ball with Controls<BallController> {
zIndex = ZIndexes.ballOnLaunchRamp;
}
/// {@template bonus_ball}
/// {@macro controlled_ball}
/// {@endtemplate}
ControlledBall.bonus({
required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) {
@ -47,12 +45,6 @@ class BallController extends ComponentController<Ball>
/// {@macro ball_controller}
BallController(Ball ball) : super(ball);
/// Event triggered when the ball is lost.
// TODO(alestiago): Refactor using behaviors.
void lost() {
component.shouldRemove = true;
}
/// 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 {
@ -70,13 +62,4 @@ class BallController extends ComponentController<Ball>
BallTurboChargingBehavior(impulse: Vector2(40, 110)),
);
}
@override
void onRemove() {
super.onRemove();
final noBallsLeft = gameRef.descendants().whereType<Ball>().isEmpty;
if (noBallsLeft) {
gameRef.read<GameBloc>().add(const RoundLost());
}
}
}

@ -2,6 +2,7 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -14,6 +15,29 @@ class ControlledPlunger extends Plunger with Controls<PlungerController> {
: super(compressionDistance: compressionDistance) {
controller = PlungerController(this);
}
@override
void release() {
super.release();
add(PlungerNoiseBehavior());
}
}
/// A behavior attached to the plunger when it launches the ball
/// which plays the related sound effects.
class PlungerNoiseBehavior extends Component with HasGameRef<PinballGame> {
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.player.play(PinballAudio.launcher);
}
@override
void update(double dt) {
super.update(dt);
removeFromParent();
}
}
/// {@template plunger_controller}

@ -1,34 +0,0 @@
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 drain}
/// Area located at the bottom of the board to detect when a [Ball] is lost.
/// {@endtemplate}
// TODO(allisonryan0002): move to components package when possible.
class Drain extends BodyComponent with ContactCallbacks {
/// {@macro drain}
Drain() : super(renderBody: false);
@override
Body createBody() {
final shape = EdgeShape()
..set(
BoardDimensions.bounds.bottomLeft.toVector2(),
BoardDimensions.bounds.bottomRight.toVector2(),
);
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef(userData: this);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
// TODO(allisonryan0002): move this to ball.dart when BallLost is removed.
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.lost();
}
}

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

@ -0,0 +1,21 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Handles removing a [Ball] from the game.
class DrainingBehavior extends ContactBehavior<Drain>
with HasGameRef<PinballGame> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
other.removeFromParent();
final ballsLeft = gameRef.descendants().whereType<Ball>().length;
if (ballsLeft - 1 == 0) {
gameRef.read<GameBloc>().add(const RoundLost());
}
}
}

@ -0,0 +1,36 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'package:pinball/game/components/drain/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template drain}
/// Area located at the bottom of the board.
///
/// Its [DrainingBehavior] handles removing a [Ball] from the game.
/// {@endtemplate}
class Drain extends BodyComponent with ContactCallbacks {
/// {@macro drain}
Drain()
: super(
renderBody: false,
children: [DrainingBehavior()],
);
/// Creates a [Drain] without any children.
///
/// This can be used for testing a [Drain]'s behaviors in isolation.
@visibleForTesting
Drain.test();
@override
Body createBody() {
final shape = EdgeShape()
..set(
BoardDimensions.bounds.bottomLeft.toVector2(),
BoardDimensions.bounds.bottomRight.toVector2(),
);
final fixtureDef = FixtureDef(shape, isSensor: true);
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}

@ -19,25 +19,25 @@ class FlutterForest extends Component with ZIndex {
Signpost(
children: [
ScoringContactBehavior(points: Points.fiveThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(22.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66),

@ -18,19 +18,19 @@ class SparkyScorch extends Component {
SparkyBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9),

@ -17,11 +17,7 @@ import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends PinballForge2DGame
with
FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController>,
MultiTouchTapDetector {
with FlameBloc, HasKeyboardHandlerComponents, MultiTouchTapDetector {
PinballGame({
required this.characterTheme,
required this.leaderboardRepository,
@ -29,7 +25,6 @@ class PinballGame extends PinballForge2DGame
required this.player,
}) : super(gravity: Vector2(0, 30)) {
images.prefix = '';
controller = _GameBallsController(this);
}
/// Identifier of the play button overlay
@ -73,6 +68,7 @@ class PinballGame extends PinballForge2DGame
await addAll(
[
GameBlocStatusListener(),
BallSpawningBehavior(),
CameraFocusingBehavior(),
CanvasComponent(
onSpritePainted: (paint) {
@ -147,43 +143,6 @@ class PinballGame extends PinballForge2DGame
}
}
class _GameBallsController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState> {
_GameBallsController(PinballGame game) : super(game);
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
return noBallsLeft && newState.status.isPlaying;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
spawnBall();
}
@override
Future<void> onLoad() async {
await super.onLoad();
spawnBall();
}
void spawnBall() {
// TODO(alestiago): Refactor with behavioural pattern.
component.ready().whenComplete(() {
final plunger = parent!.descendants().whereType<Plunger>().single;
final ball = ControlledBall.launch(
characterTheme: component.characterTheme,
)..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
component.descendants().whereType<ZCanvasComponent>().single.add(ball);
});
}
}
class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({
required CharacterTheme characterTheme,
@ -195,9 +154,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
player: player,
leaderboardRepository: leaderboardRepository,
l10n: l10n,
) {
controller = _GameBallsController(this);
}
);
Vector2? lineStart;
Vector2? lineEnd;

@ -14,11 +14,13 @@ class $AssetsMusicGen {
class $AssetsSfxGen {
const $AssetsSfxGen();
String get afterLaunch => 'assets/sfx/after_launch.mp3';
String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3';
String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3';
String get google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';
String get launcher => 'assets/sfx/launcher.mp3';
}
class Assets {

@ -22,6 +22,9 @@ enum PinballAudio {
/// Game over
gameOverVoiceOver,
/// Launcher
launcher,
}
/// Defines the contract of the creation of an [AudioPool].
@ -158,6 +161,11 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio,
path: Assets.sfx.google,
),
PinballAudio.launcher: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.launcher,
),
PinballAudio.ioPinballVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,

@ -151,6 +151,10 @@ void main() {
'packages/pinball_audio/assets/sfx/game_over_voice_over.mp3',
),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/launcher.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'),
@ -219,6 +223,18 @@ void main() {
});
});
group('launcher', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.launcher);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.launcher}'),
).called(1);
});
});
group('ioPinballVoiceOver', () {
test('plays the correct file', () async {
await Future.wait(player.load());

@ -1,5 +1,6 @@
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_flame/pinball_flame.dart';
@ -13,16 +14,23 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger}
Plunger({
required this.compressionDistance,
}) : super(renderBody: false) {
}) : super(
renderBody: false,
children: [_PlungerSpriteAnimationGroupComponent()],
) {
zIndex = ZIndexes.plunger;
layer = Layer.launcher;
}
/// Creates a [Plunger] without any children.
///
/// This can be used for testing [Plunger]'s behaviors in isolation.
@visibleForTesting
Plunger.test({required this.compressionDistance});
/// Distance the plunger can lower.
final double compressionDistance;
late final _PlungerSpriteAnimationGroupComponent _spriteComponent;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
@ -78,8 +86,10 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// Set a constant downward velocity on the [Plunger].
void pull() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
body.linearVelocity = Vector2(0, 7);
_spriteComponent.pull();
sprite.pull();
}
/// Set an upward velocity on the [Plunger].
@ -87,10 +97,12 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition].
void release() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
_pullingDownTime = 0;
final velocity = (initialPosition.y - body.position.y) * 11;
body.linearVelocity = Vector2(0, velocity);
_spriteComponent.release();
sprite.release();
}
@override
@ -127,9 +139,6 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
_spriteComponent = _PlungerSpriteAnimationGroupComponent();
await add(_spriteComponent);
}
}

@ -14,6 +14,17 @@ void main() {
group('Plunger', () {
const compressionDistance = 0.0;
test('can be instantiated', () {
expect(
Plunger(compressionDistance: compressionDistance),
isA<Plunger>(),
);
expect(
Plunger.test(compressionDistance: compressionDistance),
isA<Plunger>(),
);
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {

@ -23,7 +23,7 @@ class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('BumperNoisyBehavior', () {});
group('BumperNoiseBehavior', () {});
late PinballPlayer player;
final flameTester = FlameTester(
@ -37,7 +37,7 @@ void main() {
flameTester.testGameWidget(
'plays bumper sound',
setUp: (game, _) async {
final behavior = BumperNoisyBehavior();
final behavior = BumperNoiseBehavior();
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
await parent.ensureAdd(behavior);

@ -2,7 +2,7 @@
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/behaviors/bumper_noisy_behavior.dart';
import 'package:pinball/game/behaviors/bumper_noise_behavior.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
@ -103,13 +103,13 @@ void main() {
);
flameTester.test(
'three AndroidBumpers with BumperNoisyBehavior',
'three AndroidBumpers with BumperNoiseBehavior',
(game) async {
await game.ensureAdd(AndroidAcres());
final bumpers = game.descendants().whereType<AndroidBumper>();
for (final bumper in bumpers) {
expect(
bumper.firstChild<BumperNoisyBehavior>(),
bumper.firstChild<BumperNoiseBehavior>(),
isNotNull,
);
}

@ -0,0 +1,117 @@
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/ball_spawning_behavior.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' as theme;
import '../../../../helpers/test_games.dart';
class _MockGameState extends Mock implements GameState {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group(
'BallSpawningBehavior',
() {
final flameTester = FlameTester(EmptyPinballTestGame.new);
test('can be instantiated', () {
expect(
BallSpawningBehavior(),
isA<BallSpawningBehavior>(),
);
});
flameTester.test(
'loads',
(game) async {
final behavior = BallSpawningBehavior();
await game.ensureAdd(behavior);
expect(game.contains(behavior), isTrue);
},
);
group('listenWhen', () {
test(
'never listens when new state not playing',
() {
final waiting = const GameState.initial()
..copyWith(status: GameStatus.waiting);
final gameOver = const GameState.initial()
..copyWith(status: GameStatus.gameOver);
final behavior = BallSpawningBehavior();
expect(behavior.listenWhen(_MockGameState(), waiting), isFalse);
expect(behavior.listenWhen(_MockGameState(), gameOver), isFalse);
},
);
test(
'listens when started playing',
() {
final waiting =
const GameState.initial().copyWith(status: GameStatus.waiting);
final playing =
const GameState.initial().copyWith(status: GameStatus.playing);
final behavior = BallSpawningBehavior();
expect(behavior.listenWhen(waiting, playing), isTrue);
},
);
test(
'listens when lost rounds',
() {
final playing1 = const GameState.initial().copyWith(
status: GameStatus.playing,
rounds: 2,
);
final playing2 = const GameState.initial().copyWith(
status: GameStatus.playing,
rounds: 1,
);
final behavior = BallSpawningBehavior();
expect(behavior.listenWhen(playing1, playing2), isTrue);
},
);
test(
"doesn't listen when didn't lose any rounds",
() {
final playing = const GameState.initial().copyWith(
status: GameStatus.playing,
rounds: 2,
);
final behavior = BallSpawningBehavior();
expect(behavior.listenWhen(playing, playing), isFalse);
},
);
});
flameTester.test(
'onNewState adds a ball',
(game) async {
await game.images.load(theme.Assets.images.dash.ball.keyName);
final behavior = BallSpawningBehavior();
await game.ensureAddAll([
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
]);
expect(game.descendants().whereType<Ball>(), isEmpty);
behavior.onNewState(_MockGameState());
await game.ready();
expect(game.descendants().whereType<Ball>(), isNotEmpty);
},
);
},
);
}

@ -63,43 +63,6 @@ void main() {
);
});
flameBlocTester.testGameWidget(
"lost doesn't adds RoundLost to GameBloc "
'when there are balls left',
setUp: (game, tester) async {
final controller = BallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final otherBall = Ball();
final otherController = BallController(otherBall);
await otherBall.add(otherController);
await game.ensureAdd(otherBall);
controller.lost();
await game.ready();
},
verify: (game, tester) async {
verifyNever(() => gameBloc.add(const RoundLost()));
},
);
flameBlocTester.testGameWidget(
'lost adds RoundLost to GameBloc '
'when there are no balls left',
setUp: (game, tester) async {
final controller = BallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
controller.lost();
await game.ready();
},
verify: (game, tester) async {
verify(() => gameBloc.add(const RoundLost())).called(1);
},
);
group('turboCharge', () {
setUpAll(() {
registerFallbackValue(Vector2.zero());

@ -1,18 +1,25 @@
// ignore_for_file: cascade_invocations
import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {}
class _MockPinballPlayer extends Mock implements PinballPlayer {}
class _MockPinballGame extends Mock implements PinballGame {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
@ -20,31 +27,28 @@ void main() {
group('PlungerController', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);
group('onKeyEvent', () {
final downKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyS,
]);
late Plunger plunger;
late PlungerController controller;
setUp(() {
plunger = Plunger(compressionDistance: 10);
gameBloc = _MockGameBloc();
plunger = ControlledPlunger(compressionDistance: 10);
controller = PlungerController(plunger);
plunger.add(controller);
});
group('onKeyEvent', () {
final downKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyS,
]);
testRawKeyDownEvents(downKeys, (event) {
flameTester.test(
'moves down '
@ -129,5 +133,50 @@ void main() {
);
});
});
flameTester.test(
'adds the PlungerNoiseBehavior plunger is released',
(game) async {
await game.ensureAdd(plunger);
plunger.body.setTransform(Vector2(0, 1), 0);
plunger.release();
await game.ready();
final count =
game.descendants().whereType<PlungerNoiseBehavior>().length;
expect(count, equals(1));
},
);
});
group('PlungerNoiseBehavior', () {
late PinballGame game;
late PinballPlayer player;
late PlungerNoiseBehavior behavior;
setUp(() {
game = _MockPinballGame();
player = _MockPinballPlayer();
when(() => game.player).thenReturn(player);
behavior = PlungerNoiseBehavior();
behavior.mockGameRef(game);
});
test('plays the correct sound on load', () async {
await behavior.onLoad();
verify(() => player.play(PinballAudio.launcher)).called(1);
});
test('is removed on the first update', () {
final parent = Component();
parent.add(behavior);
parent.update(0); // Run a tick to ensure it is added
behavior.update(0); // Run its own update where the removal happens
expect(behavior.shouldRemove, isTrue);
});
});
}

@ -0,0 +1,121 @@
// 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/components/drain/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group(
'DrainingBehavior',
() {
final flameTester = FlameTester(Forge2DGame.new);
test('can be instantiated', () {
expect(DrainingBehavior(), isA<DrainingBehavior>());
});
flameTester.test(
'loads',
(game) async {
final parent = Drain.test();
final behavior = DrainingBehavior();
await parent.add(behavior);
await game.ensureAdd(parent);
expect(parent.contains(behavior), isTrue);
},
);
group('beginContact', () {
final asset = theme.Assets.images.dash.ball.keyName;
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
);
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'adds RoundLost when no balls left',
setUp: (game, tester) async {
await game.images.load(asset);
final drain = Drain.test();
final behavior = DrainingBehavior();
final ball = Ball.test();
await drain.add(behavior);
await game.ensureAddAll([drain, ball]);
behavior.beginContact(ball, _MockContact());
await game.ready();
expect(game.descendants().whereType<Ball>(), isEmpty);
verify(() => gameBloc.add(const RoundLost())).called(1);
},
);
flameBlocTester.testGameWidget(
"doesn't add RoundLost when there are balls left",
setUp: (game, tester) async {
await game.images.load(asset);
final drain = Drain.test();
final behavior = DrainingBehavior();
final ball1 = Ball.test();
final ball2 = Ball.test();
await drain.add(behavior);
await game.ensureAddAll([
drain,
ball1,
ball2,
]);
behavior.beginContact(ball1, _MockContact());
await game.ready();
expect(game.descendants().whereType<Ball>(), isNotEmpty);
verifyNever(() => gameBloc.add(const RoundLost()));
},
);
flameBlocTester.testGameWidget(
'removes the Ball',
setUp: (game, tester) async {
await game.images.load(asset);
final drain = Drain.test();
final behavior = DrainingBehavior();
final ball = Ball.test();
await drain.add(behavior);
await game.ensureAddAll([drain, ball]);
behavior.beginContact(ball, _MockContact());
await game.ready();
expect(game.descendants().whereType<Ball>(), isEmpty);
},
);
});
},
);
}

@ -3,16 +3,10 @@
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';
class _MockControlledBall extends Mock implements ControlledBall {}
class _MockBallController extends Mock implements BallController {}
import 'package:pinball/game/game.dart';
class _MockContact extends Mock implements Contact {}
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -45,19 +39,5 @@ void main() {
expect(drain.body.fixtures.first.isSensor, isTrue);
},
);
test(
'calls lost on contact with ball',
() async {
final drain = Drain();
final ball = _MockControlledBall();
final controller = _MockBallController();
when(() => ball.controller).thenReturn(controller);
drain.beginContact(ball, _MockContact());
verify(controller.lost).called(1);
},
);
});
}

@ -76,14 +76,14 @@ void main() {
);
flameTester.test(
'three DashNestBumpers with BumperNoisyBehavior',
'three DashNestBumpers with BumperNoiseBehavior',
(game) async {
final flutterForest = FlutterForest();
await game.ensureAdd(ZCanvasComponent(children: [flutterForest]));
final bumpers = game.descendants().whereType<DashNestBumper>();
for (final bumper in bumpers) {
expect(
bumper.firstChild<BumperNoisyBehavior>(),
bumper.firstChild<BumperNoiseBehavior>(),
isNotNull,
);
}

@ -77,13 +77,13 @@ void main() {
);
flameTester.test(
'three SparkyBumpers with BumperNoisyBehavior',
'three SparkyBumpers with BumperNoiseBehavior',
(game) async {
await game.ensureAdd(SparkyScorch());
final bumpers = game.descendants().whereType<SparkyBumper>();
for (final bumper in bumpers) {
expect(
bumper.firstChild<BumperNoisyBehavior>(),
bumper.firstChild<BumperNoiseBehavior>(),
isNotNull,
);
}

@ -9,6 +9,7 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
@ -17,8 +18,6 @@ import '../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {}
class _MockGameState extends Mock implements GameState {}
class _MockEventPosition extends Mock implements EventPosition {}
class _MockTapDownDetails extends Mock implements TapDownDetails {}
@ -167,8 +166,17 @@ void main() {
);
group('components', () {
// TODO(alestiago): tests that Blueprints get added once the Blueprint
// class is removed.
flameBlocTester.test(
'has only one BallSpawningBehavior',
(game) async {
await game.ready();
expect(
game.descendants().whereType<BallSpawningBehavior>().length,
equals(1),
);
},
);
flameBlocTester.test(
'has only one Drain',
(game) async {
@ -272,91 +280,6 @@ void main() {
}
},
);
group('controller', () {
group('listenWhen', () {
flameTester.testGameWidget(
'listens when all balls are lost and there are more than 0 rounds',
setUp: (game, tester) async {
// TODO(ruimiguel): check why testGameWidget doesn't add any ball
// to the game. Test needs to have no balls, so fortunately works.
final newState = _MockGameState();
when(() => newState.status).thenReturn(GameStatus.playing);
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.status).thenReturn(GameStatus.playing);
await game.ready();
expect(
game.descendants().whereType<ControlledBall>().length,
greaterThan(0),
);
expect(
game.controller.listenWhen(_MockGameState(), newState),
isFalse,
);
},
);
flameTester.testGameWidget(
"doesn't listen when game is over",
setUp: (game, tester) async {
// TODO(ruimiguel): check why testGameWidget doesn't add any ball
// to the game. Test needs to have no balls, so fortunately works.
final newState = _MockGameState();
when(() => newState.status).thenReturn(GameStatus.gameOver);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
await game.ready();
expect(
game.descendants().whereType<ControlledBall>().isEmpty,
isTrue,
);
expect(
game.controller.listenWhen(_MockGameState(), newState),
isFalse,
);
},
);
});
group('onNewState', () {
flameTester.test(
'spawns a ball',
(game) async {
final previousBalls =
game.descendants().whereType<ControlledBall>().toList();
game.controller.onNewState(_MockGameState());
await game.ready();
final currentBalls =
game.descendants().whereType<ControlledBall>().toList();
expect(
currentBalls.length,
equals(previousBalls.length + 1),
);
},
);
});
});
});
group('flipper control', () {

Loading…
Cancel
Save