From 631518395bfbbe161912792531a158c17f77cc10 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Mon, 4 Apr 2022 17:02:39 -0300 Subject: [PATCH] feat: adding sound effects --- lib/game/components/bonus_word.dart | 3 +- lib/game/components/score_points.dart | 2 +- lib/game/pinball_game.dart | 13 ++- lib/game/view/pinball_game_page.dart | 10 +- .../pinball_audio/lib/src/pinball_audio.dart | 42 +++++++- packages/pinball_audio/pubspec.yaml | 2 + .../pinball_audio/test/helpers/helpers.dart | 1 + .../pinball_audio/test/helpers/mocks.dart | 34 +++++++ .../test/src/pinball_audio_test.dart | 99 +++++++++++++++++++ test/app/view/app_test.dart | 8 +- test/game/components/bonus_word_test.dart | 24 +++++ test/game/components/score_points_test.dart | 22 ++++- test/helpers/builders.dart | 6 +- test/helpers/extensions.dart | 4 + test/helpers/mocks.dart | 3 + 15 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 packages/pinball_audio/test/helpers/helpers.dart create mode 100644 packages/pinball_audio/test/helpers/mocks.dart diff --git a/lib/game/components/bonus_word.dart b/lib/game/components/bonus_word.dart index 4aab67a9..3457e84c 100644 --- a/lib/game/components/bonus_word.dart +++ b/lib/game/components/bonus_word.dart @@ -8,7 +8,6 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template bonus_word} @@ -31,7 +30,7 @@ class BonusWord extends Component @override void onNewState(GameState state) { if (state.bonusHistory.last == GameBonus.word) { - gameRef.read().googleBonus(); + gameRef.audio.googleBonus(); final letters = children.whereType().toList(); diff --git a/lib/game/components/score_points.dart b/lib/game/components/score_points.dart index b71d1f6e..f4961599 100644 --- a/lib/game/components/score_points.dart +++ b/lib/game/components/score_points.dart @@ -39,6 +39,6 @@ class BallScorePointsCallback extends ContactCallback { Scored(points: scorePoints.points), ); - _gameRef.read().score(); + _gameRef.audio.score(); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index b5162053..437adc10 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -8,17 +8,20 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets; class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents { - PinballGame({required this.theme}) { + PinballGame({required this.theme, required this.audio}) { images.prefix = ''; } final PinballTheme theme; + final PinballAudio audio; + late final Plunger plunger; @override @@ -110,7 +113,13 @@ class PinballGame extends Forge2DGame } class DebugPinballGame extends PinballGame with TapDetector { - DebugPinballGame({required PinballTheme theme}) : super(theme: theme); + DebugPinballGame({ + required PinballTheme theme, + required PinballAudio audio, + }) : super( + theme: theme, + audio: audio, + ); @override Future onLoad() async { diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 0f58a20d..2f2069b9 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -53,13 +53,15 @@ class _PinballGameViewState extends State { void initState() { super.initState(); + final audio = context.read(); + + _game = widget._isDebugMode + ? DebugPinballGame(theme: widget.theme, audio: audio) + : PinballGame(theme: widget.theme, audio: audio); + // TODO(erickzanardo): Revisit this when we start to have more assets // this could expose a Stream (maybe even a cubit?) so we could show the // the loading progress with some fancy widgets. - _game = widget._isDebugMode - ? DebugPinballGame(theme: widget.theme) - : PinballGame(theme: widget.theme); - _fetchAssets(); } diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 65fe8a68..b2875084 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -1,20 +1,54 @@ +import 'package:audioplayers/audioplayers.dart'; import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/flame_audio.dart'; import 'package:pinball_audio/gen/assets.gen.dart'; +/// Function that defines the contract of the creation +/// of an [AudioPool] +typedef CreateAudioPool = Future Function( + String sound, { + bool? repeating, + int? maxPlayers, + int? minPlayers, + String? prefix, +}); + +/// Function that defines the contract for playing a single +/// audio +typedef PlaySingleAudio = Future Function(String); + +/// Function that defines the contract for configuring +/// an [AudioCache] instance +typedef ConfigureAudioCache = void Function(AudioCache); + /// {@template pinball_audio} /// Sound manager for the pinball game /// {@endtemplate} class PinballAudio { /// {@macro pinball_audio} - PinballAudio(); + PinballAudio({ + CreateAudioPool? createAudioPool, + PlaySingleAudio? playSingleAudio, + ConfigureAudioCache? configureAudioCache, + }) : _createAudioPool = createAudioPool ?? AudioPool.create, + _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play, + _configureAudioCache = configureAudioCache ?? + ((AudioCache a) { + a.prefix = ''; + }); + + final CreateAudioPool _createAudioPool; + + final PlaySingleAudio _playSingleAudio; + + final ConfigureAudioCache _configureAudioCache; late AudioPool _scorePool; /// Loads the sounds effects into the memory Future load() async { - FlameAudio.audioCache.prefix = ''; - _scorePool = await AudioPool.create( + _configureAudioCache(FlameAudio.audioCache); + _scorePool = await _createAudioPool( _prefixFile(Assets.sfx.plim), maxPlayers: 4, prefix: '', @@ -28,7 +62,7 @@ class PinballAudio { /// Plays the google word bonus void googleBonus() { - FlameAudio.audioCache.play(_prefixFile(Assets.sfx.google)); + _playSingleAudio(_prefixFile(Assets.sfx.google)); } String _prefixFile(String file) { diff --git a/packages/pinball_audio/pubspec.yaml b/packages/pinball_audio/pubspec.yaml index ab59222e..a34ba5b5 100644 --- a/packages/pinball_audio/pubspec.yaml +++ b/packages/pinball_audio/pubspec.yaml @@ -7,6 +7,7 @@ environment: sdk: ">=2.16.0 <3.0.0" dependencies: + audioplayers: ^0.20.1 flame_audio: ^1.0.1 flutter: sdk: flutter @@ -14,6 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + mocktail: ^0.3.0 very_good_analysis: ^2.4.0 flutter_gen: diff --git a/packages/pinball_audio/test/helpers/helpers.dart b/packages/pinball_audio/test/helpers/helpers.dart new file mode 100644 index 00000000..efe914f6 --- /dev/null +++ b/packages/pinball_audio/test/helpers/helpers.dart @@ -0,0 +1 @@ +export 'mocks.dart'; diff --git a/packages/pinball_audio/test/helpers/mocks.dart b/packages/pinball_audio/test/helpers/mocks.dart new file mode 100644 index 00000000..c80fe65b --- /dev/null +++ b/packages/pinball_audio/test/helpers/mocks.dart @@ -0,0 +1,34 @@ +// ignore_for_file: one_member_abstracts + +import 'package:audioplayers/audioplayers.dart'; +import 'package:flame_audio/audio_pool.dart'; +import 'package:mocktail/mocktail.dart'; + +abstract class _CreateAudioPoolStub { + Future onCall( + String sound, { + bool? repeating, + int? maxPlayers, + int? minPlayers, + String? prefix, + }); +} + +class CreateAudioPoolStub extends Mock implements _CreateAudioPoolStub {} + +abstract class _ConfigureAudioCacheStub { + void onCall(AudioCache cache); +} + +class ConfigureAudioCacheStub extends Mock implements _ConfigureAudioCacheStub { +} + +abstract class _PlaySingleAudioStub { + Future onCall(String url); +} + +class PlaySingleAudioStub extends Mock implements _PlaySingleAudioStub {} + +class MockAudioPool extends Mock implements AudioPool {} + +class MockAudioCache extends Mock implements AudioCache {} diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index a047ebc7..2efe9553 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -1,11 +1,110 @@ // ignore_for_file: prefer_const_constructors +import 'package:flame_audio/flame_audio.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/pinball_audio.dart'; +import '../helpers/helpers.dart'; + void main() { group('PinballAudio', () { test('can be instantiated', () { expect(PinballAudio(), isNotNull); }); + + late CreateAudioPoolStub createAudioPool; + late ConfigureAudioCacheStub configureAudioCache; + late PlaySingleAudioStub playSingleAudio; + late PinballAudio audio; + + setUpAll(() { + registerFallbackValue(MockAudioCache()); + }); + + setUp(() { + createAudioPool = CreateAudioPoolStub(); + when( + () => createAudioPool.onCall( + any(), + maxPlayers: any(named: 'maxPlayers'), + prefix: any(named: 'prefix'), + ), + ).thenAnswer((_) async => MockAudioPool()); + + configureAudioCache = ConfigureAudioCacheStub(); + when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); + + playSingleAudio = PlaySingleAudioStub(); + when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {}); + + audio = PinballAudio( + configureAudioCache: configureAudioCache.onCall, + createAudioPool: createAudioPool.onCall, + playSingleAudio: playSingleAudio.onCall, + ); + }); + + group('load', () { + test('creates the score pool', () async { + await audio.load(); + + verify( + () => createAudioPool.onCall( + 'packages/pinball_audio/${Assets.sfx.plim}', + maxPlayers: 4, + prefix: '', + ), + ).called(1); + }); + + test('configures the audio cache instance', () async { + await audio.load(); + + verify(() => configureAudioCache.onCall(FlameAudio.audioCache)) + .called(1); + }); + + test('sets the correct prefix', () async { + audio = PinballAudio( + createAudioPool: createAudioPool.onCall, + playSingleAudio: playSingleAudio.onCall, + ); + await audio.load(); + + expect(FlameAudio.audioCache.prefix, equals('')); + }); + }); + + group('score', () { + test('plays the score sound pool', () async { + final audioPool = MockAudioPool(); + when(audioPool.start).thenAnswer((_) async => () {}); + when( + () => createAudioPool.onCall( + any(), + maxPlayers: any(named: 'maxPlayers'), + prefix: any(named: 'prefix'), + ), + ).thenAnswer((_) async => audioPool); + + await audio.load(); + audio.score(); + + verify(audioPool.start).called(1); + }); + }); + + group('googleBonus', () { + test('plays the correct file', () async { + await audio.load(); + audio.googleBonus(); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.google}'), + ).called(1); + }); + }); }); } diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index f8415a58..01b5fea6 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -9,20 +9,26 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/landing/landing.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import '../../helpers/mocks.dart'; void main() { group('App', () { late LeaderboardRepository leaderboardRepository; + late PinballAudio pinballAudio; setUp(() { leaderboardRepository = MockLeaderboardRepository(); + pinballAudio = MockPinballAudio(); }); testWidgets('renders LandingPage', (tester) async { await tester.pumpWidget( - App(leaderboardRepository: leaderboardRepository), + App( + leaderboardRepository: leaderboardRepository, + pinballAudio: pinballAudio, + ), ); expect(find.byType(LandingPage), findsOneWidget); }); diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index 7d73b6bc..6b1af085 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -4,9 +4,11 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame/effects.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter_bloc/flutter_bloc.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 '../../helpers/helpers.dart'; @@ -89,6 +91,21 @@ void main() { }, ); + flameTester.test( + 'plays the google bonus sound', + (game) async { + when(() => state.bonusHistory).thenReturn([GameBonus.word]); + + final bonusWord = BonusWord(position: Vector2.zero()); + await game.ensureAdd(bonusWord); + await game.ready(); + + bonusWord.onNewState(state); + + verify(bonusWord.gameRef.audio.googleBonus).called(1); + }, + ); + flameTester.test( 'adds a color effect to reset the color when the sequence is finished', (game) async { @@ -195,11 +212,15 @@ void main() { group('bonus letter activation', () { late GameBloc gameBloc; + late PinballAudio pinballAudio; final flameBlocTester = FlameBlocTester( // TODO(alestiago): Use TestGame once BonusLetter has controller. gameBuilder: PinballGameTest.create, blocBuilder: () => gameBloc, + repositories: () => [ + RepositoryProvider.value(value: pinballAudio), + ], ); setUp(() { @@ -209,6 +230,9 @@ void main() { const Stream.empty(), initialState: const GameState.initial(), ); + + pinballAudio = MockPinballAudio(); + when(pinballAudio.googleBonus).thenAnswer((_) {}); }); flameBlocTester.testGameWidget( diff --git a/test/game/components/score_points_test.dart b/test/game/components/score_points_test.dart index f97bdada..8317f20c 100644 --- a/test/game/components/score_points_test.dart +++ b/test/game/components/score_points_test.dart @@ -2,6 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.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'; @@ -20,6 +21,7 @@ void main() { group('BallScorePointsCallback', () { late PinballGame game; late GameBloc bloc; + late PinballAudio audio; late Ball ball; late FakeScorePoints fakeScorePoints; @@ -27,6 +29,7 @@ void main() { game = MockPinballGame(); bloc = MockGameBloc(); ball = MockBall(); + audio = MockPinballAudio(); fakeScorePoints = FakeScorePoints(); }); @@ -38,7 +41,8 @@ void main() { test( 'emits Scored event with points', () { - when(game.read).thenReturn(bloc); + when(game.read).thenReturn(bloc); + when(() => game.audio).thenReturn(audio); BallScorePointsCallback(game).begin( ball, @@ -53,6 +57,22 @@ void main() { ).called(1); }, ); + + test( + 'plays a Score sound', + () { + when(game.read).thenReturn(bloc); + when(() => game.audio).thenReturn(audio); + + BallScorePointsCallback(game).begin( + ball, + fakeScorePoints, + FakeContact(), + ); + + verify(audio.score).called(1); + }, + ); }); }); } diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart index f78aebe7..efaedc55 100644 --- a/test/helpers/builders.dart +++ b/test/helpers/builders.dart @@ -7,13 +7,17 @@ class FlameBlocTester> FlameBlocTester({ required GameCreateFunction gameBuilder, required B Function() blocBuilder, + List Function()? repositories, }) : super( gameBuilder, pumpWidget: (gameWidget, tester) async { await tester.pumpWidget( BlocProvider.value( value: blocBuilder(), - child: gameWidget, + child: MultiRepositoryProvider( + providers: repositories?.call() ?? [], + child: gameWidget, + ), ), ); }, diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index b3c4c6f8..a5039381 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -1,6 +1,8 @@ import 'package:pinball/game/game.dart'; import 'package:pinball_theme/pinball_theme.dart'; +import 'helpers.dart'; + /// [PinballGame] extension to reduce boilerplate in tests. extension PinballGameTest on PinballGame { /// Create [PinballGame] with default [PinballTheme]. @@ -8,6 +10,7 @@ extension PinballGameTest on PinballGame { theme: const PinballTheme( characterTheme: DashTheme(), ), + audio: MockPinballAudio(), )..images.prefix = ''; } @@ -18,5 +21,6 @@ extension DebugPinballGameTest on DebugPinballGame { theme: const PinballTheme( characterTheme: DashTheme(), ), + audio: MockPinballAudio(), ); } diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index fbe8edfb..c6c2513c 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -8,6 +8,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/theme/theme.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; class MockPinballGame extends Mock implements PinballGame {} @@ -71,3 +72,5 @@ class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {} class MockComponentSet extends Mock implements ComponentSet {} class MockDashNestBumper extends Mock implements DashNestBumper {} + +class MockPinballAudio extends Mock implements PinballAudio {}