feat: adds sound effects (#143)

* feat: adding placeholder sound effects

* feat: adding sound effects

* fix: lint

* fix: adding pinball audio to mock

* fix: tests
pull/146/head
Erick 4 years ago committed by GitHub
parent 4e0f0ffe29
commit c65dfb6001
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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"

@ -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(

@ -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<GameBloc, GameState> {
class BonusWord extends Component
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
/// {@macro bonus_word}
BonusWord({required Vector2 position}) : _position = position;
@ -29,6 +30,8 @@ class BonusWord extends Component with BlocComponent<GameBloc, GameState> {
@override
void onNewState(GameState state) {
if (state.bonusHistory.last == GameBonus.word) {
gameRef.audio.googleBonus();
final letters = children.whereType<BonusLetter>().toList();
for (var i = 0; i < letters.length; i++) {

@ -37,5 +37,7 @@ class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
_gameRef.read<GameBloc>().add(
Scored(points: scorePoints.points),
);
_gameRef.audio.score();
}
}

@ -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<void> onLoad() async {

@ -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<PinballGameView> {
void initState() {
super.initState();
final audio = context.read<PinballAudio>();
_game = widget._isDebugMode
? DebugPinballGame(theme: widget.theme, audio: audio)
: PinballGame(theme: widget.theme, audio: audio);
// TODO(erickzanardo): Revisit this when we start to have more assets
// 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<void> _fetchAssets() async {
final pinballAudio = context.read<PinballAudio>();
await Future.wait([
_game.preLoadAssets(),
pinballAudio.load(),
]);
}
@override

@ -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');
}

@ -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,
);
});
}

@ -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,
);
});
}

@ -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,
);
});
}

@ -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

@ -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

@ -0,0 +1,4 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml
analyzer:
exclude:
- lib/**/*.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;
}

@ -0,0 +1,3 @@
library pinball_audio;
export '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<AudioPool> Function(
String sound, {
bool? repeating,
int? maxPlayers,
int? minPlayers,
String? prefix,
});
/// Function that defines the contract for playing a single
/// audio
typedef PlaySingleAudio = Future<void> Function(String);
/// Function that defines the contract for configuring
/// an [AudioCache] instance
typedef ConfigureAudioCache = void Function(AudioCache);
/// {@template pinball_audio}
/// 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<void> 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';
}
}

@ -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/

@ -0,0 +1,34 @@
// ignore_for_file: one_member_abstracts
import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:mocktail/mocktail.dart';
abstract class _CreateAudioPoolStub {
Future<AudioPool> onCall(
String sound, {
bool? repeating,
int? maxPlayers,
int? minPlayers,
String? prefix,
});
}
class CreateAudioPoolStub extends Mock implements _CreateAudioPoolStub {}
abstract class _ConfigureAudioCacheStub {
void onCall(AudioCache cache);
}
class ConfigureAudioCacheStub extends Mock implements _ConfigureAudioCacheStub {
}
abstract class _PlaySingleAudioStub {
Future<void> onCall(String url);
}
class PlaySingleAudioStub extends Mock implements _PlaySingleAudioStub {}
class MockAudioPool extends Mock implements AudioPool {}
class MockAudioCache extends Mock implements AudioCache {}

@ -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);
});
});
});
}

@ -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');
}

@ -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"

@ -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:

@ -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);
});

@ -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<PinballGame, GameBloc>(
// TODO(alestiago): Use TestGame once BonusLetter has controller.
gameBuilder: PinballGameTest.create,
blocBuilder: () => gameBloc,
repositories: () => [
RepositoryProvider<PinballAudio>.value(value: pinballAudio),
],
);
setUp(() {
@ -209,6 +230,9 @@ void main() {
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
pinballAudio = MockPinballAudio();
when(pinballAudio.googleBonus).thenAnswer((_) {});
});
flameBlocTester.testGameWidget(

@ -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<GameBloc>(game.read).thenReturn(bloc);
when(game.read<GameBloc>).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<GameBloc>).thenReturn(bloc);
when(() => game.audio).thenReturn(audio);
BallScorePointsCallback(game).begin(
ball,
fakeScorePoints,
FakeContact(),
);
verify(audio.score).called(1);
},
);
});
});
}

@ -7,13 +7,19 @@ class FlameBlocTester<T extends FlameGame, B extends Bloc<dynamic, dynamic>>
FlameBlocTester({
required GameCreateFunction<T> gameBuilder,
required B Function() blocBuilder,
List<RepositoryProvider> 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,
),
),
);
},

@ -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(),
);
}

@ -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 {}

@ -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<void> 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,
),
),
),
),
);
);
});
}
}

Loading…
Cancel
Save