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 'camera_focusing_behavior.dart';
export 'scoring_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_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> { class BumperNoiseBehavior extends ContactBehavior with HasGameRef<PinballGame> {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);

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

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

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

@ -22,9 +22,7 @@ class ControlledBall extends Ball with Controls<BallController> {
zIndex = ZIndexes.ballOnLaunchRamp; zIndex = ZIndexes.ballOnLaunchRamp;
} }
/// {@template bonus_ball}
/// {@macro controlled_ball} /// {@macro controlled_ball}
/// {@endtemplate}
ControlledBall.bonus({ ControlledBall.bonus({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) { }) : super(assetPath: characterTheme.ball.keyName) {
@ -47,12 +45,6 @@ class BallController extends ComponentController<Ball>
/// {@macro ball_controller} /// {@macro ball_controller}
BallController(Ball ball) : super(ball); 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 /// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge
/// sequence runs, then boosts the ball out of the computer. /// sequence runs, then boosts the ball out of the computer.
Future<void> turboCharge() async { Future<void> turboCharge() async {
@ -70,13 +62,4 @@ class BallController extends ComponentController<Ball>
BallTurboChargingBehavior(impulse: Vector2(40, 110)), 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:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -14,6 +15,29 @@ class ControlledPlunger extends Plunger with Controls<PlungerController> {
: super(compressionDistance: compressionDistance) { : super(compressionDistance: compressionDistance) {
controller = PlungerController(this); 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} /// {@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( Signpost(
children: [ children: [
ScoringContactBehavior(points: Points.fiveThousand), ScoringContactBehavior(points: Points.fiveThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(8.35, -58.3), )..initialPosition = Vector2(8.35, -58.3),
DashNestBumper.main( DashNestBumper.main(
children: [ children: [
ScoringContactBehavior(points: Points.twoHundredThousand), ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(18.55, -59.35), )..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a( DashNestBumper.a(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(8.95, -51.95), )..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b( DashNestBumper.b(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(22.3, -46.75), )..initialPosition = Vector2(22.3, -46.75),
DashAnimatronic()..position = Vector2(20, -66), DashAnimatronic()..position = Vector2(20, -66),

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

@ -17,11 +17,7 @@ import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends PinballForge2DGame class PinballGame extends PinballForge2DGame
with with FlameBloc, HasKeyboardHandlerComponents, MultiTouchTapDetector {
FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController>,
MultiTouchTapDetector {
PinballGame({ PinballGame({
required this.characterTheme, required this.characterTheme,
required this.leaderboardRepository, required this.leaderboardRepository,
@ -29,7 +25,6 @@ class PinballGame extends PinballForge2DGame
required this.player, required this.player,
}) : super(gravity: Vector2(0, 30)) { }) : super(gravity: Vector2(0, 30)) {
images.prefix = ''; images.prefix = '';
controller = _GameBallsController(this);
} }
/// Identifier of the play button overlay /// Identifier of the play button overlay
@ -73,6 +68,7 @@ class PinballGame extends PinballForge2DGame
await addAll( await addAll(
[ [
GameBlocStatusListener(), GameBlocStatusListener(),
BallSpawningBehavior(),
CameraFocusingBehavior(), CameraFocusingBehavior(),
CanvasComponent( CanvasComponent(
onSpritePainted: (paint) { 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 { class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({ DebugPinballGame({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
@ -195,9 +154,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
player: player, player: player,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
l10n: l10n, l10n: l10n,
) { );
controller = _GameBallsController(this);
}
Vector2? lineStart; Vector2? lineStart;
Vector2? lineEnd; Vector2? lineEnd;

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

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

@ -151,6 +151,10 @@ void main() {
'packages/pinball_audio/assets/sfx/game_over_voice_over.mp3', 'packages/pinball_audio/assets/sfx/game_over_voice_over.mp3',
), ),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/launcher.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'), .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', () { group('ioPinballVoiceOver', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(player.load());

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

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

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

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

@ -1,18 +1,25 @@
// ignore_for_file: cascade_invocations
import 'dart:collection'; import 'dart:collection';
import 'package:bloc_test/bloc_test.dart'; 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:flame_test/flame_test.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {} class _MockGameBloc extends Mock implements GameBloc {}
class _MockPinballPlayer extends Mock implements PinballPlayer {}
class _MockPinballGame extends Mock implements PinballGame {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new); final flameTester = FlameTester(EmptyPinballTestGame.new);
@ -20,15 +27,21 @@ void main() {
group('PlungerController', () { group('PlungerController', () {
late GameBloc gameBloc; late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>( final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new, gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
); );
late Plunger plunger;
late PlungerController controller;
setUp(() {
gameBloc = _MockGameBloc();
plunger = ControlledPlunger(compressionDistance: 10);
controller = PlungerController(plunger);
plunger.add(controller);
});
group('onKeyEvent', () { group('onKeyEvent', () {
final downKeys = UnmodifiableListView([ final downKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowDown,
@ -36,15 +49,6 @@ void main() {
LogicalKeyboardKey.keyS, LogicalKeyboardKey.keyS,
]); ]);
late Plunger plunger;
late PlungerController controller;
setUp(() {
plunger = Plunger(compressionDistance: 10);
controller = PlungerController(plunger);
plunger.add(controller);
});
testRawKeyDownEvents(downKeys, (event) { testRawKeyDownEvents(downKeys, (event) {
flameTester.test( flameTester.test(
'moves down ' '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_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package: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() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -45,19 +39,5 @@ void main() {
expect(drain.body.fixtures.first.isSensor, isTrue); 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( flameTester.test(
'three DashNestBumpers with BumperNoisyBehavior', 'three DashNestBumpers with BumperNoiseBehavior',
(game) async { (game) async {
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
await game.ensureAdd(ZCanvasComponent(children: [flutterForest])); await game.ensureAdd(ZCanvasComponent(children: [flutterForest]));
final bumpers = game.descendants().whereType<DashNestBumper>(); final bumpers = game.descendants().whereType<DashNestBumper>();
for (final bumper in bumpers) { for (final bumper in bumpers) {
expect( expect(
bumper.firstChild<BumperNoisyBehavior>(), bumper.firstChild<BumperNoiseBehavior>(),
isNotNull, isNotNull,
); );
} }

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

@ -9,6 +9,7 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:pinball_theme/pinball_theme.dart' as theme;
@ -17,8 +18,6 @@ import '../helpers/helpers.dart';
class _MockGameBloc extends Mock implements GameBloc {} class _MockGameBloc extends Mock implements GameBloc {}
class _MockGameState extends Mock implements GameState {}
class _MockEventPosition extends Mock implements EventPosition {} class _MockEventPosition extends Mock implements EventPosition {}
class _MockTapDownDetails extends Mock implements TapDownDetails {} class _MockTapDownDetails extends Mock implements TapDownDetails {}
@ -167,8 +166,17 @@ void main() {
); );
group('components', () { group('components', () {
// TODO(alestiago): tests that Blueprints get added once the Blueprint flameBlocTester.test(
// class is removed. 'has only one BallSpawningBehavior',
(game) async {
await game.ready();
expect(
game.descendants().whereType<BallSpawningBehavior>().length,
equals(1),
);
},
);
flameBlocTester.test( flameBlocTester.test(
'has only one Drain', 'has only one Drain',
(game) async { (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', () { group('flipper control', () {

Loading…
Cancel
Save