feat: adding sound effects

pull/143/head
Erick Zanardo 4 years ago
parent b72ff3178f
commit 631518395b

@ -8,7 +8,6 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
/// {@template bonus_word} /// {@template bonus_word}
@ -31,7 +30,7 @@ class BonusWord extends Component
@override @override
void onNewState(GameState state) { void onNewState(GameState state) {
if (state.bonusHistory.last == GameBonus.word) { if (state.bonusHistory.last == GameBonus.word) {
gameRef.read<PinballAudio>().googleBonus(); gameRef.audio.googleBonus();
final letters = children.whereType<BonusLetter>().toList(); final letters = children.whereType<BonusLetter>().toList();

@ -39,6 +39,6 @@ class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
Scored(points: scorePoints.points), Scored(points: scorePoints.points),
); );
_gameRef.read<PinballAudio>().score(); _gameRef.audio.score();
} }
} }

@ -8,17 +8,20 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.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_components/pinball_components.dart' hide Assets;
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class PinballGame extends Forge2DGame class PinballGame extends Forge2DGame
with FlameBloc, HasKeyboardHandlerComponents { with FlameBloc, HasKeyboardHandlerComponents {
PinballGame({required this.theme}) { PinballGame({required this.theme, required this.audio}) {
images.prefix = ''; images.prefix = '';
} }
final PinballTheme theme; final PinballTheme theme;
final PinballAudio audio;
late final Plunger plunger; late final Plunger plunger;
@override @override
@ -110,7 +113,13 @@ class PinballGame extends Forge2DGame
} }
class DebugPinballGame extends PinballGame with TapDetector { 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 @override
Future<void> onLoad() async { Future<void> onLoad() async {

@ -53,13 +53,15 @@ class _PinballGameViewState extends State<PinballGameView> {
void initState() { void initState() {
super.initState(); super.initState();
final audio = context.read<PinballAudio>();
_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 // 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 // this could expose a Stream (maybe even a cubit?) so we could show the
// the loading progress with some fancy widgets. // the loading progress with some fancy widgets.
_game = widget._isDebugMode
? DebugPinballGame(theme: widget.theme)
: PinballGame(theme: widget.theme);
_fetchAssets(); _fetchAssets();
} }

@ -1,20 +1,54 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/gen/assets.gen.dart';
/// Function that defines the contract of the creation
/// of an [AudioPool]
typedef CreateAudioPool = Future<AudioPool> Function(
String sound, {
bool? repeating,
int? maxPlayers,
int? minPlayers,
String? prefix,
});
/// Function that defines the contract for playing a single
/// audio
typedef PlaySingleAudio = Future<void> Function(String);
/// Function that defines the contract for configuring
/// an [AudioCache] instance
typedef ConfigureAudioCache = void Function(AudioCache);
/// {@template pinball_audio} /// {@template pinball_audio}
/// Sound manager for the pinball game /// Sound manager for the pinball game
/// {@endtemplate} /// {@endtemplate}
class PinballAudio { class PinballAudio {
/// {@macro pinball_audio} /// {@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; late AudioPool _scorePool;
/// Loads the sounds effects into the memory /// Loads the sounds effects into the memory
Future<void> load() async { Future<void> load() async {
FlameAudio.audioCache.prefix = ''; _configureAudioCache(FlameAudio.audioCache);
_scorePool = await AudioPool.create( _scorePool = await _createAudioPool(
_prefixFile(Assets.sfx.plim), _prefixFile(Assets.sfx.plim),
maxPlayers: 4, maxPlayers: 4,
prefix: '', prefix: '',
@ -28,7 +62,7 @@ class PinballAudio {
/// Plays the google word bonus /// Plays the google word bonus
void googleBonus() { void googleBonus() {
FlameAudio.audioCache.play(_prefixFile(Assets.sfx.google)); _playSingleAudio(_prefixFile(Assets.sfx.google));
} }
String _prefixFile(String file) { String _prefixFile(String file) {

@ -7,6 +7,7 @@ environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.16.0 <3.0.0"
dependencies: dependencies:
audioplayers: ^0.20.1
flame_audio: ^1.0.1 flame_audio: ^1.0.1
flutter: flutter:
sdk: flutter sdk: flutter
@ -14,6 +15,7 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mocktail: ^0.3.0
very_good_analysis: ^2.4.0 very_good_analysis: ^2.4.0
flutter_gen: flutter_gen:

@ -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<AudioPool> 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<void> onCall(String url);
}
class PlaySingleAudioStub extends Mock implements _PlaySingleAudioStub {}
class MockAudioPool extends Mock implements AudioPool {}
class MockAudioCache extends Mock implements AudioCache {}

@ -1,11 +1,110 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors
import 'package:flame_audio/flame_audio.dart';
import 'package:flutter_test/flutter_test.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 'package:pinball_audio/pinball_audio.dart';
import '../helpers/helpers.dart';
void main() { void main() {
group('PinballAudio', () { group('PinballAudio', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(PinballAudio(), isNotNull); 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);
});
});
}); });
} }

@ -9,20 +9,26 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart'; import 'package:pinball/app/app.dart';
import 'package:pinball/landing/landing.dart'; import 'package:pinball/landing/landing.dart';
import 'package:pinball_audio/pinball_audio.dart';
import '../../helpers/mocks.dart'; import '../../helpers/mocks.dart';
void main() { void main() {
group('App', () { group('App', () {
late LeaderboardRepository leaderboardRepository; late LeaderboardRepository leaderboardRepository;
late PinballAudio pinballAudio;
setUp(() { setUp(() {
leaderboardRepository = MockLeaderboardRepository(); leaderboardRepository = MockLeaderboardRepository();
pinballAudio = MockPinballAudio();
}); });
testWidgets('renders LandingPage', (tester) async { testWidgets('renders LandingPage', (tester) async {
await tester.pumpWidget( await tester.pumpWidget(
App(leaderboardRepository: leaderboardRepository), App(
leaderboardRepository: leaderboardRepository,
pinballAudio: pinballAudio,
),
); );
expect(find.byType(LandingPage), findsOneWidget); expect(find.byType(LandingPage), findsOneWidget);
}); });

@ -4,9 +4,11 @@ import 'package:bloc_test/bloc_test.dart';
import 'package:flame/effects.dart'; import 'package:flame/effects.dart';
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_bloc/flutter_bloc.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 '../../helpers/helpers.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( flameTester.test(
'adds a color effect to reset the color when the sequence is finished', 'adds a color effect to reset the color when the sequence is finished',
(game) async { (game) async {
@ -195,11 +212,15 @@ void main() {
group('bonus letter activation', () { group('bonus letter activation', () {
late GameBloc gameBloc; late GameBloc gameBloc;
late PinballAudio pinballAudio;
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
// TODO(alestiago): Use TestGame once BonusLetter has controller. // TODO(alestiago): Use TestGame once BonusLetter has controller.
gameBuilder: PinballGameTest.create, gameBuilder: PinballGameTest.create,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
repositories: () => [
RepositoryProvider<PinballAudio>.value(value: pinballAudio),
],
); );
setUp(() { setUp(() {
@ -209,6 +230,9 @@ void main() {
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
initialState: const GameState.initial(), initialState: const GameState.initial(),
); );
pinballAudio = MockPinballAudio();
when(pinballAudio.googleBonus).thenAnswer((_) {});
}); });
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(

@ -2,6 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.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';
@ -20,6 +21,7 @@ void main() {
group('BallScorePointsCallback', () { group('BallScorePointsCallback', () {
late PinballGame game; late PinballGame game;
late GameBloc bloc; late GameBloc bloc;
late PinballAudio audio;
late Ball ball; late Ball ball;
late FakeScorePoints fakeScorePoints; late FakeScorePoints fakeScorePoints;
@ -27,6 +29,7 @@ void main() {
game = MockPinballGame(); game = MockPinballGame();
bloc = MockGameBloc(); bloc = MockGameBloc();
ball = MockBall(); ball = MockBall();
audio = MockPinballAudio();
fakeScorePoints = FakeScorePoints(); fakeScorePoints = FakeScorePoints();
}); });
@ -38,7 +41,8 @@ void main() {
test( test(
'emits Scored event with points', 'emits Scored event with points',
() { () {
when<GameBloc>(game.read).thenReturn(bloc); when(game.read<GameBloc>).thenReturn(bloc);
when(() => game.audio).thenReturn(audio);
BallScorePointsCallback(game).begin( BallScorePointsCallback(game).begin(
ball, ball,
@ -53,6 +57,22 @@ void main() {
).called(1); ).called(1);
}, },
); );
test(
'plays a Score sound',
() {
when(game.read<GameBloc>).thenReturn(bloc);
when(() => game.audio).thenReturn(audio);
BallScorePointsCallback(game).begin(
ball,
fakeScorePoints,
FakeContact(),
);
verify(audio.score).called(1);
},
);
}); });
}); });
} }

@ -7,14 +7,18 @@ class FlameBlocTester<T extends FlameGame, B extends Bloc<dynamic, dynamic>>
FlameBlocTester({ FlameBlocTester({
required GameCreateFunction<T> gameBuilder, required GameCreateFunction<T> gameBuilder,
required B Function() blocBuilder, required B Function() blocBuilder,
List<RepositoryProvider> Function()? repositories,
}) : super( }) : super(
gameBuilder, gameBuilder,
pumpWidget: (gameWidget, tester) async { pumpWidget: (gameWidget, tester) async {
await tester.pumpWidget( await tester.pumpWidget(
BlocProvider.value( BlocProvider.value(
value: blocBuilder(), value: blocBuilder(),
child: MultiRepositoryProvider(
providers: repositories?.call() ?? [],
child: gameWidget, child: gameWidget,
), ),
),
); );
}, },
); );

@ -1,6 +1,8 @@
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import 'helpers.dart';
/// [PinballGame] extension to reduce boilerplate in tests. /// [PinballGame] extension to reduce boilerplate in tests.
extension PinballGameTest on PinballGame { extension PinballGameTest on PinballGame {
/// Create [PinballGame] with default [PinballTheme]. /// Create [PinballGame] with default [PinballTheme].
@ -8,6 +10,7 @@ extension PinballGameTest on PinballGame {
theme: const PinballTheme( theme: const PinballTheme(
characterTheme: DashTheme(), characterTheme: DashTheme(),
), ),
audio: MockPinballAudio(),
)..images.prefix = ''; )..images.prefix = '';
} }
@ -18,5 +21,6 @@ extension DebugPinballGameTest on DebugPinballGame {
theme: const PinballTheme( theme: const PinballTheme(
characterTheme: DashTheme(), characterTheme: DashTheme(),
), ),
audio: MockPinballAudio(),
); );
} }

@ -8,6 +8,7 @@ import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball/theme/theme.dart'; import 'package:pinball/theme/theme.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
class MockPinballGame extends Mock implements PinballGame {} class MockPinballGame extends Mock implements PinballGame {}
@ -71,3 +72,5 @@ class MockSpaceshipExitRailEnd extends Mock implements SpaceshipExitRailEnd {}
class MockComponentSet extends Mock implements ComponentSet {} class MockComponentSet extends Mock implements ComponentSet {}
class MockDashNestBumper extends Mock implements DashNestBumper {} class MockDashNestBumper extends Mock implements DashNestBumper {}
class MockPinballAudio extends Mock implements PinballAudio {}

Loading…
Cancel
Save