diff --git a/lib/game/behaviors/behaviors.dart b/lib/game/behaviors/behaviors.dart index 8d5ee04e..190d014e 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -4,4 +4,5 @@ export 'bonus_noise_behavior.dart'; export 'bumper_noise_behavior.dart'; export 'camera_focusing_behavior.dart'; export 'character_selection_behavior.dart'; +export 'cow_bumper_noise_behavior.dart'; export 'scoring_behavior.dart'; diff --git a/lib/game/behaviors/cow_bumper_noise_behavior.dart b/lib/game/behaviors/cow_bumper_noise_behavior.dart new file mode 100644 index 00000000..14ad1307 --- /dev/null +++ b/lib/game/behaviors/cow_bumper_noise_behavior.dart @@ -0,0 +1,13 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class CowBumperNoiseBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + readProvider().play(PinballAudio.cowMoo); + } +} diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index 902eb11c..cfaf7c7a 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -47,7 +47,7 @@ class AndroidAcres extends Component { AndroidBumper.cow( children: [ ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), + CowBumperNoiseBehavior(), ], )..initialPosition = Vector2(-20.7, -13), AndroidSpaceshipBonusBehavior(), diff --git a/packages/pinball_audio/assets/sfx/cow_moo.mp3 b/packages/pinball_audio/assets/sfx/cow_moo.mp3 new file mode 100644 index 00000000..ce69e941 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/cow_moo.mp3 differ diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart index bdd8e09e..bdb1527a 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -17,6 +17,7 @@ class $AssetsSfxGen { String get android => 'assets/sfx/android.mp3'; String get bumperA => 'assets/sfx/bumper_a.mp3'; String get bumperB => 'assets/sfx/bumper_b.mp3'; + String get cowMoo => 'assets/sfx/cow_moo.mp3'; String get dash => 'assets/sfx/dash.mp3'; String get dino => 'assets/sfx/dino.mp3'; String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3'; diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 1756d965..e8b9c8ed 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -1,32 +1,36 @@ import 'dart:math'; import 'package:audioplayers/audioplayers.dart'; +import 'package:clock/clock.dart'; import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/flame_audio.dart'; import 'package:flutter/material.dart'; import 'package:pinball_audio/gen/assets.gen.dart'; -/// Sounds available for play +/// Sounds available to play. enum PinballAudio { - /// Google + /// Google. google, - /// Bumper + /// Bumper. bumper, - /// Background music + /// Cow moo. + cowMoo, + + /// Background music. backgroundMusic, - /// IO Pinball voice over + /// IO Pinball voice over. ioPinballVoiceOver, - /// Game over + /// Game over. gameOverVoiceOver, - /// Launcher + /// Launcher. launcher, - /// Sparky + /// Sparky. sparky, /// Android @@ -145,8 +149,37 @@ class _BumperAudio extends _Audio { } } +class _ThrottledAudio extends _Audio { + _ThrottledAudio({ + required this.preCacheSingleAudio, + required this.playSingleAudio, + required this.path, + required this.duration, + }); + + final PreCacheSingleAudio preCacheSingleAudio; + final PlaySingleAudio playSingleAudio; + final String path; + final Duration duration; + + DateTime? _lastPlayed; + + @override + Future load() => preCacheSingleAudio(prefixFile(path)); + + @override + void play() { + final now = clock.now(); + if (_lastPlayed == null || + (_lastPlayed != null && now.difference(_lastPlayed!) > duration)) { + _lastPlayed = now; + playSingleAudio(prefixFile(path)); + } + } +} + /// {@template pinball_audio_player} -/// Sound manager for the pinball game +/// Sound manager for the pinball game. /// {@endtemplate} class PinballAudioPlayer { /// {@macro pinball_audio_player} @@ -212,6 +245,12 @@ class PinballAudioPlayer { createAudioPool: _createAudioPool, seed: _seed, ), + PinballAudio.cowMoo: _ThrottledAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.cowMoo, + duration: const Duration(seconds: 2), + ), PinballAudio.backgroundMusic: _LoopAudio( preCacheSingleAudio: _preCacheSingleAudio, loopSingleAudio: _loopSingleAudio, @@ -232,19 +271,19 @@ class PinballAudioPlayer { final Random _seed; - /// Registered audios on the Player + /// Registered audios on the Player. @visibleForTesting // ignore: library_private_types_in_public_api late final Map audios; - /// Loads the sounds effects into the memory + /// Loads the sounds effects into the memory. List> load() { _configureAudioCache(FlameAudio.audioCache); return audios.values.map((a) => a.load()).toList(); } - /// Plays the received audio + /// Plays the received audio. void play(PinballAudio audio) { assert( audios.containsKey(audio), diff --git a/packages/pinball_audio/pubspec.yaml b/packages/pinball_audio/pubspec.yaml index 74713dfa..8c99d1fc 100644 --- a/packages/pinball_audio/pubspec.yaml +++ b/packages/pinball_audio/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: audioplayers: ^0.20.1 + clock: ^1.1.0 flame_audio: ^1.0.1 flutter: sdk: flutter diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 8374e820..d1ff6f06 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:audioplayers/audioplayers.dart'; +import 'package:clock/clock.dart'; import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/flame_audio.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -43,6 +44,8 @@ class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {} class _MockRandom extends Mock implements Random {} +class _MockClock extends Mock implements Clock {} + void main() { group('PinballAudio', () { late _MockCreateAudioPool createAudioPool; @@ -171,6 +174,10 @@ void main() { () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/sfx/launcher.mp3'), ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/cow_moo.mp3'), + ).called(1); verify( () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/music/background.mp3'), @@ -227,6 +234,42 @@ void main() { }); }); + group('cow moo', () { + test('plays the correct file', () async { + await Future.wait(audioPlayer.load()); + audioPlayer.play(PinballAudio.cowMoo); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), + ).called(1); + }); + + test('only plays the sound again after 2 seconds', () async { + final clock = _MockClock(); + await withClock(clock, () async { + when(clock.now).thenReturn(DateTime(2022)); + await Future.wait(audioPlayer.load()); + audioPlayer + ..play(PinballAudio.cowMoo) + ..play(PinballAudio.cowMoo); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), + ).called(1); + + when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 2)); + audioPlayer.play(PinballAudio.cowMoo); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), + ).called(1); + }); + }); + }); + group('google', () { test('plays the correct file', () async { await Future.wait(audioPlayer.load()); diff --git a/test/game/behaviors/bumper_noise_behavior_test.dart b/test/game/behaviors/bumper_noise_behavior_test.dart index 58bda07d..cf6c7900 100644 --- a/test/game/behaviors/bumper_noise_behavior_test.dart +++ b/test/game/behaviors/bumper_noise_behavior_test.dart @@ -16,9 +16,7 @@ class _TestGame extends Forge2DGame { return ensureAdd( FlameProvider.value( audioPlayer, - children: [ - child, - ], + children: [child], ), ); } diff --git a/test/game/behaviors/cow_bumper_noise_behavior_test.dart b/test/game/behaviors/cow_bumper_noise_behavior_test.dart new file mode 100644 index 00000000..27a62e0b --- /dev/null +++ b/test/game/behaviors/cow_bumper_noise_behavior_test.dart @@ -0,0 +1,58 @@ +// ignore_for_file: cascade_invocations + +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/behaviors/behaviors.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + Future pump( + _TestBodyComponent child, { + required PinballAudioPlayer audioPlayer, + }) { + return ensureAdd( + FlameProvider.value( + audioPlayer, + children: [child], + ), + ); + } +} + +class _TestBodyComponent extends BodyComponent { + @override + Body createBody() => world.createBody(BodyDef()); +} + +class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} + +class _MockContact extends Mock implements Contact {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CowBumperNoiseBehavior', () { + late PinballAudioPlayer audioPlayer; + final flameTester = FlameTester(_TestGame.new); + + setUp(() { + audioPlayer = _MockPinballAudioPlayer(); + }); + flameTester.testGameWidget( + 'plays cow moo sound on contact', + setUp: (game, _) async { + final behavior = CowBumperNoiseBehavior(); + final parent = _TestBodyComponent(); + await game.pump(parent, audioPlayer: audioPlayer); + await parent.ensureAdd(behavior); + behavior.beginContact(Object(), _MockContact()); + }, + verify: (_, __) async { + verify(() => audioPlayer.play(PinballAudio.cowMoo)).called(1); + }, + ); + }); +} diff --git a/test/game/components/android_acres/android_acres_test.dart b/test/game/components/android_acres/android_acres_test.dart index cbb21f33..a99e8bd7 100644 --- a/test/game/components/android_acres/android_acres_test.dart +++ b/test/game/components/android_acres/android_acres_test.dart @@ -4,7 +4,7 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/behaviors/bumper_noise_behavior.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -117,16 +117,33 @@ void main() { ); flameTester.test( - 'three AndroidBumpers with BumperNoiseBehavior', + 'two AndroidBumpers with BumperNoiseBehavior', (game) async { await game.pump(AndroidAcres()); final bumpers = game.descendants().whereType(); + var behaviorCount = 0; for (final bumper in bumpers) { - expect( - bumper.firstChild(), - isNotNull, - ); + if (bumper.firstChild() != null) { + behaviorCount++; + } } + + expect(behaviorCount, equals(2)); + }, + ); + + flameTester.test( + 'one AndroidBumper with CowBumperNoiseBehavior', + (game) async { + await game.pump(AndroidAcres()); + final bumpers = game.descendants().whereType(); + + expect( + bumpers.singleWhere( + (bumper) => bumper.firstChild() != null, + ), + isA(), + ); }, ); });