Merge branch 'main' into refactor/remove-game-balls-controller

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

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

@ -0,0 +1,84 @@
import 'package:flame/components.dart';
import 'package:flame/game.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';
/// {@template focus_data}
/// Defines a [Camera] focus point.
/// {@endtemplate}
class FocusData {
/// {@template focus_data}
FocusData({
required this.zoom,
required this.position,
});
/// The amount of zoom.
final double zoom;
/// The position of the camera.
final Vector2 position;
}
/// Changes the game focus when the [GameBloc] status changes.
class CameraFocusingBehavior extends Component
with ParentIsA<FlameGame>, BlocComponent<GameBloc, GameState> {
late final Map<String, FocusData> _foci;
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.status != newState.status;
}
@override
void onNewState(GameState state) {
switch (state.status) {
case GameStatus.waiting:
break;
case GameStatus.playing:
_zoom(_foci['game']!);
break;
case GameStatus.gameOver:
_zoom(_foci['backbox']!);
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
_foci = {
'game': FocusData(
zoom: parent.size.y / 16,
position: Vector2(0, -7.8),
),
'waiting': FocusData(
zoom: parent.size.y / 18,
position: Vector2(0, -112),
),
'backbox': FocusData(
zoom: parent.size.y / 10,
position: Vector2(0, -111),
),
};
_snap(_foci['waiting']!);
}
void _snap(FocusData data) {
parent.camera
..speed = 100
..followVector2(data.position)
..zoom = data.zoom;
}
void _zoom(FocusData data) {
final zoom = CameraZoom(value: data.zoom);
zoom.completed.then((_) {
parent.camera.moveTo(data.position);
});
add(zoom);
}
}

@ -1,93 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds helpers methods to Flame's [Camera].
extension CameraX on Camera {
/// Instantly apply the point of focus to the [Camera].
void snapToFocus(FocusData data) {
followVector2(data.position);
zoom = data.zoom;
}
/// Returns a [CameraZoom] that can be added to a [FlameGame].
CameraZoom focusToCameraZoom(FocusData data) {
final zoom = CameraZoom(value: data.zoom);
zoom.completed.then((_) {
moveTo(data.position);
});
return zoom;
}
}
/// {@template focus_data}
/// Model class that defines a focus point of the camera.
/// {@endtemplate}
class FocusData {
/// {@template focus_data}
FocusData({
required this.zoom,
required this.position,
});
/// The amount of zoom.
final double zoom;
/// The position of the camera.
final Vector2 position;
}
/// {@template camera_controller}
/// A [Component] that controls its game camera focus.
/// {@endtemplate}
class CameraController extends ComponentController<FlameGame> {
/// {@macro camera_controller}
CameraController(FlameGame component) : super(component) {
final gameZoom = component.size.y / 16;
final waitingBackboxZoom = component.size.y / 18;
final gameOverBackboxZoom = component.size.y / 10;
gameFocus = FocusData(
zoom: gameZoom,
position: Vector2(0, -7.8),
);
waitingBackboxFocus = FocusData(
zoom: waitingBackboxZoom,
position: Vector2(0, -112),
);
gameOverBackboxFocus = FocusData(
zoom: gameOverBackboxZoom,
position: Vector2(0, -111),
);
// Game starts with the camera focused on the [Backbox].
component.camera
..speed = 100
..snapToFocus(waitingBackboxFocus);
}
/// Holds the data for the game focus point.
late final FocusData gameFocus;
/// Holds the data for the waiting backbox focus point.
late final FocusData waitingBackboxFocus;
/// Holds the data for the game over backbox focus point.
late final FocusData gameOverBackboxFocus;
/// Move the camera focus to the game board.
void focusOnGame() {
component.add(component.camera.focusToCameraZoom(gameFocus));
}
/// Move the camera focus to the waiting backbox.
void focusOnWaitingBackbox() {
component.add(component.camera.focusToCameraZoom(waitingBackboxFocus));
}
/// Move the camera focus to the game over backbox.
void focusOnGameOverBackbox() {
component.add(component.camera.focusToCameraZoom(gameOverBackboxFocus));
}
}

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart';
export 'bottom_group.dart';
export 'camera_controller.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';

