diff --git a/.github/workflows/pinball_audio.yaml b/.github/workflows/pinball_audio.yaml new file mode 100644 index 00000000..7a43413a --- /dev/null +++ b/.github/workflows/pinball_audio.yaml @@ -0,0 +1,19 @@ +name: pinball_audio + +on: + push: + paths: + - "packages/pinball_audio/**" + - ".github/workflows/pinball_audio.yaml" + + pull_request: + paths: + - "packages/pinball_audio/**" + - ".github/workflows/pinball_audio.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/pinball_audio + coverage_excludes: "lib/gen/*.dart" diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 8de80730..521d575e 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -13,18 +13,27 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/landing/landing.dart'; +import 'package:pinball_audio/pinball_audio.dart'; class App extends StatelessWidget { - const App({Key? key, required LeaderboardRepository leaderboardRepository}) - : _leaderboardRepository = leaderboardRepository, + const App({ + Key? key, + required LeaderboardRepository leaderboardRepository, + required PinballAudio pinballAudio, + }) : _leaderboardRepository = leaderboardRepository, + _pinballAudio = pinballAudio, super(key: key); final LeaderboardRepository _leaderboardRepository; + final PinballAudio _pinballAudio; @override Widget build(BuildContext context) { - return RepositoryProvider.value( - value: _leaderboardRepository, + return MultiRepositoryProvider( + providers: [ + RepositoryProvider.value(value: _leaderboardRepository), + RepositoryProvider.value(value: _pinballAudio), + ], child: MaterialApp( title: 'I/O Pinball', theme: ThemeData( diff --git a/lib/game/components/bonus_word.dart b/lib/game/components/bonus_word.dart index e7f1626a..3457e84c 100644 --- a/lib/game/components/bonus_word.dart +++ b/lib/game/components/bonus_word.dart @@ -13,7 +13,8 @@ import 'package:pinball_components/pinball_components.dart'; /// {@template bonus_word} /// Loads all [BonusLetter]s to compose a [BonusWord]. /// {@endtemplate} -class BonusWord extends Component with BlocComponent { +class BonusWord extends Component + with BlocComponent, HasGameRef { /// {@macro bonus_word} BonusWord({required Vector2 position}) : _position = position; @@ -29,6 +30,8 @@ class BonusWord extends Component with BlocComponent { @override void onNewState(GameState state) { if (state.bonusHistory.last == GameBonus.word) { + gameRef.audio.googleBonus(); + final letters = children.whereType().toList(); for (var i = 0; i < letters.length; i++) { diff --git a/lib/game/components/score_points.dart b/lib/game/components/score_points.dart index 39910777..ce13c718 100644 --- a/lib/game/components/score_points.dart +++ b/lib/game/components/score_points.dart @@ -37,5 +37,7 @@ class BallScorePointsCallback extends ContactCallback { _gameRef.read().add( Scored(points: scorePoints.points), ); + + _gameRef.audio.score(); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 6f4a0c81..e09ab461 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 @@ -109,7 +112,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 0fa6a1ad..e50eb2d7 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_theme/pinball_theme.dart'; class PinballGamePage extends StatelessWidget { @@ -51,13 +52,24 @@ 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)) - ..preLoadAssets(); + _fetchAssets(); + } + + Future _fetchAssets() async { + final pinballAudio = context.read(); + await Future.wait([ + _game.preLoadAssets(), + pinballAudio.load(), + ]); } @override diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 97be7f3e..370d8fcf 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -3,8 +3,6 @@ /// FlutterGen /// ***************************************************** -// ignore_for_file: directives_ordering,unnecessary_import - import 'package:flutter/widgets.dart'; class $AssetsImagesGen { @@ -17,7 +15,6 @@ class $AssetsImagesGen { class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); - /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); } diff --git a/lib/main_development.dart b/lib/main_development.dart index 8673eff4..8944073d 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -8,10 +8,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/bootstrap.dart'; +import 'package:pinball_audio/pinball_audio.dart'; void main() { bootstrap((firestore) async { final leaderboardRepository = LeaderboardRepository(firestore); - return App(leaderboardRepository: leaderboardRepository); + final pinballAudio = PinballAudio(); + return App( + leaderboardRepository: leaderboardRepository, + pinballAudio: pinballAudio, + ); }); } diff --git a/lib/main_production.dart b/lib/main_production.dart index 8673eff4..8944073d 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -8,10 +8,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/bootstrap.dart'; +import 'package:pinball_audio/pinball_audio.dart'; void main() { bootstrap((firestore) async { final leaderboardRepository = LeaderboardRepository(firestore); - return App(leaderboardRepository: leaderboardRepository); + final pinballAudio = PinballAudio(); + return App( + leaderboardRepository: leaderboardRepository, + pinballAudio: pinballAudio, + ); }); } diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 8673eff4..8944073d 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -8,10 +8,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/bootstrap.dart'; +import 'package:pinball_audio/pinball_audio.dart'; void main() { bootstrap((firestore) async { final leaderboardRepository = LeaderboardRepository(firestore); - return App(leaderboardRepository: leaderboardRepository); + final pinballAudio = PinballAudio(); + return App( + leaderboardRepository: leaderboardRepository, + pinballAudio: pinballAudio, + ); }); } diff --git a/packages/pinball_audio/.gitignore b/packages/pinball_audio/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/pinball_audio/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/pinball_audio/README.md b/packages/pinball_audio/README.md new file mode 100644 index 00000000..f8b69df7 --- /dev/null +++ b/packages/pinball_audio/README.md @@ -0,0 +1,11 @@ +# pinball_audio + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Package with the sound manager for the pinball game + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/pinball_audio/analysis_options.yaml b/packages/pinball_audio/analysis_options.yaml new file mode 100644 index 00000000..f8155aa6 --- /dev/null +++ b/packages/pinball_audio/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml +analyzer: + exclude: + - lib/**/*.gen.dart diff --git a/packages/pinball_audio/assets/sfx/google.ogg b/packages/pinball_audio/assets/sfx/google.ogg new file mode 100644 index 00000000..dafaa8d4 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/google.ogg differ diff --git a/packages/pinball_audio/assets/sfx/plim.ogg b/packages/pinball_audio/assets/sfx/plim.ogg new file mode 100644 index 00000000..137c22b7 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/plim.ogg differ diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart new file mode 100644 index 00000000..3609b939 --- /dev/null +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -0,0 +1,69 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +import 'package:flutter/widgets.dart'; + +class $AssetsSfxGen { + const $AssetsSfxGen(); + + String get google => 'assets/sfx/google.ogg'; + String get plim => 'assets/sfx/plim.ogg'; +} + +class Assets { + Assets._(); + + static const $AssetsSfxGen sfx = $AssetsSfxGen(); +} + +class AssetGenImage extends AssetImage { + const AssetGenImage(String assetName) + : super(assetName, package: 'pinball_audio'); + + Image image({ + Key? key, + ImageFrameBuilder? frameBuilder, + ImageLoadingBuilder? loadingBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? width, + double? height, + Color? color, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + }) { + return Image( + key: key, + image: this, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + color: color, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + ); + } + + String get path => assetName; +} diff --git a/packages/pinball_audio/lib/pinball_audio.dart b/packages/pinball_audio/lib/pinball_audio.dart new file mode 100644 index 00000000..a72ce165 --- /dev/null +++ b/packages/pinball_audio/lib/pinball_audio.dart @@ -0,0 +1,3 @@ +library pinball_audio; + +export 'src/pinball_audio.dart'; diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart new file mode 100644 index 00000000..b2875084 --- /dev/null +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -0,0 +1,71 @@ +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({ + 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 { + _configureAudioCache(FlameAudio.audioCache); + _scorePool = await _createAudioPool( + _prefixFile(Assets.sfx.plim), + maxPlayers: 4, + prefix: '', + ); + } + + /// Plays the basic score sound + void score() { + _scorePool.start(); + } + + /// Plays the google word bonus + void googleBonus() { + _playSingleAudio(_prefixFile(Assets.sfx.google)); + } + + String _prefixFile(String file) { + return 'packages/pinball_audio/$file'; + } +} diff --git a/packages/pinball_audio/pubspec.yaml b/packages/pinball_audio/pubspec.yaml new file mode 100644 index 00000000..a34ba5b5 --- /dev/null +++ b/packages/pinball_audio/pubspec.yaml @@ -0,0 +1,28 @@ +name: pinball_audio +description: Package with the sound manager for the pinball game +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + audioplayers: ^0.20.1 + flame_audio: ^1.0.1 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^0.3.0 + very_good_analysis: ^2.4.0 + +flutter_gen: + line_length: 80 + assets: + package_parameter_enabled: true + +flutter: + assets: + - assets/sfx/ 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 new file mode 100644 index 00000000..2efe9553 --- /dev/null +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -0,0 +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/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index c928df79..48ddbaff 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -3,16 +3,12 @@ /// FlutterGen /// ***************************************************** -// ignore_for_file: directives_ordering,unnecessary_import - import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); - /// File path: assets/images/ball.png AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); - $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesChromeDinoGen get chromeDino => @@ -21,11 +17,8 @@ class $AssetsImagesGen { const $AssetsImagesDashBumperGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); - - /// File path: assets/images/flutter_sign_post.png AssetGenImage get flutterSignPost => const AssetGenImage('assets/images/flutter_sign_post.png'); - $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); @@ -34,11 +27,8 @@ class $AssetsImagesGen { class $AssetsImagesBaseboardGen { const $AssetsImagesBaseboardGen(); - /// File path: assets/images/baseboard/left.png AssetGenImage get left => const AssetGenImage('assets/images/baseboard/left.png'); - - /// File path: assets/images/baseboard/right.png AssetGenImage get right => const AssetGenImage('assets/images/baseboard/right.png'); } @@ -46,11 +36,8 @@ class $AssetsImagesBaseboardGen { class $AssetsImagesBoundaryGen { const $AssetsImagesBoundaryGen(); - /// File path: assets/images/boundary/bottom.png AssetGenImage get bottom => const AssetGenImage('assets/images/boundary/bottom.png'); - - /// File path: assets/images/boundary/outer.png AssetGenImage get outer => const AssetGenImage('assets/images/boundary/outer.png'); } @@ -58,11 +45,8 @@ class $AssetsImagesBoundaryGen { class $AssetsImagesChromeDinoGen { const $AssetsImagesChromeDinoGen(); - /// File path: assets/images/chrome_dino/head.png AssetGenImage get head => const AssetGenImage('assets/images/chrome_dino/head.png'); - - /// File path: assets/images/chrome_dino/mouth.png AssetGenImage get mouth => const AssetGenImage('assets/images/chrome_dino/mouth.png'); } @@ -79,11 +63,8 @@ class $AssetsImagesDashBumperGen { class $AssetsImagesDinoGen { const $AssetsImagesDinoGen(); - /// File path: assets/images/dino/dino-land-bottom.png AssetGenImage get dinoLandBottom => const AssetGenImage('assets/images/dino/dino-land-bottom.png'); - - /// File path: assets/images/dino/dino-land-top.png AssetGenImage get dinoLandTop => const AssetGenImage('assets/images/dino/dino-land-top.png'); } @@ -91,11 +72,8 @@ class $AssetsImagesDinoGen { class $AssetsImagesFlipperGen { const $AssetsImagesFlipperGen(); - /// File path: assets/images/flipper/left.png AssetGenImage get left => const AssetGenImage('assets/images/flipper/left.png'); - - /// File path: assets/images/flipper/right.png AssetGenImage get right => const AssetGenImage('assets/images/flipper/right.png'); } @@ -103,11 +81,8 @@ class $AssetsImagesFlipperGen { class $AssetsImagesLaunchRampGen { const $AssetsImagesLaunchRampGen(); - /// File path: assets/images/launch_ramp/foreground-railing.png AssetGenImage get foregroundRailing => const AssetGenImage('assets/images/launch_ramp/foreground-railing.png'); - - /// File path: assets/images/launch_ramp/ramp.png AssetGenImage get ramp => const AssetGenImage('assets/images/launch_ramp/ramp.png'); } @@ -115,16 +90,12 @@ class $AssetsImagesLaunchRampGen { class $AssetsImagesSpaceshipGen { const $AssetsImagesSpaceshipGen(); - /// File path: assets/images/spaceship/bridge.png AssetGenImage get bridge => const AssetGenImage('assets/images/spaceship/bridge.png'); - $AssetsImagesSpaceshipRailGen get rail => const $AssetsImagesSpaceshipRailGen(); $AssetsImagesSpaceshipRampGen get ramp => const $AssetsImagesSpaceshipRampGen(); - - /// File path: assets/images/spaceship/saucer.png AssetGenImage get saucer => const AssetGenImage('assets/images/spaceship/saucer.png'); } @@ -132,11 +103,8 @@ class $AssetsImagesSpaceshipGen { class $AssetsImagesDashBumperAGen { const $AssetsImagesDashBumperAGen(); - /// File path: assets/images/dash_bumper/a/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/a/active.png'); - - /// File path: assets/images/dash_bumper/a/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/a/inactive.png'); } @@ -144,11 +112,8 @@ class $AssetsImagesDashBumperAGen { class $AssetsImagesDashBumperBGen { const $AssetsImagesDashBumperBGen(); - /// File path: assets/images/dash_bumper/b/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/b/active.png'); - - /// File path: assets/images/dash_bumper/b/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/b/inactive.png'); } @@ -156,11 +121,8 @@ class $AssetsImagesDashBumperBGen { class $AssetsImagesDashBumperMainGen { const $AssetsImagesDashBumperMainGen(); - /// File path: assets/images/dash_bumper/main/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/main/active.png'); - - /// File path: assets/images/dash_bumper/main/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/main/inactive.png'); } @@ -168,11 +130,8 @@ class $AssetsImagesDashBumperMainGen { class $AssetsImagesSpaceshipRailGen { const $AssetsImagesSpaceshipRailGen(); - /// File path: assets/images/spaceship/rail/foreground.png AssetGenImage get foreground => const AssetGenImage('assets/images/spaceship/rail/foreground.png'); - - /// File path: assets/images/spaceship/rail/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/rail/main.png'); } @@ -180,15 +139,10 @@ class $AssetsImagesSpaceshipRailGen { class $AssetsImagesSpaceshipRampGen { const $AssetsImagesSpaceshipRampGen(); - /// File path: assets/images/spaceship/ramp/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/ramp/main.png'); - - /// File path: assets/images/spaceship/ramp/railing-background.png AssetGenImage get railingBackground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-background.png'); - - /// File path: assets/images/spaceship/ramp/railing-foreground.png AssetGenImage get railingForeground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-foreground.png'); } diff --git a/pubspec.lock b/pubspec.lock index ada9db4e..240c5a9f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + audioplayers: + dependency: transitive + description: + name: audioplayers + url: "https://pub.dartlang.org" + source: hosted + version: "0.20.1" bloc: dependency: "direct main" description: @@ -148,6 +155,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" file: dependency: transitive description: @@ -183,6 +197,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + flame_audio: + dependency: transitive + description: + name: flame_audio + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" flame_bloc: dependency: "direct main" description: @@ -259,6 +280,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" http_multi_server: dependency: transitive description: @@ -392,6 +420,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + pinball_audio: + dependency: "direct main" + description: + path: "packages/pinball_audio" + relative: true + source: path + version: "1.0.0+1" pinball_components: dependency: "direct main" description: @@ -406,6 +490,13 @@ packages: relative: true source: path version: "1.0.0+1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" plugin_platform_interface: dependency: transitive description: @@ -420,6 +511,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" provider: dependency: transitive description: @@ -509,6 +607,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -544,6 +649,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" vector_math: dependency: transitive description: @@ -586,6 +698,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" yaml: dependency: transitive description: @@ -595,4 +721,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.16.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" diff --git a/pubspec.yaml b/pubspec.yaml index a0cca553..161afb85 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,8 @@ dependencies: intl: ^0.17.0 leaderboard_repository: path: packages/leaderboard_repository + pinball_audio: + path: packages/pinball_audio pinball_components: path: packages/pinball_components pinball_theme: 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..d0eea644 100644 --- a/test/helpers/builders.dart +++ b/test/helpers/builders.dart @@ -7,13 +7,19 @@ 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: repositories == null + ? gameWidget + : 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 f139bc7b..c0dec5f5 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 {} @@ -69,3 +70,5 @@ class MockFixture extends Mock implements Fixture {} class MockComponentSet extends Mock implements ComponentSet {} class MockDashNestBumper extends Mock implements DashNestBumper {} + +class MockPinballAudio extends Mock implements PinballAudio {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index d5e819b4..722dc44c 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -14,9 +14,18 @@ import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/theme/theme.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'helpers.dart'; +PinballAudio _buildDefaultPinballAudio() { + final audio = MockPinballAudio(); + + when(audio.load).thenAnswer((_) => Future.value()); + + return audio; +} + extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { @@ -24,31 +33,41 @@ extension PumpApp on WidgetTester { GameBloc? gameBloc, ThemeCubit? themeCubit, LeaderboardRepository? leaderboardRepository, + PinballAudio? pinballAudio, }) { - return pumpWidget( - RepositoryProvider.value( - value: leaderboardRepository ?? MockLeaderboardRepository(), - child: MultiBlocProvider( + return runAsync(() { + return pumpWidget( + MultiRepositoryProvider( providers: [ - BlocProvider.value( - value: themeCubit ?? MockThemeCubit(), + RepositoryProvider.value( + value: leaderboardRepository ?? MockLeaderboardRepository(), ), - BlocProvider.value( - value: gameBloc ?? MockGameBloc(), + RepositoryProvider.value( + value: pinballAudio ?? _buildDefaultPinballAudio(), ), ], - child: MaterialApp( - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, + child: MultiBlocProvider( + providers: [ + BlocProvider.value( + value: themeCubit ?? MockThemeCubit(), + ), + BlocProvider.value( + value: gameBloc ?? MockGameBloc(), + ), ], - supportedLocales: AppLocalizations.supportedLocales, - home: navigator != null - ? MockNavigatorProvider(navigator: navigator, child: widget) - : widget, + child: MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: navigator != null + ? MockNavigatorProvider(navigator: navigator, child: widget) + : widget, + ), ), ), - ), - ); + ); + }); } }