fix: merge conflicts with plunger

pull/145/head
RuiAlonso 4 years ago
commit 659a872147

@ -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:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/landing/landing.dart'; import 'package:pinball/landing/landing.dart';
import 'package:pinball_audio/pinball_audio.dart';
class App extends StatelessWidget { class App extends StatelessWidget {
const App({Key? key, required LeaderboardRepository leaderboardRepository}) const App({
: _leaderboardRepository = leaderboardRepository, Key? key,
required LeaderboardRepository leaderboardRepository,
required PinballAudio pinballAudio,
}) : _leaderboardRepository = leaderboardRepository,
_pinballAudio = pinballAudio,
super(key: key); super(key: key);
final LeaderboardRepository _leaderboardRepository; final LeaderboardRepository _leaderboardRepository;
final PinballAudio _pinballAudio;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RepositoryProvider.value( return MultiRepositoryProvider(
value: _leaderboardRepository, providers: [
RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _pinballAudio),
],
child: MaterialApp( child: MaterialApp(
title: 'I/O Pinball', title: 'I/O Pinball',
theme: ThemeData( theme: ThemeData(

@ -8,7 +8,7 @@ import 'package:pinball_components/pinball_components.dart';
class Board extends Component { class Board extends Component {
/// {@macro board} /// {@macro board}
// TODO(alestiago): Make Board a Blueprint and sort out priorities. // TODO(alestiago): Make Board a Blueprint and sort out priorities.
Board() : super(priority: 5); Board() : super(priority: 1);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -83,8 +83,8 @@ class _BottomGroupSide extends Component {
final kicker = Kicker( final kicker = Kicker(
side: _side, side: _side,
)..initialPosition = Vector2( )..initialPosition = Vector2(
(22.0 * direction) + centerXAdjustment, (22.4 * direction) + centerXAdjustment,
-26, -25,
); );
await addAll([flipper, baseboard, kicker]); await addAll([flipper, baseboard, kicker]);

@ -13,7 +13,8 @@ import 'package:pinball_components/pinball_components.dart';
/// {@template bonus_word} /// {@template bonus_word}
/// Loads all [BonusLetter]s to compose a [BonusWord]. /// Loads all [BonusLetter]s to compose a [BonusWord].
/// {@endtemplate} /// {@endtemplate}
class BonusWord extends Component with BlocComponent<GameBloc, GameState> { class BonusWord extends Component
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
/// {@macro bonus_word} /// {@macro bonus_word}
BonusWord({required Vector2 position}) : _position = position; BonusWord({required Vector2 position}) : _position = position;
@ -29,6 +30,8 @@ class BonusWord extends Component with BlocComponent<GameBloc, GameState> {
@override @override
void onNewState(GameState state) { void onNewState(GameState state) {
if (state.bonusHistory.last == GameBonus.word) { if (state.bonusHistory.last == GameBonus.word) {
gameRef.audio.googleBonus();
final letters = children.whereType<BonusLetter>().toList(); final letters = children.whereType<BonusLetter>().toList();
for (var i = 0; i < letters.length; i++) { for (var i = 0; i < letters.length; i++) {

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

@ -13,6 +13,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.flipper.right.keyName), images.load(components.Assets.images.flipper.right.keyName),
images.load(components.Assets.images.baseboard.left.keyName), images.load(components.Assets.images.baseboard.left.keyName),
images.load(components.Assets.images.baseboard.right.keyName), images.load(components.Assets.images.baseboard.right.keyName),
images.load(components.Assets.images.kicker.left.keyName),
images.load(components.Assets.images.kicker.right.keyName),
images.load(components.Assets.images.launchRamp.ramp.keyName), images.load(components.Assets.images.launchRamp.ramp.keyName),
images.load( images.load(
components.Assets.images.launchRamp.foregroundRailing.keyName, components.Assets.images.launchRamp.foregroundRailing.keyName,

@ -7,18 +7,19 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class PinballGame extends Forge2DGame class PinballGame extends Forge2DGame
with FlameBloc, HasKeyboardHandlerComponents { with FlameBloc, HasKeyboardHandlerComponents {
PinballGame({required this.theme}) { PinballGame({required this.theme, required this.audio}) {
images.prefix = ''; images.prefix = '';
} }
final PinballTheme theme; final PinballTheme theme;
late final Plunger plunger; final PinballAudio audio;
@override @override
void onAttach() { void onAttach() {
@ -31,11 +32,13 @@ class PinballGame extends Forge2DGame
_addContactCallbacks(); _addContactCallbacks();
await _addGameBoundaries(); await _addGameBoundaries();
unawaited(add(Board()));
unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp()));
unawaited(_addPlunger()); unawaited(_addPlunger());
unawaited(add(Board()));
unawaited(addFromBlueprint(DinoWalls()));
unawaited(_addBonusWord()); unawaited(_addBonusWord());
unawaited(_addRamps()); unawaited(addFromBlueprint(SpaceshipRamp()));
unawaited( unawaited(
addFromBlueprint( addFromBlueprint(
Spaceship( Spaceship(
@ -64,17 +67,10 @@ class PinballGame extends Forge2DGame
Future<void> _addGameBoundaries() async { Future<void> _addGameBoundaries() async {
await add(BottomWall()); await add(BottomWall());
createBoundaries(this).forEach(add); createBoundaries(this).forEach(add);
unawaited(
addFromBlueprint(
DinoWalls(
position: Vector2(-2.4, 0),
),
),
);
} }
Future<void> _addPlunger() async { Future<void> _addPlunger() async {
plunger = Plunger(compressionDistance: 29) final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19); ..initialPosition = Vector2(38, -19);
await add(plunger); await add(plunger);
} }
@ -90,24 +86,31 @@ class PinballGame extends Forge2DGame
); );
} }
Future<void> _addRamps() async { Future<void> spawnBall() async {
unawaited(addFromBlueprint(SpaceshipRamp())); // TODO(alestiago): Remove once this logic is moved to controller.
unawaited(addFromBlueprint(LaunchRamp())); var plunger = firstChild<Plunger>();
} if (plunger == null) {
await add(plunger = Plunger(compressionDistance: 1));
}
void spawnBall() {
final ball = ControlledBall.launch( final ball = ControlledBall.launch(
theme: theme, theme: theme,
)..initialPosition = Vector2( )..initialPosition = Vector2(
plunger.body.position.x, plunger.body.position.x,
plunger.body.position.y + Ball.size.y, plunger.body.position.y + Ball.size.y,
); );
add(ball); await add(ball);
} }
} }
class DebugPinballGame extends PinballGame with TapDetector { class DebugPinballGame extends PinballGame with TapDetector {
DebugPinballGame({required PinballTheme theme}) : super(theme: theme); DebugPinballGame({
required PinballTheme theme,
required PinballAudio audio,
}) : super(
theme: theme,
audio: audio,
);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {

@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
class PinballGamePage extends StatelessWidget { class PinballGamePage extends StatelessWidget {
@ -51,13 +52,24 @@ class _PinballGameViewState extends State<PinballGameView> {
void initState() { void initState() {
super.initState(); super.initState();
final audio = context.read<PinballAudio>();
_game = widget._isDebugMode
? DebugPinballGame(theme: widget.theme, audio: audio)
: PinballGame(theme: widget.theme, audio: audio);
// TODO(erickzanardo): Revisit this when we start to have more assets // TODO(erickzanardo): Revisit this when we start to have more assets
// this could expose a Stream (maybe even a cubit?) so we could show the // this could expose a Stream (maybe even a cubit?) so we could show the
// the loading progress with some fancy widgets. // the loading progress with some fancy widgets.
_game = (widget._isDebugMode _fetchAssets();
? DebugPinballGame(theme: widget.theme) }
: PinballGame(theme: widget.theme))
..preLoadAssets(); Future<void> _fetchAssets() async {
final pinballAudio = context.read<PinballAudio>();
await Future.wait([
_game.preLoadAssets(),
pinballAudio.load(),
]);
} }
@override @override

@ -3,8 +3,6 @@
/// FlutterGen /// FlutterGen
/// ***************************************************** /// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
@ -17,7 +15,6 @@ class $AssetsImagesGen {
class $AssetsImagesComponentsGen { class $AssetsImagesComponentsGen {
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
/// File path: assets/images/components/background.png
AssetGenImage get background => AssetGenImage get background =>
const AssetGenImage('assets/images/components/background.png'); const AssetGenImage('assets/images/components/background.png');

@ -8,10 +8,15 @@
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart'; import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart'; import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart';
void main() { void main() {
bootstrap((firestore) async { bootstrap((firestore) async {
final leaderboardRepository = LeaderboardRepository(firestore); 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:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart'; import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart'; import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart';
void main() { void main() {
bootstrap((firestore) async { bootstrap((firestore) async {
final leaderboardRepository = LeaderboardRepository(firestore); 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:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart'; import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart'; import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart';
void main() { void main() {
bootstrap((firestore) async { bootstrap((firestore) async {
final leaderboardRepository = LeaderboardRepository(firestore); 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);
});
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@ -26,6 +26,7 @@ class $AssetsImagesGen {
AssetGenImage get flutterSignPost => AssetGenImage get flutterSignPost =>
const AssetGenImage('assets/images/flutter_sign_post.png'); const AssetGenImage('assets/images/flutter_sign_post.png');
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp => $AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
@ -100,6 +101,18 @@ class $AssetsImagesFlipperGen {
const AssetGenImage('assets/images/flipper/right.png'); const AssetGenImage('assets/images/flipper/right.png');
} }
class $AssetsImagesKickerGen {
const $AssetsImagesKickerGen();
/// File path: assets/images/kicker/left.png
AssetGenImage get left =>
const AssetGenImage('assets/images/kicker/left.png');
/// File path: assets/images/kicker/right.png
AssetGenImage get right =>
const AssetGenImage('assets/images/kicker/right.png');
}
class $AssetsImagesLaunchRampGen { class $AssetsImagesLaunchRampGen {
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();

@ -12,16 +12,13 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@endtemplate} /// {@endtemplate}
class DinoWalls extends Forge2DBlueprint { class DinoWalls extends Forge2DBlueprint {
/// {@macro dinowalls} /// {@macro dinowalls}
DinoWalls({required this.position}); DinoWalls();
/// The [position] where the elements will be created
final Vector2 position;
@override @override
void build(_) { void build(_) {
addAll([ addAll([
_DinoTopWall()..initialPosition = position, _DinoTopWall(),
_DinoBottomWall()..initialPosition = position, _DinoBottomWall(),
]); ]);
} }
} }
@ -31,7 +28,7 @@ class DinoWalls extends Forge2DBlueprint {
/// {@endtemplate} /// {@endtemplate}
class _DinoTopWall extends BodyComponent with InitialPosition { class _DinoTopWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall} ///{@macro dino_top_wall}
_DinoTopWall() : super(priority: 2); _DinoTopWall() : super(priority: 1);
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
@ -129,7 +126,7 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
/// {@endtemplate} /// {@endtemplate}
class _DinoBottomWall extends BodyComponent with InitialPosition { class _DinoBottomWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall} ///{@macro dino_top_wall}
_DinoBottomWall() : super(priority: 2); _DinoBottomWall() : super(priority: 1);
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];

@ -1,10 +1,10 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame/extensions.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:geometry/geometry.dart' as geometry show centroid; import 'package:geometry/geometry.dart' as geometry show centroid;
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
/// {@template kicker} /// {@template kicker}
/// Triangular [BodyType.static] body that propels the [Ball] towards the /// Triangular [BodyType.static] body that propels the [Ball] towards the
@ -16,12 +16,7 @@ class Kicker extends BodyComponent with InitialPosition {
/// {@macro kicker} /// {@macro kicker}
Kicker({ Kicker({
required BoardSide side, required BoardSide side,
}) : _side = side { }) : _side = side;
// TODO(alestiago): Use sprite instead of color when provided.
paint = Paint()
..color = const Color(0xFF00FF00)
..style = PaintingStyle.fill;
}
/// Whether the [Kicker] is on the left or right side of the board. /// Whether the [Kicker] is on the left or right side of the board.
/// ///
@ -31,24 +26,22 @@ class Kicker extends BodyComponent with InitialPosition {
final BoardSide _side; final BoardSide _side;
/// The size of the [Kicker] body. /// The size of the [Kicker] body.
// TODO(alestiago): Use size from PositionedBodyComponent instead, static final Vector2 size = Vector2(4.4, 15);
// once a sprite is given.
static final Vector2 size = Vector2(4, 10);
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[]; final fixturesDefs = <FixtureDef>[];
final direction = _side.direction; final direction = _side.direction;
const quarterPi = math.pi / 4; const quarterPi = math.pi / 4;
final upperCircle = CircleShape()..radius = 1.45; final upperCircle = CircleShape()..radius = 1.6;
upperCircle.position.setValues(0, -upperCircle.radius / 2); upperCircle.position.setValues(0, -upperCircle.radius / 2);
final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0; final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0;
fixturesDefs.add(upperCircleFixtureDef); fixturesDefs.add(upperCircleFixtureDef);
final lowerCircle = CircleShape()..radius = 1.45; final lowerCircle = CircleShape()..radius = 1.6;
lowerCircle.position.setValues( lowerCircle.position.setValues(
size.x * -direction, size.x * -direction,
-size.y, -size.y - 0.8,
); );
final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0; final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0;
fixturesDefs.add(lowerCircleFixtureDef); fixturesDefs.add(lowerCircleFixtureDef);
@ -60,8 +53,7 @@ class Kicker extends BodyComponent with InitialPosition {
upperCircle.radius * direction, upperCircle.radius * direction,
0, 0,
), ),
// TODO(alestiago): Use values from design. Vector2(2.5 * direction, -size.y + 2),
Vector2(2.0 * direction, -size.y + 2),
); );
final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0; final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0;
fixturesDefs.add(wallFacingLineFixtureDef); fixturesDefs.add(wallFacingLineFixtureDef);
@ -125,6 +117,27 @@ class Kicker extends BodyComponent with InitialPosition {
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
final sprite = await gameRef.loadSprite(
(_side.isLeft)
? Assets.images.kicker.left.keyName
: Assets.images.kicker.right.keyName,
);
await add(
SpriteComponent(
sprite: sprite,
size: Vector2(8.7, 19),
anchor: Anchor.center,
position: Vector2(0.7 * -_side.direction, -2.2),
),
);
}
} }
// TODO(alestiago): Evaluate if there's value on generalising this to // TODO(alestiago): Evaluate if there's value on generalising this to

@ -142,7 +142,7 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
class _LaunchRampForegroundRailing extends BodyComponent class _LaunchRampForegroundRailing extends BodyComponent
with InitialPosition, Layered { with InitialPosition, Layered {
/// {@macro launch_ramp_foreground_railing} /// {@macro launch_ramp_foreground_railing}
_LaunchRampForegroundRailing() : super(priority: 4) { _LaunchRampForegroundRailing() : super(priority: 1) {
layer = Layer.launcher; layer = Layer.launcher;
} }
@ -207,7 +207,6 @@ class _LaunchRampForegroundRailing extends BodyComponent
size: sprite.originalSize / 10, size: sprite.originalSize / 10,
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(22.8, 0), position: Vector2(22.8, 0),
priority: 4,
), ),
); );
} }

@ -38,6 +38,7 @@ flutter:
- assets/images/spaceship/rail/ - assets/images/spaceship/rail/
- assets/images/spaceship/ramp/ - assets/images/spaceship/ramp/
- assets/images/chrome_dino/ - assets/images/chrome_dino/
- assets/images/kicker/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -6,6 +6,7 @@
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
import 'package:dashbook/dashbook.dart'; import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sandbox/stories/kicker/stories.dart';
import 'package:sandbox/stories/stories.dart'; import 'package:sandbox/stories/stories.dart';
void main() { void main() {
@ -19,5 +20,6 @@ void main() {
addBaseboardStories(dashbook); addBaseboardStories(dashbook);
addChromeDinoStories(dashbook); addChromeDinoStories(dashbook);
addDashNestBumperStories(dashbook); addDashNestBumperStories(dashbook);
addKickerStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -0,0 +1,39 @@
import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class KickerGame extends BasicBallGame {
KickerGame({
required this.trace,
}) : super(color: const Color(0xFFFF0000));
static const info = '''
Shows how Kickers are rendered.
- Activate the "trace" parameter to overlay the body.
- Tap anywhere on the screen to spawn a ball into the game.
''';
final bool trace;
@override
Future<void> onLoad() async {
await super.onLoad();
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final leftKicker = Kicker(side: BoardSide.left)
..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y);
await add(leftKicker);
final rightKicker = Kicker(side: BoardSide.right)
..initialPosition = Vector2(center.x + (Kicker.size.x * 2), center.y);
await add(rightKicker);
if (trace) {
leftKicker.trace();
rightKicker.trace();
}
}
}

@ -0,0 +1,17 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/kicker/kicker_game.dart';
void addKickerStories(Dashbook dashbook) {
dashbook.storiesOf('Kickers').add(
'Basic',
(context) => GameWidget(
game: KickerGame(
trace: context.boolProperty('Trace', true),
),
),
codeLink: buildSourceLink('kicker_game/basic.dart'),
info: KickerGame.info,
);
}

@ -1,6 +1,5 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -15,7 +14,7 @@ void main() {
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final dinoWalls = DinoWalls(position: Vector2.zero()); final dinoWalls = DinoWalls();
await game.addFromBlueprint(dinoWalls); await game.addFromBlueprint(dinoWalls);
await game.ready(); await game.ready();

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

@ -5,10 +5,34 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() { void main() {
group('Kicker', () { group('Kicker', () {
// TODO(alestiago): Include golden tests for left and right. final flameTester = FlameTester(TestGame.new);
final flameTester = FlameTester(Forge2DGame.new);
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
final leftKicker = Kicker(
side: BoardSide.left,
)..initialPosition = Vector2(-20, 0);
final rightKicker = Kicker(
side: BoardSide.right,
)..initialPosition = Vector2(20, 0);
await game.addAll([leftKicker, rightKicker]);
await game.ready();
game.camera.followVector2(Vector2.zero());
},
// TODO(ruimiguel): enable test when workflows are fixed.
//verify: (game, tester) async {
// await expectLater(
// find.byGame<Forge2DGame>(),
// matchesGoldenFile('golden/kickers.png'),
// );
//},
);
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',

@ -29,6 +29,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.2" version: "2.8.2"
audioplayers:
dependency: transitive
description:
name: audioplayers
url: "https://pub.dartlang.org"
source: hosted
version: "0.20.1"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -148,6 +155,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -183,6 +197,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" 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: flame_bloc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -259,6 +280,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.4"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -392,6 +420,62 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" 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: pinball_components:
dependency: "direct main" dependency: "direct main"
description: description:
@ -406,6 +490,13 @@ packages:
relative: true relative: true
source: path source: path
version: "1.0.0+1" 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: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -420,6 +511,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.4"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -509,6 +607,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
synchronized:
dependency: transitive
description:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0+2"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -544,6 +649,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -586,6 +698,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" 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: yaml:
dependency: transitive dependency: transitive
description: description:
@ -595,4 +721,4 @@ packages:
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.16.0 <3.0.0" dart: ">=2.16.0 <3.0.0"
flutter: ">=2.5.0" flutter: ">=2.8.0"

@ -23,6 +23,8 @@ dependencies:
intl: ^0.17.0 intl: ^0.17.0
leaderboard_repository: leaderboard_repository:
path: packages/leaderboard_repository path: packages/leaderboard_repository
pinball_audio:
path: packages/pinball_audio
pinball_components: pinball_components:
path: packages/pinball_components path: packages/pinball_components
pinball_theme: pinball_theme:

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

@ -9,7 +9,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(EmptyPinballGameTest.new);
group('Board', () { group('Board', () {
flameTester.test( flameTester.test(
@ -78,7 +78,6 @@ void main() {
flameTester.test( flameTester.test(
'one FlutterForest', 'one FlutterForest',
(game) async { (game) async {
// TODO(alestiago): change to [NestBumpers] once provided.
final board = Board(); final board = Board();
await game.ready(); await game.ready();
await game.ensureAdd(board); await game.ensureAdd(board);

@ -4,24 +4,28 @@ import 'package:bloc_test/bloc_test.dart';
import 'package:flame/effects.dart'; import 'package:flame/effects.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(EmptyPinballGameTest.new);
group('BonusWord', () { group('BonusWord', () {
flameTester.test( flameTester.test(
'loads the letters correctly', 'loads the letters correctly',
(game) async { (game) async {
await game.ready(); final bonusWord = BonusWord(
position: Vector2.zero(),
);
await game.ensureAdd(bonusWord);
final bonusWord = game.children.whereType<BonusWord>().first; final letters = bonusWord.descendants().whereType<BonusLetter>();
final letters = bonusWord.children.whereType<BonusLetter>();
expect(letters.length, equals(GameBloc.bonusWord.length)); expect(letters.length, equals(GameBloc.bonusWord.length));
}, },
); );
@ -89,6 +93,21 @@ void main() {
}, },
); );
flameTester.test(
'plays the google bonus sound',
(game) async {
when(() => state.bonusHistory).thenReturn([GameBonus.word]);
final bonusWord = BonusWord(position: Vector2.zero());
await game.ensureAdd(bonusWord);
await game.ready();
bonusWord.onNewState(state);
verify(bonusWord.gameRef.audio.googleBonus).called(1);
},
);
flameTester.test( flameTester.test(
'adds a color effect to reset the color when the sequence is finished', 'adds a color effect to reset the color when the sequence is finished',
(game) async { (game) async {
@ -118,7 +137,7 @@ void main() {
}); });
group('BonusLetter', () { group('BonusLetter', () {
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(EmptyPinballGameTest.new);
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
@ -195,11 +214,14 @@ void main() {
group('bonus letter activation', () { group('bonus letter activation', () {
late GameBloc gameBloc; late GameBloc gameBloc;
late PinballAudio pinballAudio;
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
// TODO(alestiago): Use TestGame once BonusLetter has controller. gameBuilder: EmptyPinballGameTest.new,
gameBuilder: PinballGameTest.create,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
repositories: () => [
RepositoryProvider<PinballAudio>.value(value: pinballAudio),
],
); );
setUp(() { setUp(() {
@ -209,19 +231,28 @@ void main() {
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
initialState: const GameState.initial(), initialState: const GameState.initial(),
); );
pinballAudio = MockPinballAudio();
when(pinballAudio.googleBonus).thenAnswer((_) {});
}); });
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'adds BonusLetterActivated to GameBloc when not activated', 'adds BonusLetterActivated to GameBloc when not activated',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ready(); final bonusWord = BonusWord(
final bonusLetter = game.descendants().whereType<BonusLetter>().first; position: Vector2.zero(),
);
await game.ensureAdd(bonusWord);
bonusLetter.activate(); final bonusLetters =
await game.ready(); game.descendants().whereType<BonusLetter>().toList();
}, for (var index = 0; index < bonusLetters.length; index++) {
verify: (game, tester) async { final bonusLetter = bonusLetters[index];
verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1); bonusLetter.activate();
await game.ready();
verify(() => gameBloc.add(BonusLetterActivated(index))).called(1);
}
}, },
); );
@ -285,25 +316,33 @@ void main() {
); );
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'only listens when there is a change on the letter status', 'listens when there is a change on the letter status',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ready(); final bonusWord = BonusWord(
final bonusLetter = game.descendants().whereType<BonusLetter>().first; position: Vector2.zero(),
bonusLetter.activate();
},
verify: (game, tester) async {
const state = GameState(
score: 0,
balls: 2,
activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [],
);
final bonusLetter = game.descendants().whereType<BonusLetter>().first;
expect(
bonusLetter.listenWhen(const GameState.initial(), state),
isTrue,
); );
await game.ensureAdd(bonusWord);
final bonusLetters =
game.descendants().whereType<BonusLetter>().toList();
for (var index = 0; index < bonusLetters.length; index++) {
final bonusLetter = bonusLetters[index];
bonusLetter.activate();
await game.ready();
final state = GameState(
score: 0,
balls: 2,
activatedBonusLetters: [index],
activatedDashNests: const {},
bonusHistory: const [],
);
expect(
bonusLetter.listenWhen(const GameState.initial(), state),
isTrue,
);
}
}, },
); );
}); });

@ -13,7 +13,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(EmptyPinballGameTest.new);
group('BonusBallController', () { group('BonusBallController', () {
late Ball ball; late Ball ball;
@ -67,7 +67,7 @@ void main() {
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: PinballGameTest.create, gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
); );
@ -155,13 +155,13 @@ void main() {
await game.ensureAdd(ball); await game.ensureAdd(ball);
final state = MockGameState(); final state = MockGameState();
when(() => state.balls).thenReturn(2); when(() => state.balls).thenReturn(1);
final previousBalls = game.descendants().whereType<Ball>().toList(); final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state); controller.onNewState(state);
await game.ready(); await game.ready();
final currentBalls = game.descendants().whereType<Ball>(); final currentBalls = game.descendants().whereType<Ball>().toList();
expect(currentBalls.contains(ball), isFalse); expect(currentBalls.contains(ball), isFalse);
expect(currentBalls.length, equals(previousBalls.length)); expect(currentBalls.length, equals(previousBalls.length));

@ -10,7 +10,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(EmptyPinballGameTest.new);
group('FlipperController', () { group('FlipperController', () {
group('onKeyEvent', () { group('onKeyEvent', () {

@ -25,7 +25,7 @@ void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) {
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(EmptyPinballGameTest.new);
group('FlutterForest', () { group('FlutterForest', () {
flameTester.test( flameTester.test(
@ -146,16 +146,15 @@ void main() {
}); });
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>( final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: PinballGameTest.create, gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc, blocBuilder: () => gameBloc,
); );
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'add DashNestActivated event', 'add DashNestActivated event',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.ready(); final flutterForest = FlutterForest();
final flutterForest = await game.ensureAdd(flutterForest);
game.descendants().whereType<FlutterForest>().first;
await game.ensureAdd(ball); await game.ensureAdd(ball);
final bumpers = final bumpers =
@ -177,15 +176,16 @@ void main() {
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
await game.ensureAdd(ball); await game.ensureAdd(ball);
game.addContactCallback(BallScorePointsCallback(game));
final bumpers = final bumpers = flutterForest.descendants().whereType<ScorePoints>();
flutterForest.descendants().whereType<DashNestBumper>();
for (final bumper in bumpers) { for (final bumper in bumpers) {
beginContact(game, bumper, ball); beginContact(game, bumper, ball);
final points = (bumper as ScorePoints).points;
verify( verify(
() => gameBloc.add(Scored(points: points)), () => gameBloc.add(
Scored(points: bumper.points),
),
).called(1); ).called(1);
} }
}, },

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

@ -12,8 +12,8 @@ import '../helpers/helpers.dart';
void main() { void main() {
group('PinballGame', () { group('PinballGame', () {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(PinballGameTest.new);
final debugModeFlameTester = FlameTester(DebugPinballGameTest.create); final debugModeFlameTester = FlameTester(DebugPinballGameTest.new);
// TODO(alestiago): test if [PinballGame] registers // TODO(alestiago): test if [PinballGame] registers
// [BallScorePointsCallback] once the following issue is resolved: // [BallScorePointsCallback] once the following issue is resolved:

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

@ -1,22 +1,29 @@
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
/// [PinballGame] extension to reduce boilerplate in tests. import 'helpers.dart';
extension PinballGameTest on PinballGame {
/// Create [PinballGame] with default [PinballTheme]. class PinballGameTest extends PinballGame {
static PinballGame create() => PinballGame( PinballGameTest()
theme: const PinballTheme( : super(
characterTheme: DashTheme(), audio: MockPinballAudio(),
), theme: const PinballTheme(
)..images.prefix = ''; characterTheme: DashTheme(),
),
);
}
class DebugPinballGameTest extends DebugPinballGame {
DebugPinballGameTest()
: super(
audio: MockPinballAudio(),
theme: const PinballTheme(
characterTheme: DashTheme(),
),
);
} }
/// [DebugPinballGame] extension to reduce boilerplate in tests. class EmptyPinballGameTest extends PinballGameTest {
extension DebugPinballGameTest on DebugPinballGame { @override
/// Create [PinballGame] with default [PinballTheme]. Future<void> onLoad() async {}
static DebugPinballGame create() => DebugPinballGame(
theme: const PinballTheme(
characterTheme: DashTheme(),
),
);
} }

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

@ -14,9 +14,18 @@ import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart'; import 'package:pinball/theme/theme.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'helpers.dart'; import 'helpers.dart';
PinballAudio _buildDefaultPinballAudio() {
final audio = MockPinballAudio();
when(audio.load).thenAnswer((_) => Future.value());
return audio;
}
extension PumpApp on WidgetTester { extension PumpApp on WidgetTester {
Future<void> pumpApp( Future<void> pumpApp(
Widget widget, { Widget widget, {
@ -24,31 +33,41 @@ extension PumpApp on WidgetTester {
GameBloc? gameBloc, GameBloc? gameBloc,
ThemeCubit? themeCubit, ThemeCubit? themeCubit,
LeaderboardRepository? leaderboardRepository, LeaderboardRepository? leaderboardRepository,
PinballAudio? pinballAudio,
}) { }) {
return pumpWidget( return runAsync(() {
RepositoryProvider.value( return pumpWidget(
value: leaderboardRepository ?? MockLeaderboardRepository(), MultiRepositoryProvider(
child: MultiBlocProvider(
providers: [ providers: [
BlocProvider.value( RepositoryProvider.value(
value: themeCubit ?? MockThemeCubit(), value: leaderboardRepository ?? MockLeaderboardRepository(),
), ),
BlocProvider.value( RepositoryProvider.value(
value: gameBloc ?? MockGameBloc(), value: pinballAudio ?? _buildDefaultPinballAudio(),
), ),
], ],
child: MaterialApp( child: MultiBlocProvider(
localizationsDelegates: const [ providers: [
AppLocalizations.delegate, BlocProvider.value(
GlobalMaterialLocalizations.delegate, value: themeCubit ?? MockThemeCubit(),
),
BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
),
], ],
supportedLocales: AppLocalizations.supportedLocales, child: MaterialApp(
home: navigator != null localizationsDelegates: const [
? MockNavigatorProvider(navigator: navigator, child: widget) AppLocalizations.delegate,
: widget, GlobalMaterialLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: navigator != null
? MockNavigatorProvider(navigator: navigator, child: widget)
: widget,
),
), ),
), ),
), );
); });
} }
} }

Loading…
Cancel
Save