@ -18,15 +18,14 @@ class GameBlocStatusListener extends Component
break;
case GameStatus.playing:
gameRef.player.play(PinballAudio.backgroundMusic);
gameRef.firstChild<CameraController>()?.focusOnGame();
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
case GameStatus.gameOver:
gameRef.player.play(PinballAudio.gameOverVoiceOver);
gameRef.descendants().whereType<Backbox>().first.requestInitials(
score: state.displayScore,
character: gameRef.characterTheme,
);
gameRef.firstChild<CameraController>()!.focusOnGameOverBackbox();
break;
}
}

@ -43,8 +43,6 @@ class PinballGame extends PinballForge2DGame
@override
Future<void> onLoad() async {
await add(CameraController(this));
final machine = [
BoardBackgroundSpriteComponent(),
Boundaries(),
@ -71,6 +69,7 @@ class PinballGame extends PinballForge2DGame
[
GameBlocStatusListener(),
BallSpawningBehavior(),
CameraFocusingBehavior(),
CanvasComponent(
onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) {

@ -50,17 +50,19 @@ class _ScoreDisplay extends StatelessWidget {
Widget build(BuildContext context) {
final l10n = context.l10n;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
l10n.score.toLowerCase(),
style: Theme.of(context).textTheme.subtitle1,
),
const _ScoreText(),
const RoundCountDisplay(),
],
return FittedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
l10n.score.toLowerCase(),
style: Theme.of(context).textTheme.subtitle1,
),
const _ScoreText(),
const RoundCountDisplay(),
],
),
);
}
}
@ -72,11 +74,9 @@ class _ScoreText extends StatelessWidget {
Widget build(BuildContext context) {
final score = context.select((GameBloc bloc) => bloc.state.displayScore);
return FittedBox(
child: Text(
score.formatScore(),
style: Theme.of(context).textTheme.headline2,
),
return Text(
score.formatScore(),
style: Theme.of(context).textTheme.headline1,
);
}
}

@ -16,6 +16,7 @@ class $AssetsSfxGen {
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';
}

@ -18,7 +18,10 @@ enum PinballAudio {
backgroundMusic,
/// IO Pinball voice over
ioPinballVoiceOver
ioPinballVoiceOver,
/// Game over
gameOverVoiceOver,
}
/// Defines the contract of the creation of an [AudioPool].
@ -30,20 +33,16 @@ typedef CreateAudioPool = Future<AudioPool> Function(
String? prefix,
});
/// Function that defines the contract for playing a single
/// audio
/// Defines the contract for playing a single audio.
typedef PlaySingleAudio = Future<void> Function(String);
/// Function that defines the contract for looping a single
/// audio
/// Defines the contract for looping a single audio.
typedef LoopSingleAudio = Future<void> Function(String);
/// Function that defines the contract for pre fetching an
/// audio
/// Defines the contract for pre fetching an audio.
typedef PreCacheSingleAudio = Future<void> Function(String);
/// Function that defines the contract for configuring
/// an [AudioCache] instance
/// Defines the contract for configuring an [AudioCache] instance.
typedef ConfigureAudioCache = void Function(AudioCache);
abstract class _Audio {
@ -164,6 +163,11 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio,
path: Assets.sfx.ioPinballVoiceOver,
),
PinballAudio.gameOverVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.gameOverVoiceOver,
),
PinballAudio.bumper: _BumperAudio(
createAudioPool: _createAudioPool,
seed: _seed,

@ -146,6 +146,11 @@ void main() {
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
),
).called(1);
verify(
() => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/game_over_voice_over.mp3',
),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'),
@ -227,6 +232,19 @@ void main() {
});
});
group('gameOverVoiceOver', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.gameOverVoiceOver);
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.gameOverVoiceOver}',
),
).called(1);
});
});
group('backgroundMusic', () {
test('plays the correct file', () async {
await Future.wait(player.load());

@ -45,7 +45,7 @@ abstract class PinballTextStyle {
);
static const subtitle1 = TextStyle(
fontSize: 10,
fontSize: 12,
fontFamily: _primaryFontFamily,
package: _fontPackage,
color: PinballColors.yellow,

@ -26,9 +26,9 @@ void main() {
expect(style.color, PinballColors.white);
});
test('subtitle1 has fontSize 10 and yellow color', () {
test('subtitle1 has fontSize 12 and yellow color', () {
const style = PinballTextStyle.subtitle1;
expect(style.fontSize, 10);
expect(style.fontSize, 12);
expect(style.color, PinballColors.yellow);
});

@ -0,0 +1,126 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/behaviors/camera_focusing_behavior.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group(
'CameraFocusingBehavior',
() {
final flameTester = FlameTester(
EmptyPinballTestGame.new,
);
test('can be instantiated', () {
expect(
CameraFocusingBehavior(),
isA<CameraFocusingBehavior>(),
);
});
flameTester.test('loads', (game) async {
final behavior = CameraFocusingBehavior();
await game.ensureAdd(behavior);
expect(game.contains(behavior), isTrue);
});
flameTester.test(
'changes focus when loaded',
(game) async {
final behavior = CameraFocusingBehavior();
final previousZoom = game.camera.zoom;
expect(game.camera.follow, isNull);
await game.ensureAdd(behavior);
expect(game.camera.follow, isNotNull);
expect(game.camera.zoom, isNot(equals(previousZoom)));
},
);
flameTester.test(
'listenWhen only listens when status changes',
(game) async {
final behavior = CameraFocusingBehavior();
const waiting = GameState.initial();
final playing =
const GameState.initial().copyWith(status: GameStatus.playing);
final gameOver =
const GameState.initial().copyWith(status: GameStatus.gameOver);
expect(behavior.listenWhen(waiting, waiting), isFalse);
expect(behavior.listenWhen(waiting, playing), isTrue);
expect(behavior.listenWhen(waiting, gameOver), isTrue);
expect(behavior.listenWhen(playing, playing), isFalse);
expect(behavior.listenWhen(playing, waiting), isTrue);
expect(behavior.listenWhen(playing, gameOver), isTrue);
expect(behavior.listenWhen(gameOver, gameOver), isFalse);
expect(behavior.listenWhen(gameOver, waiting), isTrue);
expect(behavior.listenWhen(gameOver, playing), isTrue);
},
);
group('onNewState', () {
flameTester.test(
'zooms when started playing',
(game) async {
final playing =
const GameState.initial().copyWith(status: GameStatus.playing);
final behavior = CameraFocusingBehavior();
await game.ensureAdd(behavior);
behavior.onNewState(playing);
final previousPosition = game.camera.position.clone();
await game.ready();
final zoom = behavior.children.whereType<CameraZoom>().single;
game.update(zoom.controller.duration!);
game.update(0);
expect(zoom.controller.completed, isTrue);
expect(
game.camera.position,
isNot(equals(previousPosition)),
);
},
);
flameTester.test(
'zooms when game is over',
(game) async {
final playing = const GameState.initial().copyWith(
status: GameStatus.gameOver,
);
final behavior = CameraFocusingBehavior();
await game.ensureAdd(behavior);
behavior.onNewState(playing);
final previousPosition = game.camera.position.clone();
await game.ready();
final zoom = behavior.children.whereType<CameraZoom>().single;
game.update(zoom.controller.duration!);
game.update(0);
expect(zoom.controller.completed, isTrue);
expect(
game.camera.position,
isNot(equals(previousPosition)),
);
},
);
});
},
);
}

@ -1,113 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/components/camera_controller.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('CameraController', () {
late FlameGame game;
late CameraController controller;
setUp(() async {
game = FlameGame()..onGameResize(Vector2(100, 200));
controller = CameraController(game);
await game.ensureAdd(controller);
});
test('loads correctly', () async {
expect(game.firstChild<CameraController>(), isNotNull);
});
test('correctly calculates the zooms', () async {
expect(controller.gameFocus.zoom.toInt(), equals(12));
expect(controller.waitingBackboxFocus.zoom.toInt(), equals(11));
});
test('correctly sets the initial zoom and position', () async {
expect(game.camera.zoom, equals(controller.waitingBackboxFocus.zoom));
expect(
game.camera.follow,
equals(controller.waitingBackboxFocus.position),
);
});
group('focusOnGame', () {
test('changes the zoom', () async {
controller.focusOnGame();
await game.ready();
final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.gameFocus.zoom));
});
test('moves the camera after the zoom is completed', () async {
controller.focusOnGame();
await game.ready();
final cameraZoom = game.firstChild<CameraZoom>()!;
final future = cameraZoom.completed;
game.update(10);
game.update(0); // Ensure that the component was removed
await future;
expect(game.camera.position, Vector2(-4, -120));
});
});
group('focusOnWaitingBackbox', () {
test('changes the zoom', () async {
controller.focusOnWaitingBackbox();
await game.ready();
final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.waitingBackboxFocus.zoom));
});
test('moves the camera after the zoom is completed', () async {
controller.focusOnWaitingBackbox();
await game.ready();
final cameraZoom = game.firstChild<CameraZoom>()!;
final future = cameraZoom.completed;
game.update(10);
game.update(0); // Ensure that the component was removed
await future;
expect(game.camera.position, Vector2(-4.5, -121));
});
});
group('focusOnGameOverBackbox', () {
test('changes the zoom', () async {
controller.focusOnGameOverBackbox();
await game.ready();
final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.gameOverBackboxFocus.zoom));
});
test('moves the camera after the zoom is completed', () async {
controller.focusOnGameOverBackbox();
await game.ready();
final cameraZoom = game.firstChild<CameraZoom>()!;
final future = cameraZoom.completed;
game.update(10);
game.update(0); // Ensure that the component was removed
await future;
expect(game.camera.position, Vector2(-2.5, -117));
});
});
});
}

