From e90de8dedd2f7bb9e843e357fd718f792725c06f Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Tue, 10 May 2022 02:27:20 -0500 Subject: [PATCH 1/8] fix: zoom level for plunger (#453) --- lib/game/behaviors/camera_focusing_behavior.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/behaviors/camera_focusing_behavior.dart b/lib/game/behaviors/camera_focusing_behavior.dart index ec3e918f..6279f30e 100644 --- a/lib/game/behaviors/camera_focusing_behavior.dart +++ b/lib/game/behaviors/camera_focusing_behavior.dart @@ -51,7 +51,7 @@ class CameraFocusingBehavior extends Component position: _foci[GameStatus.waiting]?.position ?? Vector2(0, -112), ), GameStatus.playing: _FocusData( - zoom: size.y / 165, + zoom: size.y / 160, position: _foci[GameStatus.playing]?.position ?? Vector2(0, -7.8), ), GameStatus.gameOver: _FocusData( From 5007705e82d4ac0d6e2afaded1275019148ec5a3 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 10 May 2022 12:37:46 +0100 Subject: [PATCH 2/8] fix: enable auto-pulling for all devices (#455) --- .../components/game_bloc_status_listener.dart | 27 ++++------- lib/game/pinball_game.dart | 2 +- .../behaviors/plunger_pulling_behavior.dart | 15 ++++-- .../plunger/cubit/plunger_cubit.dart | 2 + .../plunger/cubit/plunger_state.dart | 6 ++- .../lib/src/components/plunger/plunger.dart | 1 + .../plunger_pulling_behavior_test.dart | 47 +++---------------- .../plunger/cubit/plunger_cubit_test.dart | 25 ++++++++++ .../game_bloc_status_listener_test.dart | 22 ++------- test/game/pinball_game_test.dart | 2 +- 10 files changed, 67 insertions(+), 82 deletions(-) create mode 100644 packages/pinball_components/test/src/components/plunger/cubit/plunger_cubit_test.dart diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index 6ffd2ad3..fb5fc485 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -5,7 +5,6 @@ import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import 'package:platform_helper/platform_helper.dart'; /// Listens to the [GameBloc] and updates the game accordingly. class GameBlocStatusListener extends Component @@ -68,31 +67,25 @@ class GameBlocStatusListener extends Component gameRef.descendants().whereType().single.bloc.onReset(); } - void _addPlungerBehaviors(Plunger plunger) { - final platformHelper = readProvider(); - const pullingStrength = 7.0; - final provider = - plunger.firstChild>()!; - - if (platformHelper.isMobile) { - provider.add( - PlungerAutoPullingBehavior(strength: pullingStrength), - ); - } else { - provider.addAll( + void _addPlungerBehaviors(Plunger plunger) => plunger + .firstChild>()! + .addAll( [ - PlungerKeyControllingBehavior(), - PlungerPullingBehavior(strength: pullingStrength), + PlungerPullingBehavior(strength: 7), + PlungerAutoPullingBehavior(), + PlungerKeyControllingBehavior() ], ); - } - } void _removePlungerBehaviors(Plunger plunger) { plunger .descendants() .whereType() .forEach(plunger.remove); + plunger + .descendants() + .whereType() + .forEach(plunger.remove); plunger .descendants() .whereType() diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index ad81425f..23769756 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -167,7 +167,7 @@ class PinballGame extends PinballForge2DGame .whereType>() .first .bloc - .pulled(); + .autoPulled(); } else { final tappedLeftSide = info.eventPosition.widget.x < canvasSize.x / 2; focusedBoardSide[pointerId] = diff --git a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart index db6bcaa3..7dc0d99b 100644 --- a/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart +++ b/packages/pinball_components/lib/src/components/plunger/behaviors/plunger_pulling_behavior.dart @@ -28,14 +28,19 @@ class PlungerPullingBehavior extends Component } } -class PlungerAutoPullingBehavior extends PlungerPullingBehavior { - PlungerAutoPullingBehavior({ - required double strength, - }) : super(strength: strength); +class PlungerAutoPullingBehavior extends Component + with FlameBlocReader { + late final Plunger _plunger; + + @override + Future onLoad() async { + await super.onLoad(); + _plunger = parent!.parent! as Plunger; + } @override void update(double dt) { - super.update(dt); + if (!bloc.state.isAutoPulling) return; final joint = _plunger.body.joints.whereType().single; final reachedBottom = joint.getJointTranslation() <= joint.getLowerLimit(); diff --git a/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart index ce845197..694f7c94 100644 --- a/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart +++ b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_cubit.dart @@ -8,4 +8,6 @@ class PlungerCubit extends Cubit { void pulled() => emit(PlungerState.pulling); void released() => emit(PlungerState.releasing); + + void autoPulled() => emit(PlungerState.autoPulling); } diff --git a/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart index 8b82ef96..0590d6fd 100644 --- a/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart +++ b/packages/pinball_components/lib/src/components/plunger/cubit/plunger_state.dart @@ -4,9 +4,13 @@ enum PlungerState { pulling, releasing, + + autoPulling, } extension PlungerStateX on PlungerState { - bool get isPulling => this == PlungerState.pulling; + bool get isPulling => + this == PlungerState.pulling || this == PlungerState.autoPulling; bool get isReleasing => this == PlungerState.releasing; + bool get isAutoPulling => this == PlungerState.autoPulling; } diff --git a/packages/pinball_components/lib/src/components/plunger/plunger.dart b/packages/pinball_components/lib/src/components/plunger/plunger.dart index fbb7a437..488c79bb 100644 --- a/packages/pinball_components/lib/src/components/plunger/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger/plunger.dart @@ -132,6 +132,7 @@ class _PlungerSpriteAnimationGroupComponent animations = { PlungerState.releasing: pullAnimation.reversed(), PlungerState.pulling: pullAnimation, + PlungerState.autoPulling: pullAnimation, }; current = readBloc().state; diff --git a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart index 4eec7029..c289edee 100644 --- a/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/plunger/behaviors/plunger_pulling_behavior_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; @@ -12,7 +13,7 @@ import 'package:pinball_components/pinball_components.dart'; class _TestGame extends Forge2DGame { Future pump( - PlungerPullingBehavior behavior, { + Component behavior, { PlungerCubit? plungerBloc, }) async { final plunger = Plunger.test(); @@ -85,62 +86,28 @@ void main() { group('PlungerAutoPullingBehavior', () { test('can be instantiated', () { expect( - PlungerAutoPullingBehavior(strength: 0), + PlungerAutoPullingBehavior(), isA(), ); }); flameTester.test('can be loaded', (game) async { - final behavior = PlungerAutoPullingBehavior(strength: 0); + final behavior = PlungerAutoPullingBehavior(); await game.pump(behavior); expect(game.descendants(), contains(behavior)); }); - flameTester.test( - "pulls while joint hasn't reached limit", - (game) async { - final plungerBloc = _MockPlungerCubit(); - whenListen( - plungerBloc, - Stream.value(PlungerState.pulling), - initialState: PlungerState.pulling, - ); - - const strength = 2.0; - final behavior = PlungerAutoPullingBehavior( - strength: strength, - ); - await game.pump( - behavior, - plungerBloc: plungerBloc, - ); - final plunger = behavior.ancestors().whereType().single; - final joint = _MockPrismaticJoint(); - when(joint.getJointTranslation).thenReturn(2); - when(joint.getLowerLimit).thenReturn(0); - plunger.body.joints.add(joint); - - game.update(0); - - expect(plunger.body.linearVelocity.x, equals(0)); - expect(plunger.body.linearVelocity.y, equals(strength)); - }, - ); - flameTester.test( 'releases when joint reaches limit', (game) async { final plungerBloc = _MockPlungerCubit(); whenListen( plungerBloc, - Stream.value(PlungerState.pulling), - initialState: PlungerState.pulling, + Stream.value(PlungerState.autoPulling), + initialState: PlungerState.autoPulling, ); - const strength = 2.0; - final behavior = PlungerAutoPullingBehavior( - strength: strength, - ); + final behavior = PlungerAutoPullingBehavior(); await game.pump( behavior, plungerBloc: plungerBloc, diff --git a/packages/pinball_components/test/src/components/plunger/cubit/plunger_cubit_test.dart b/packages/pinball_components/test/src/components/plunger/cubit/plunger_cubit_test.dart new file mode 100644 index 00000000..31c802f9 --- /dev/null +++ b/packages/pinball_components/test/src/components/plunger/cubit/plunger_cubit_test.dart @@ -0,0 +1,25 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('PlungerCubit', () { + test('can be instantiated', () { + expect(PlungerCubit(), isA()); + }); + + blocTest( + 'overrides previous pulling state', + build: PlungerCubit.new, + act: (cubit) => cubit + ..pulled() + ..autoPulled() + ..pulled(), + expect: () => [ + PlungerState.pulling, + PlungerState.autoPulling, + PlungerState.pulling, + ], + ); + }); +} diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart index 069731fd..4f7f7222 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -36,7 +36,6 @@ class _TestGame extends Forge2DGame with HasTappables { Future pump( Iterable children, { PinballAudioPlayer? pinballAudioPlayer, - PlatformHelper? platformHelper, GoogleWordCubit? googleWordBloc, }) async { return ensureAdd( @@ -62,7 +61,7 @@ class _TestGame extends Forge2DGame with HasTappables { _MockAppLocalizations(), ), FlameProvider.value( - platformHelper ?? PlatformHelper(), + PlatformHelper(), ), ], children: children, @@ -80,8 +79,6 @@ class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { class _MockShareRepository extends Mock implements ShareRepository {} -class _MockPlatformHelper extends Mock implements PlatformHelper {} - class _MockPlungerCubit extends Mock implements PlungerCubit {} class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {} @@ -278,7 +275,7 @@ void main() { create: PlungerCubit.new, children: [ PlungerPullingBehavior(strength: 0), - PlungerAutoPullingBehavior(strength: 0) + PlungerAutoPullingBehavior() ], ), ); @@ -460,10 +457,8 @@ void main() { ); flameTester.test( - 'adds PlungerKeyControllingBehavior to Plunger when on desktop', + 'adds PlungerKeyControllingBehavior to Plunger', (game) async { - final platformHelper = _MockPlatformHelper(); - when(() => platformHelper.isMobile).thenReturn(false); final component = GameBlocStatusListener(); final leaderboardRepository = _MockLeaderboardRepository(); final shareRepository = _MockShareRepository(); @@ -482,7 +477,6 @@ void main() { bloc: _MockSignpostCubit(), ), ], - platformHelper: platformHelper, ); await plunger.ensureAdd( FlameBlocProvider( @@ -506,10 +500,8 @@ void main() { ); flameTester.test( - 'adds PlungerPullingBehavior to Plunger when on desktop', + 'adds PlungerPullingBehavior to Plunger', (game) async { - final platformHelper = _MockPlatformHelper(); - when(() => platformHelper.isMobile).thenReturn(false); final component = GameBlocStatusListener(); final leaderboardRepository = _MockLeaderboardRepository(); final shareRepository = _MockShareRepository(); @@ -528,7 +520,6 @@ void main() { bloc: _MockSignpostCubit(), ), ], - platformHelper: platformHelper, ); await plunger.ensureAdd( FlameBlocProvider( @@ -549,10 +540,8 @@ void main() { ); flameTester.test( - 'adds PlungerAutoPullingBehavior to Plunger when on mobile', + 'adds PlungerAutoPullingBehavior to Plunger', (game) async { - final platformHelper = _MockPlatformHelper(); - when(() => platformHelper.isMobile).thenReturn(true); final component = GameBlocStatusListener(); final leaderboardRepository = _MockLeaderboardRepository(); final shareRepository = _MockShareRepository(); @@ -571,7 +560,6 @@ void main() { bloc: _MockSignpostCubit(), ), ], - platformHelper: platformHelper, ); await plunger.ensureAdd( FlameBlocProvider( diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 05b9442c..b44295ed 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -434,7 +434,7 @@ void main() { .single .bloc; - expect(plungerBloc.state, PlungerState.pulling); + expect(plungerBloc.state, PlungerState.autoPulling); }); }); }); From d2dd83ff6679d849f144f5df8c264d986c27f622 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Tue, 10 May 2022 07:59:28 -0500 Subject: [PATCH 3/8] fix: ball getting stuck under dino (#454) * fix: ball going under dino * chore: remove debug rendering * chore: unused import Co-authored-by: Alejandro Santiago Co-authored-by: Tom Arra --- lib/game/components/dino_desert/dino_desert.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/components/dino_desert/dino_desert.dart b/lib/game/components/dino_desert/dino_desert.dart index 1d7b9072..561466fb 100644 --- a/lib/game/components/dino_desert/dino_desert.dart +++ b/lib/game/components/dino_desert/dino_desert.dart @@ -42,7 +42,7 @@ class _BarrierBehindDino extends BodyComponent { Body createBody() { final shape = EdgeShape() ..set( - Vector2(25.3, -14.2), + Vector2(24.2, -14.8), Vector2(25.3, -7.7), ); From 1ee4557855993ac50c8790623bf0c54cacc05fbe Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Tue, 10 May 2022 08:57:38 -0500 Subject: [PATCH 4/8] fix: throttle dino audio (#457) --- .../pinball_audio/lib/src/pinball_audio.dart | 3 ++- .../test/src/pinball_audio_test.dart | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index c62f197d..4a3d04d8 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -279,10 +279,11 @@ class PinballAudioPlayer { playSingleAudio: _playSingleAudio, path: Assets.sfx.sparky, ), - PinballAudio.dino: _SimplePlayAudio( + PinballAudio.dino: _ThrottledAudio( preCacheSingleAudio: _preCacheSingleAudio, playSingleAudio: _playSingleAudio, path: Assets.sfx.dino, + duration: const Duration(seconds: 6), ), PinballAudio.dash: _SimplePlayAudio( preCacheSingleAudio: _preCacheSingleAudio, diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 554e9752..1c82815d 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -454,6 +454,32 @@ void main() { ), ).called(1); }); + + test('only plays the sound again after 6 seconds', () async { + final clock = _MockClock(); + await withClock(clock, () async { + when(clock.now).thenReturn(DateTime(2022)); + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); + audioPlayer + ..play(PinballAudio.dino) + ..play(PinballAudio.dino); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.dino}'), + ).called(1); + + when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 6)); + audioPlayer.play(PinballAudio.dino); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.dino}'), + ).called(1); + }); + }); }); group('android', () { From 937a18a207d3e3a913a13c836866fb9be4145207 Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Tue, 10 May 2022 16:27:29 +0200 Subject: [PATCH 5/8] fix: game controls are only allowed while the game is playing (#459) Co-authored-by: Tom Arra --- lib/game/pinball_game.dart | 5 +- test/game/pinball_game_test.dart | 92 ++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 23769756..2b003207 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -155,12 +155,11 @@ class PinballGame extends PinballForge2DGame @override void onTapDown(int pointerId, TapDownInfo info) { - if (info.raw.kind == PointerDeviceKind.touch) { + if (info.raw.kind == PointerDeviceKind.touch && + _gameBloc.state.status.isPlaying) { final rocket = descendants().whereType().first; final bounds = rocket.topLeftPosition & rocket.size; - // NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 - // we need to check it at the highest level manually. final tappedRocket = bounds.contains(info.eventPosition.game.toOffset()); if (tappedRocket) { descendants() diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index b44295ed..92608a1d 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -246,9 +246,61 @@ void main() { }); group('flipper control', () { + flameTester.test('tap control only works if game is playing', + (game) async { + await game.ready(); + + final gameBloc = game + .descendants() + .whereType>() + .first + .bloc; + + final eventPosition = _MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = _MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = _MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flipperBloc = game + .descendants() + .whereType() + .where((flipper) => flipper.side == BoardSide.left) + .single + .descendants() + .whereType>() + .first + .bloc; + + gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.gameOver)); + + game.onTapDown(0, tapDownEvent); + await Future.delayed(Duration.zero); + expect(flipperBloc.state, FlipperState.movingDown); + + gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing)); + + game.onTapDown(0, tapDownEvent); + await Future.delayed(Duration.zero); + expect(flipperBloc.state, FlipperState.movingUp); + }); + flameTester.test('tap down moves left flipper up', (game) async { await game.ready(); + final gameBloc = game + .descendants() + .whereType>() + .first + .bloc; + + gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing)); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero()); @@ -278,6 +330,14 @@ void main() { flameTester.test('tap down moves right flipper up', (game) async { await game.ready(); + final gameBloc = game + .descendants() + .whereType>() + .first + .bloc; + + gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing)); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(game.canvasSize); @@ -307,6 +367,14 @@ void main() { flameTester.test('tap up moves flipper down', (game) async { await game.ready(); + final gameBloc = game + .descendants() + .whereType>() + .first + .bloc; + + gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing)); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero()); @@ -332,6 +400,14 @@ void main() { flameTester.test('tap cancel moves flipper down', (game) async { await game.ready(); + final gameBloc = game + .descendants() + .whereType>() + .first + .bloc; + + gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing)); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.zero()); when(() => eventPosition.widget).thenReturn(Vector2.zero()); @@ -363,6 +439,14 @@ void main() { (game) async { await game.ready(); + final gameBloc = game + .descendants() + .whereType>() + .first + .bloc; + + gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing)); + final raw = _MockTapDownDetails(); when(() => raw.kind).thenReturn(PointerDeviceKind.touch); @@ -416,6 +500,14 @@ void main() { flameTester.test('plunger control tap down emits plunging', (game) async { await game.ready(); + final gameBloc = game + .descendants() + .whereType>() + .first + .bloc; + + gameBloc.emit(gameBloc.state.copyWith(status: GameStatus.playing)); + final eventPosition = _MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2(40, 60)); From a73f464afe0854b9365cdc4e14913b2d2d35842d Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Tue, 10 May 2022 10:05:19 -0500 Subject: [PATCH 6/8] refactor: increase animation cooldown (#460) Co-authored-by: Tom Arra --- lib/game/components/flutter_forest/flutter_forest.dart | 2 +- lib/game/components/sparky_scorch/sparky_scorch.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index 57bd24c4..9717ee8b 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -42,7 +42,7 @@ class FlutterForest extends Component with ZIndex { )..initialPosition = Vector2(21.8, -46.75), DashAnimatronic( children: [ - AnimatronicLoopingBehavior(animationCoolDown: 4), + AnimatronicLoopingBehavior(animationCoolDown: 11), ], )..position = Vector2(20, -66), FlutterForestBonusBehavior(), diff --git a/lib/game/components/sparky_scorch/sparky_scorch.dart b/lib/game/components/sparky_scorch/sparky_scorch.dart index c7cd71d3..2d0a239c 100644 --- a/lib/game/components/sparky_scorch/sparky_scorch.dart +++ b/lib/game/components/sparky_scorch/sparky_scorch.dart @@ -35,7 +35,7 @@ class SparkyScorch extends Component { )..initialPosition = Vector2(-3.3, -52.55), SparkyAnimatronic( children: [ - AnimatronicLoopingBehavior(animationCoolDown: 3), + AnimatronicLoopingBehavior(animationCoolDown: 8), ], )..position = Vector2(-14, -58.2), SparkyComputer( From 5492cfaef2cb95bf800756715e89cc4c04601b99 Mon Sep 17 00:00:00 2001 From: Erick Date: Tue, 10 May 2022 12:21:52 -0300 Subject: [PATCH 7/8] feat: better audio pool (#461) * feat: better audio pool * cspell * lint * typo * coverage * Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * suggestions * suggestions * pr * lint * lint * lint Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Tom Arra --- .vscode/cspell.json | 3 +- .../pinball_audio/lib/src/pinball_audio.dart | 129 ++++---- .../lib/src/pinball_audio_pool.dart | 87 ++++++ .../test/src/pinball_audio_pool_test.dart | 74 +++++ .../test/src/pinball_audio_test.dart | 284 +++++++----------- 5 files changed, 350 insertions(+), 227 deletions(-) create mode 100644 packages/pinball_audio/lib/src/pinball_audio_pool.dart create mode 100644 packages/pinball_audio/test/src/pinball_audio_pool_test.dart diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 71c54903..8c82b1d2 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -42,7 +42,8 @@ "unawaited", "unfocus", "unlayered", - "vsync" + "vsync", + "microtask" ], "ignorePaths": [ ".github/workflows/**" diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 4a3d04d8..796c8cdc 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -2,10 +2,10 @@ 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'; +import 'package:pinball_audio/src/pinball_audio_pool.dart'; /// Sounds available to play. enum PinballAudio { @@ -52,17 +52,8 @@ enum PinballAudio { flipper, } -/// Defines the contract of the creation of an [AudioPool]. -typedef CreateAudioPool = Future Function( - String sound, { - bool? repeating, - int? maxPlayers, - int? minPlayers, - String? prefix, -}); - /// Defines the contract for playing a single audio. -typedef PlaySingleAudio = Future Function(String, {double volume}); +typedef PlaySingleAudio = Future Function(String, {double volume}); /// Defines the contract for looping a single audio. typedef LoopSingleAudio = Future Function(String, {double volume}); @@ -94,13 +85,20 @@ class _SimplePlayAudio extends _Audio { final PlaySingleAudio playSingleAudio; final String path; final double? volume; + AudioPlayer? _player; @override Future load() => preCacheSingleAudio(prefixFile(path)); @override - void play() { - playSingleAudio(prefixFile(path), volume: volume ?? 1); + Future play() async { + final url = prefixFile(path); + final volume = this.volume ?? 1; + if (_player == null) { + _player = await playSingleAudio(url, volume: volume); + } else { + await _player!.play(url, volume: volume); + } } } @@ -153,81 +151,94 @@ class _SingleLoopAudio extends _LoopAudio { class _SingleAudioPool extends _Audio { _SingleAudioPool({ required this.path, - required this.createAudioPool, + required this.duration, required this.maxPlayers, + required this.preCacheSingleAudio, + required this.playSingleAudio, }); final String path; - final CreateAudioPool createAudioPool; final int maxPlayers; - late AudioPool pool; + final Duration duration; + final PreCacheSingleAudio preCacheSingleAudio; + final PlaySingleAudio playSingleAudio; + late PinballAudioPool pool; @override Future load() async { - pool = await createAudioPool( - prefixFile(path), - maxPlayers: maxPlayers, - prefix: '', + pool = PinballAudioPool( + path: prefixFile(path), + poolSize: maxPlayers, + preCacheSingleAudio: preCacheSingleAudio, + playSingleAudio: playSingleAudio, + duration: duration, ); + await pool.load(); } @override - void play() => pool.start(); + void play() => pool.play(); } class _RandomABAudio extends _Audio { _RandomABAudio({ - required this.createAudioPool, + required this.preCacheSingleAudio, + required this.playSingleAudio, required this.seed, required this.audioAssetA, required this.audioAssetB, + required this.duration, this.volume, }); - final CreateAudioPool createAudioPool; + final PreCacheSingleAudio preCacheSingleAudio; + final PlaySingleAudio playSingleAudio; final Random seed; final String audioAssetA; final String audioAssetB; + final Duration duration; final double? volume; - late AudioPool audioA; - late AudioPool audioB; + late PinballAudioPool audioA; + late PinballAudioPool audioB; @override Future load() async { - await Future.wait( - [ - createAudioPool( - prefixFile(audioAssetA), - maxPlayers: 4, - prefix: '', - ).then((pool) => audioA = pool), - createAudioPool( - prefixFile(audioAssetB), - maxPlayers: 4, - prefix: '', - ).then((pool) => audioB = pool), - ], + audioA = PinballAudioPool( + path: prefixFile(audioAssetA), + poolSize: 4, + preCacheSingleAudio: preCacheSingleAudio, + playSingleAudio: playSingleAudio, + duration: duration, ); + audioB = PinballAudioPool( + path: prefixFile(audioAssetB), + poolSize: 4, + preCacheSingleAudio: preCacheSingleAudio, + playSingleAudio: playSingleAudio, + duration: duration, + ); + await Future.wait([audioA.load(), audioB.load()]); } @override void play() { - (seed.nextBool() ? audioA : audioB).start(volume: volume ?? 1); + (seed.nextBool() ? audioA : audioB).play(volume: volume ?? 1); } } -class _ThrottledAudio extends _Audio { +class _ThrottledAudio extends _SimplePlayAudio { _ThrottledAudio({ - required this.preCacheSingleAudio, - required this.playSingleAudio, - required this.path, + required PreCacheSingleAudio preCacheSingleAudio, + required PlaySingleAudio playSingleAudio, + required String path, required this.duration, - }); + }) : super( + preCacheSingleAudio: preCacheSingleAudio, + playSingleAudio: playSingleAudio, + path: path, + ); - final PreCacheSingleAudio preCacheSingleAudio; - final PlaySingleAudio playSingleAudio; - final String path; final Duration duration; DateTime? _lastPlayed; @@ -236,12 +247,12 @@ class _ThrottledAudio extends _Audio { Future load() => preCacheSingleAudio(prefixFile(path)); @override - void play() { + Future play() async { final now = clock.now(); if (_lastPlayed == null || (_lastPlayed != null && now.difference(_lastPlayed!) > duration)) { _lastPlayed = now; - playSingleAudio(prefixFile(path)); + await super.play(); } } } @@ -252,14 +263,12 @@ class _ThrottledAudio extends _Audio { class PinballAudioPlayer { /// {@macro pinball_audio_player} PinballAudioPlayer({ - CreateAudioPool? createAudioPool, PlaySingleAudio? playSingleAudio, LoopSingleAudio? loopSingleAudio, PreCacheSingleAudio? preCacheSingleAudio, ConfigureAudioCache? configureAudioCache, Random? seed, - }) : _createAudioPool = createAudioPool ?? AudioPool.create, - _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play, + }) : _playSingleAudio = playSingleAudio ?? FlameAudio.audioCache.play, _loopSingleAudio = loopSingleAudio ?? FlameAudio.audioCache.loop, _preCacheSingleAudio = preCacheSingleAudio ?? FlameAudio.audioCache.load, @@ -308,8 +317,10 @@ class PinballAudioPlayer { ), PinballAudio.flipper: _SingleAudioPool( path: Assets.sfx.flipper, - createAudioPool: _createAudioPool, - maxPlayers: 2, + maxPlayers: 4, + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + duration: const Duration(milliseconds: 200), ), PinballAudio.ioPinballVoiceOver: _SimplePlayAudio( preCacheSingleAudio: _preCacheSingleAudio, @@ -322,17 +333,21 @@ class PinballAudioPlayer { path: Assets.sfx.gameOverVoiceOver, ), PinballAudio.bumper: _RandomABAudio( - createAudioPool: _createAudioPool, + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, seed: _seed, audioAssetA: Assets.sfx.bumperA, audioAssetB: Assets.sfx.bumperB, + duration: const Duration(seconds: 1), volume: 0.6, ), PinballAudio.kicker: _RandomABAudio( - createAudioPool: _createAudioPool, + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, seed: _seed, audioAssetA: Assets.sfx.kickerA, audioAssetB: Assets.sfx.kickerB, + duration: const Duration(seconds: 1), volume: 0.6, ), PinballAudio.cowMoo: _ThrottledAudio( @@ -350,8 +365,6 @@ class PinballAudioPlayer { }; } - final CreateAudioPool _createAudioPool; - final PlaySingleAudio _playSingleAudio; final LoopSingleAudio _loopSingleAudio; diff --git a/packages/pinball_audio/lib/src/pinball_audio_pool.dart b/packages/pinball_audio/lib/src/pinball_audio_pool.dart new file mode 100644 index 00000000..659d325e --- /dev/null +++ b/packages/pinball_audio/lib/src/pinball_audio_pool.dart @@ -0,0 +1,87 @@ +import 'dart:async'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:pinball_audio/pinball_audio.dart'; + +class _PlayerEntry { + _PlayerEntry({ + required this.available, + required this.player, + }); + + bool available; + final AudioPlayer player; +} + +/// {@template pinball_audio_pool} +/// Creates an audio player pool used to trigger many sounds at the same time. +/// {@endtemplate} +class PinballAudioPool { + /// {@macro pinball_audio_pool} + PinballAudioPool({ + required this.path, + required this.poolSize, + required this.preCacheSingleAudio, + required this.playSingleAudio, + required this.duration, + }); + + /// Sounds path. + final String path; + + /// Max size of this pool. + final int poolSize; + + /// Function to cache audios. + final PreCacheSingleAudio preCacheSingleAudio; + + /// Function to play audios. + final PlaySingleAudio playSingleAudio; + + /// How long the sound lasts. + final Duration duration; + + final List<_PlayerEntry> _players = []; + + /// Loads the pool. + Future load() async { + await preCacheSingleAudio(path); + } + + /// Plays the pool. + Future play({double volume = 1}) async { + AudioPlayer? player; + if (_players.length < poolSize) { + _players.add( + _PlayerEntry( + available: false, + player: player = await playSingleAudio(path, volume: volume), + ), + ); + } else { + final entries = _players.where((entry) => entry.available); + if (entries.isNotEmpty) { + final entry = entries.first..available = false; + + player = entry.player; + unawaited(entry.player.play(path, volume: volume)); + } + } + + if (player != null) { + unawaited( + Future.delayed(duration).then( + (_) { + _returnEntryAvailability(player!); + }, + ), + ); + } else {} + } + + void _returnEntryAvailability( + AudioPlayer player, + ) { + _players.where((entry) => entry.player == player).single.available = true; + } +} diff --git a/packages/pinball_audio/test/src/pinball_audio_pool_test.dart b/packages/pinball_audio/test/src/pinball_audio_pool_test.dart new file mode 100644 index 00000000..3b132808 --- /dev/null +++ b/packages/pinball_audio/test/src/pinball_audio_pool_test.dart @@ -0,0 +1,74 @@ +// ignore_for_file: one_member_abstracts + +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_audio/src/pinball_audio_pool.dart'; + +class _MockAudioPlayer extends Mock implements AudioPlayer {} + +class _MockPlaySingleAudio extends Mock { + Future onCall(String path, {double volume}); +} + +abstract class _PreCacheSingleAudio { + Future onCall(String path); +} + +class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {} + +void main() { + group('PinballAudioPool', () { + late _PreCacheSingleAudio preCacheSingleAudio; + late _MockPlaySingleAudio playSingleAudio; + late PinballAudioPool pool; + late AudioPlayer audioPlayer; + + setUp(() { + preCacheSingleAudio = _MockPreCacheSingleAudio(); + when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {}); + + audioPlayer = _MockAudioPlayer(); + when(() => audioPlayer.play(any(), volume: any(named: 'volume'))) + .thenAnswer((_) async => 1); + + playSingleAudio = _MockPlaySingleAudio(); + when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume'))) + .thenAnswer((_) async => audioPlayer); + + pool = PinballAudioPool( + path: 'path', + poolSize: 1, + preCacheSingleAudio: preCacheSingleAudio.onCall, + playSingleAudio: playSingleAudio.onCall, + duration: const Duration(milliseconds: 10), + ); + }); + + test('pre cache the sound', () async { + await pool.load(); + verify(() => preCacheSingleAudio.onCall('path')).called(1); + }); + + test('plays a fresh sound', () async { + await pool.load(); + await pool.play(); + + verify( + () => playSingleAudio.onCall( + 'path', + volume: any(named: 'volume'), + ), + ).called(1); + }); + + test('plays from the pool after it returned', () async { + await pool.load(); + await pool.play(); + await Future.delayed(const Duration(milliseconds: 12)); + await pool.play(); + + verify(() => audioPlayer.play('path')).called(1); + }); + }); +} diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 1c82815d..9fd5b8e7 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -3,33 +3,22 @@ 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'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/pinball_audio.dart'; -class _MockAudioPool extends Mock implements AudioPool {} - class _MockAudioCache extends Mock implements AudioCache {} -class _MockCreateAudioPool extends Mock { - Future onCall( - String sound, { - bool? repeating, - int? maxPlayers, - int? minPlayers, - String? prefix, - }); -} +class _MockAudioPlayer extends Mock implements AudioPlayer {} class _MockConfigureAudioCache extends Mock { void onCall(AudioCache cache); } class _MockPlaySingleAudio extends Mock { - Future onCall(String path, {double volume}); + Future onCall(String path, {double volume}); } class _MockLoopSingleAudio extends Mock { @@ -48,7 +37,6 @@ class _MockClock extends Mock implements Clock {} void main() { group('PinballAudio', () { - late _MockCreateAudioPool createAudioPool; late _MockConfigureAudioCache configureAudioCache; late _MockPlaySingleAudio playSingleAudio; late _MockLoopSingleAudio loopSingleAudio; @@ -61,21 +49,12 @@ void main() { }); setUp(() { - createAudioPool = _MockCreateAudioPool(); - when( - () => createAudioPool.onCall( - any(), - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => _MockAudioPool()); - configureAudioCache = _MockConfigureAudioCache(); when(() => configureAudioCache.onCall(any())).thenAnswer((_) {}); playSingleAudio = _MockPlaySingleAudio(); when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume'))) - .thenAnswer((_) async {}); + .thenAnswer((_) async => _MockAudioPlayer()); loopSingleAudio = _MockLoopSingleAudio(); when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume'))) @@ -88,7 +67,6 @@ void main() { audioPlayer = PinballAudioPlayer( configureAudioCache: configureAudioCache.onCall, - createAudioPool: createAudioPool.onCall, playSingleAudio: playSingleAudio.onCall, loopSingleAudio: loopSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall, @@ -101,64 +79,6 @@ void main() { }); group('load', () { - test('creates the bumpers pools', () async { - await Future.wait( - audioPlayer.load().map((loadableBuilder) => loadableBuilder()), - ); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.bumperA}', - maxPlayers: 4, - prefix: '', - ), - ).called(1); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.bumperB}', - maxPlayers: 4, - prefix: '', - ), - ).called(1); - }); - - test('creates the kicker pools', () async { - await Future.wait( - audioPlayer.load().map((loadableBuilder) => loadableBuilder()), - ); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.kickerA}', - maxPlayers: 4, - prefix: '', - ), - ).called(1); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.kickerB}', - maxPlayers: 4, - prefix: '', - ), - ).called(1); - }); - - test('creates the flipper pool', () async { - await Future.wait( - audioPlayer.load().map((loadableBuilder) => loadableBuilder()), - ); - - verify( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.flipper}', - maxPlayers: 2, - prefix: '', - ), - ).called(1); - }); - test('configures the audio cache instance', () async { await Future.wait( audioPlayer.load().map((loadableBuilder) => loadableBuilder()), @@ -170,7 +90,6 @@ void main() { test('sets the correct prefix', () async { audioPlayer = PinballAudioPlayer( - createAudioPool: createAudioPool.onCall, playSingleAudio: playSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall, ); @@ -186,6 +105,26 @@ void main() { audioPlayer.load().map((loadableBuilder) => loadableBuilder()), ); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/bumper_a.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/bumper_b.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/kicker_a.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/kicker_b.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/flipper.mp3'), + ).called(1); verify( () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/sfx/google.mp3'), @@ -236,33 +175,6 @@ void main() { }); group('bumper', () { - late AudioPool bumperAPool; - late AudioPool bumperBPool; - - setUp(() { - bumperAPool = _MockAudioPool(); - when(() => bumperAPool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.bumperA}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => bumperAPool); - - bumperBPool = _MockAudioPool(); - when(() => bumperBPool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.bumperB}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => bumperBPool); - }); - group('when seed is true', () { test('plays the bumper A sound pool', () async { when(seed.nextBool).thenReturn(true); @@ -271,7 +183,12 @@ void main() { ); audioPlayer.play(PinballAudio.bumper); - verify(() => bumperAPool.start(volume: 0.6)).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.bumperA}', + volume: 0.6, + ), + ).called(1); }); }); @@ -283,39 +200,17 @@ void main() { ); audioPlayer.play(PinballAudio.bumper); - verify(() => bumperBPool.start(volume: 0.6)).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.bumperB}', + volume: 0.6, + ), + ).called(1); }); }); }); group('kicker', () { - late AudioPool kickerAPool; - late AudioPool kickerBPool; - - setUp(() { - kickerAPool = _MockAudioPool(); - when(() => kickerAPool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.kickerA}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => kickerAPool); - - kickerBPool = _MockAudioPool(); - when(() => kickerBPool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.kickerB}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => kickerBPool); - }); - group('when seed is true', () { test('plays the kicker A sound pool', () async { when(seed.nextBool).thenReturn(true); @@ -324,7 +219,12 @@ void main() { ); audioPlayer.play(PinballAudio.kicker); - verify(() => kickerAPool.start(volume: 0.6)).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.kickerA}', + volume: 0.6, + ), + ).called(1); }); }); @@ -336,27 +236,17 @@ void main() { ); audioPlayer.play(PinballAudio.kicker); - verify(() => kickerBPool.start(volume: 0.6)).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.kickerB}', + volume: 0.6, + ), + ).called(1); }); }); }); group('flipper', () { - late AudioPool pool; - - setUp(() { - pool = _MockAudioPool(); - when(() => pool.start(volume: any(named: 'volume'))) - .thenAnswer((_) async => () {}); - when( - () => createAudioPool.onCall( - 'packages/pinball_audio/${Assets.sfx.flipper}', - maxPlayers: any(named: 'maxPlayers'), - prefix: any(named: 'prefix'), - ), - ).thenAnswer((_) async => pool); - }); - test('plays the flipper sound pool', () async { when(seed.nextBool).thenReturn(true); await Future.wait( @@ -364,7 +254,12 @@ void main() { ); audioPlayer.play(PinballAudio.flipper); - verify(() => pool.start()).called(1); + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.flipper}', + volume: any(named: 'volume'), + ), + ).called(1); }); }); @@ -376,14 +271,21 @@ void main() { audioPlayer.play(PinballAudio.cowMoo); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.cowMoo}', + volume: any(named: 'volume'), + ), ).called(1); }); test('only plays the sound again after 2 seconds', () async { final clock = _MockClock(); await withClock(clock, () async { + final audioPlayerInstance = _MockAudioPlayer(); + when( + () => playSingleAudio.onCall(any(), volume: any(named: 'volume')), + ).thenAnswer((_) async => audioPlayerInstance); + when(clock.now).thenReturn(DateTime(2022)); await Future.wait( audioPlayer.load().map((loadableBuilder) => loadableBuilder()), @@ -393,16 +295,20 @@ void main() { ..play(PinballAudio.cowMoo); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.cowMoo}', + volume: any(named: 'volume'), + ), ).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}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.cowMoo}', + volume: any(named: 'volume'), + ), ).called(1); }); }); @@ -422,6 +328,44 @@ void main() { ), ).called(1); }); + + test('uses the cached player on the second time', () async { + final audioPlayerCache = _MockAudioPlayer(); + when(() => audioPlayerCache.play(any(), volume: any(named: 'volume'))) + .thenAnswer((_) async => 0); + + when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume'))) + .thenAnswer((_) async => audioPlayerCache); + audioPlayer = PinballAudioPlayer( + configureAudioCache: configureAudioCache.onCall, + playSingleAudio: playSingleAudio.onCall, + loopSingleAudio: loopSingleAudio.onCall, + preCacheSingleAudio: preCacheSingleAudio.onCall, + seed: seed, + ); + + await Future.wait( + audioPlayer.load().map((loadableBuilder) => loadableBuilder()), + ); + audioPlayer.play(PinballAudio.google); + + verify( + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.google}', + volume: any(named: 'volume'), + ), + ).called(1); + + await Future.microtask(() {}); + + audioPlayer.play(PinballAudio.google); + verify( + () => audioPlayerCache.play( + 'packages/pinball_audio/${Assets.sfx.google}', + volume: any(named: 'volume'), + ), + ).called(1); + }); }); group('sparky', () { @@ -467,16 +411,20 @@ void main() { ..play(PinballAudio.dino); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.dino}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.dino}', + volume: any(named: 'volume'), + ), ).called(1); when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 6)); audioPlayer.play(PinballAudio.dino); verify( - () => playSingleAudio - .onCall('packages/pinball_audio/${Assets.sfx.dino}'), + () => playSingleAudio.onCall( + 'packages/pinball_audio/${Assets.sfx.dino}', + volume: any(named: 'volume'), + ), ).called(1); }); }); From 86980a70353ea0d43b59f50352dc1cf5ae68a3f3 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Tue, 10 May 2022 10:31:44 -0500 Subject: [PATCH 8/8] fix: ball theming on mobile (#462) --- .../character_selection_behavior.dart | 16 +++++--- .../character_selection_behavior_test.dart | 40 ++++++++++++++++++- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/lib/game/behaviors/character_selection_behavior.dart b/lib/game/behaviors/character_selection_behavior.dart index 27003d75..e62438f6 100644 --- a/lib/game/behaviors/character_selection_behavior.dart +++ b/lib/game/behaviors/character_selection_behavior.dart @@ -2,6 +2,8 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:platform_helper/platform_helper.dart'; /// Updates the [ArcadeBackground] and launch [Ball] to reflect character /// selections. @@ -11,12 +13,14 @@ class CharacterSelectionBehavior extends Component HasGameRef { @override void onNewState(CharacterThemeState state) { - gameRef - .descendants() - .whereType() - .single - .bloc - .onCharacterSelected(state.characterTheme); + if (!readProvider().isMobile) { + gameRef + .descendants() + .whereType() + .single + .bloc + .onCharacterSelected(state.characterTheme); + } gameRef .descendants() .whereType() diff --git a/test/game/behaviors/character_selection_behavior_test.dart b/test/game/behaviors/character_selection_behavior_test.dart index acf140a2..7fe5439f 100644 --- a/test/game/behaviors/character_selection_behavior_test.dart +++ b/test/game/behaviors/character_selection_behavior_test.dart @@ -77,8 +77,45 @@ void main() { ); flameTester.test( - 'onNewState calls onCharacterSelected on the arcade background bloc', + 'onNewState does not call onCharacterSelected on the arcade background ' + 'bloc when platform is mobile', (game) async { + final platformHelper = _MockPlatformHelper(); + when(() => platformHelper.isMobile).thenAnswer((_) => true); + final arcadeBackgroundBloc = _MockArcadeBackgroundCubit(); + whenListen( + arcadeBackgroundBloc, + const Stream.empty(), + initialState: const ArcadeBackgroundState.initial(), + ); + final behavior = CharacterSelectionBehavior(); + await game.pump( + [ + behavior, + ZCanvasComponent(), + Plunger.test(), + Ball.test(), + ], + platformHelper: platformHelper, + ); + + const dinoThemeState = CharacterThemeState(theme.DinoTheme()); + behavior.onNewState(dinoThemeState); + await game.ready(); + + verifyNever( + () => arcadeBackgroundBloc + .onCharacterSelected(dinoThemeState.characterTheme), + ); + }, + ); + + flameTester.test( + 'onNewState calls onCharacterSelected on the arcade background ' + 'bloc when platform is not mobile', + (game) async { + final platformHelper = _MockPlatformHelper(); + when(() => platformHelper.isMobile).thenAnswer((_) => false); final arcadeBackgroundBloc = _MockArcadeBackgroundCubit(); whenListen( arcadeBackgroundBloc, @@ -96,6 +133,7 @@ void main() { Plunger.test(), Ball.test(), ], + platformHelper: platformHelper, ); const dinoThemeState = CharacterThemeState(theme.DinoTheme());