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

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

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

@ -13,6 +13,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.flipper.right.keyName),
images.load(components.Assets.images.baseboard.left.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.foregroundRailing.keyName,

@ -7,18 +7,19 @@ 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;
late final Plunger plunger;
final PinballAudio audio;
@override
void onAttach() {
@ -31,11 +32,13 @@ class PinballGame extends Forge2DGame
_addContactCallbacks();
await _addGameBoundaries();
unawaited(add(Board()));
unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp()));
unawaited(_addPlunger());
unawaited(add(Board()));
unawaited(addFromBlueprint(DinoWalls()));
unawaited(_addBonusWord());
unawaited(_addRamps());
unawaited(addFromBlueprint(SpaceshipRamp()));
unawaited(
addFromBlueprint(
Spaceship(
@ -64,17 +67,10 @@ class PinballGame extends Forge2DGame
Future<void> _addGameBoundaries() async {
await add(BottomWall());
createBoundaries(this).forEach(add);
unawaited(
addFromBlueprint(
DinoWalls(
position: Vector2(-2.4, 0),
),
),
);
}
Future<void> _addPlunger() async {
plunger = Plunger(compressionDistance: 29)
final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19);
await add(plunger);
}
@ -90,24 +86,31 @@ class PinballGame extends Forge2DGame
);
}
Future<void> _addRamps() async {
unawaited(addFromBlueprint(SpaceshipRamp()));
unawaited(addFromBlueprint(LaunchRamp()));
Future<void> spawnBall() async {
// TODO(alestiago): Remove once this logic is moved to controller.
var plunger = firstChild<Plunger>();
if (plunger == null) {
await add(plunger = Plunger(compressionDistance: 1));
}
void spawnBall() {
final ball = ControlledBall.launch(
theme: theme,
)..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y + Ball.size.y,
);
add(ball);
await add(ball);
}
}
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);
});
});
});
}

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 =>
const AssetGenImage('assets/images/flutter_sign_post.png');
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
@ -100,6 +101,18 @@ class $AssetsImagesFlipperGen {
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 {
const $AssetsImagesLaunchRampGen();

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

@ -1,10 +1,10 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
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}
/// Triangular [BodyType.static] body that propels the [Ball] towards the
@ -16,12 +16,7 @@ class Kicker extends BodyComponent with InitialPosition {
/// {@macro kicker}
Kicker({
required BoardSide side,
}) : _side = side {
// TODO(alestiago): Use sprite instead of color when provided.
paint = Paint()
..color = const Color(0xFF00FF00)
..style = PaintingStyle.fill;
}
}) : _side = side;
/// 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;
/// The size of the [Kicker] body.
// TODO(alestiago): Use size from PositionedBodyComponent instead,
// once a sprite is given.
static final Vector2 size = Vector2(4, 10);
static final Vector2 size = Vector2(4.4, 15);
List<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[];
final direction = _side.direction;
const quarterPi = math.pi / 4;
final upperCircle = CircleShape()..radius = 1.45;
final upperCircle = CircleShape()..radius = 1.6;
upperCircle.position.setValues(0, -upperCircle.radius / 2);
final upperCircleFixtureDef = FixtureDef(upperCircle)..friction = 0;
fixturesDefs.add(upperCircleFixtureDef);
final lowerCircle = CircleShape()..radius = 1.45;
final lowerCircle = CircleShape()..radius = 1.6;
lowerCircle.position.setValues(
size.x * -direction,
-size.y,
-size.y - 0.8,
);
final lowerCircleFixtureDef = FixtureDef(lowerCircle)..friction = 0;
fixturesDefs.add(lowerCircleFixtureDef);
@ -60,8 +53,7 @@ class Kicker extends BodyComponent with InitialPosition {
upperCircle.radius * direction,
0,
),
// TODO(alestiago): Use values from design.
Vector2(2.0 * direction, -size.y + 2),
Vector2(2.5 * direction, -size.y + 2),
);
final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge)..friction = 0;
fixturesDefs.add(wallFacingLineFixtureDef);
@ -125,6 +117,27 @@ class Kicker extends BodyComponent with InitialPosition {
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

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

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

@ -6,6 +6,7 @@
// https://opensource.org/licenses/MIT.
import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart';
import 'package:sandbox/stories/kicker/stories.dart';
import 'package:sandbox/stories/stories.dart';
void main() {
@ -19,5 +20,6 @@ void main() {
addBaseboardStories(dashbook);
addChromeDinoStories(dashbook);
addDashNestBumperStories(dashbook);
addKickerStories(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
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
@ -15,7 +14,7 @@ void main() {
flameTester.test(
'loads correctly',
(game) async {
final dinoWalls = DinoWalls(position: Vector2.zero());
final dinoWalls = DinoWalls();
await game.addFromBlueprint(dinoWalls);
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:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('Kicker', () {
// TODO(alestiago): Include golden tests for left and right.
final flameTester = FlameTester(Forge2DGame.new);
final flameTester = FlameTester(TestGame.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(
'loads correctly',

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

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

@ -4,24 +4,28 @@ 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';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create);
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('BonusWord', () {
flameTester.test(
'loads the letters correctly',
(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.children.whereType<BonusLetter>();
final letters = bonusWord.descendants().whereType<BonusLetter>();
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(
'adds a color effect to reset the color when the sequence is finished',
(game) async {
@ -118,7 +137,7 @@ void main() {
});
group('BonusLetter', () {
final flameTester = FlameTester(PinballGameTest.create);
final flameTester = FlameTester(EmptyPinballGameTest.new);
flameTester.test(
'loads correctly',
@ -195,11 +214,14 @@ 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,
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
repositories: () => [
RepositoryProvider<PinballAudio>.value(value: pinballAudio),
],
);
setUp(() {
@ -209,19 +231,28 @@ void main() {
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
pinballAudio = MockPinballAudio();
when(pinballAudio.googleBonus).thenAnswer((_) {});
});
flameBlocTester.testGameWidget(
'adds BonusLetterActivated to GameBloc when not activated',
setUp: (game, tester) async {
await game.ready();
final bonusLetter = game.descendants().whereType<BonusLetter>().first;
final bonusWord = BonusWord(
position: Vector2.zero(),
);
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();
},
verify: (game, tester) async {
verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1);
verify(() => gameBloc.add(BonusLetterActivated(index))).called(1);
}
},
);
@ -285,25 +316,33 @@ void main() {
);
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 {
await game.ready();
final bonusLetter = game.descendants().whereType<BonusLetter>().first;
final bonusWord = BonusWord(
position: Vector2.zero(),
);
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();
},
verify: (game, tester) async {
const state = GameState(
await game.ready();
final state = GameState(
score: 0,
balls: 2,
activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [],
activatedBonusLetters: [index],
activatedDashNests: const {},
bonusHistory: const [],
);
final bonusLetter = game.descendants().whereType<BonusLetter>().first;
expect(
bonusLetter.listenWhen(const GameState.initial(), state),
isTrue,
);
}
},
);
});

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

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

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

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

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

@ -7,14 +7,20 @@ 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: repositories == null
? gameWidget
: MultiRepositoryProvider(
providers: repositories.call(),
child: gameWidget,
),
),
);
},
);

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

@ -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,10 +33,19 @@ extension PumpApp on WidgetTester {
GameBloc? gameBloc,
ThemeCubit? themeCubit,
LeaderboardRepository? leaderboardRepository,
PinballAudio? pinballAudio,
}) {
return runAsync(() {
return pumpWidget(
MultiRepositoryProvider(
providers: [
RepositoryProvider.value(
value: leaderboardRepository ?? MockLeaderboardRepository(),
),
RepositoryProvider.value(
value: pinballAudio ?? _buildDefaultPinballAudio(),
),
],
child: MultiBlocProvider(
providers: [
BlocProvider.value(
@ -50,5 +68,6 @@ extension PumpApp on WidgetTester {
),
),
);
});
}
}

Loading…
Cancel
Save