@ -11,8 +11,6 @@ class _MockPinballGame extends Mock implements PinballGame {}
class _MockBackbox extends Mock implements Backbox {}
class _MockCameraController extends Mock implements CameraController {}
class _MockActiveOverlaysNotifier extends Mock
implements ActiveOverlaysNotifier {}
@ -46,20 +44,18 @@ void main() {
group('onNewState', () {
late PinballGame game;
late Backbox backbox;
late CameraController cameraController;
late GameBlocStatusListener gameFlowController;
late GameBlocStatusListener gameBlocStatusListener;
late PinballPlayer pinballPlayer;
late ActiveOverlaysNotifier overlays;
setUp(() {
game = _MockPinballGame();
backbox = _MockBackbox();
cameraController = _MockCameraController();
gameFlowController = GameBlocStatusListener();
gameBlocStatusListener = GameBlocStatusListener();
overlays = _MockActiveOverlaysNotifier();
pinballPlayer = _MockPinballPlayer();
gameFlowController.mockGameRef(game);
gameBlocStatusListener.mockGameRef(game);
when(
() => backbox.requestInitials(
@ -67,22 +63,18 @@ void main() {
character: any(named: 'character'),
),
).thenAnswer((_) async {});
when(cameraController.focusOnWaitingBackbox).thenAnswer((_) async {});
when(cameraController.focusOnGame).thenAnswer((_) async {});
when(() => overlays.remove(any())).thenAnswer((_) => true);
when(() => game.descendants().whereType<Backbox>())
.thenReturn([backbox]);
when(game.firstChild<CameraController>).thenReturn(cameraController);
when(() => game.overlays).thenReturn(overlays);
when(() => game.characterTheme).thenReturn(DashTheme());
when(() => game.player).thenReturn(pinballPlayer);
});
test(
'changes the backbox display and camera correctly '
'when the game is over',
'changes the backbox display when the game is over',
() {
final state = GameState(
totalScore: 0,
@ -92,7 +84,7 @@ void main() {
bonusHistory: const [],
status: GameStatus.gameOver,
);
gameFlowController.onNewState(state);
gameBlocStatusListener.onNewState(state);
verify(
() => backbox.requestInitials(
@ -100,18 +92,16 @@ void main() {
character: any(named: 'character'),
),
).called(1);
verify(cameraController.focusOnGameOverBackbox).called(1);
},
);
test(
'changes the backbox and camera correctly when it is not a game over',
'changes the backbox when it is not a game over',
() {
gameFlowController.onNewState(
gameBlocStatusListener.onNewState(
GameState.initial().copyWith(status: GameStatus.playing),
);
verify(cameraController.focusOnGame).called(1);
verify(() => overlays.remove(PinballGame.playButtonOverlay))
.called(1);
},
@ -120,7 +110,7 @@ void main() {
test(
'plays the background music on start',
() {
gameFlowController.onNewState(
gameBlocStatusListener.onNewState(
GameState.initial().copyWith(status: GameStatus.playing),
);
@ -128,6 +118,18 @@ void main() {
.called(1);
},
);
test(
'plays the game over voice over when it is game over',
() {
gameBlocStatusListener.onNewState(
GameState.initial().copyWith(status: GameStatus.gameOver),
);
verify(() => pinballPlayer.play(PinballAudio.gameOverVoiceOver))
.called(1);
},
);
});
});
}

Loading…
Cancel
Save