Merge branch 'main' into chore/max-score

pull/408/head
Tom Arra 3 years ago committed by GitHub
commit 3167a98651
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

@ -9,20 +9,24 @@ import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
import 'package:share_repository/share_repository.dart';
class App extends StatelessWidget {
const App({
Key? key,
required AuthenticationRepository authenticationRepository,
required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository,
required PinballAudioPlayer pinballAudioPlayer,
}) : _authenticationRepository = authenticationRepository,
_leaderboardRepository = leaderboardRepository,
_shareRepository = shareRepository,
_pinballAudioPlayer = pinballAudioPlayer,
super(key: key);
final AuthenticationRepository _authenticationRepository;
final LeaderboardRepository _leaderboardRepository;
final ShareRepository _shareRepository;
final PinballAudioPlayer _pinballAudioPlayer;
@override
@ -31,6 +35,7 @@ class App extends StatelessWidget {
providers: [
RepositoryProvider.value(value: _authenticationRepository),
RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _shareRepository),
RepositoryProvider.value(value: _pinballAudioPlayer),
],
child: MultiBlocProvider(

@ -17,29 +17,32 @@ class AssetsLoadingPage extends StatelessWidget {
Widget build(BuildContext context) {
final l10n = context.l10n;
final headline1 = Theme.of(context).textTheme.headline1;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Assets.images.loadingGame.ioPinball.image(),
),
const SizedBox(height: 40),
AnimatedEllipsisText(
l10n.loading,
style: headline1,
),
const SizedBox(height: 40),
FractionallySizedBox(
widthFactor: 0.8,
child: BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
builder: (context, state) {
return PinballLoadingIndicator(value: state.progress);
},
return Container(
decoration: const CrtBackground(),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Assets.images.loadingGame.ioPinball.image(),
),
),
],
const SizedBox(height: 40),
AnimatedEllipsisText(
l10n.loading,
style: headline1,
),
const SizedBox(height: 40),
FractionallySizedBox(
widthFactor: 0.8,
child: BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
builder: (context, state) {
return PinballLoadingIndicator(value: state.progress);
},
),
),
],
),
),
);
}

@ -1,7 +1,8 @@
export 'ball_spawning_behavior.dart';
export 'ball_theming_behavior.dart';
export 'bonus_ball_spawning_behavior.dart';
export 'bonus_noise_behavior.dart';
export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart';
export 'character_selection_behavior.dart';
export 'cow_bumper_noise_behavior.dart';
export 'scoring_behavior.dart';

@ -3,18 +3,25 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart';
/// Updates the launch [Ball] to reflect character selections.
class BallThemingBehavior extends Component
/// Updates the [ArcadeBackground] and launch [Ball] to reflect character
/// selections.
class CharacterSelectionBehavior extends Component
with
FlameBlocListenable<CharacterThemeCubit, CharacterThemeState>,
HasGameRef {
@override
void onNewState(CharacterThemeState state) {
gameRef
.descendants()
.whereType<ArcadeBackground>()
.single
.bloc
.onCharacterSelected(state.characterTheme);
gameRef
.descendants()
.whereType<Ball>()
.single
.bloc
.onThemeChanged(state.characterTheme);
.onCharacterSelected(state.characterTheme);
}
}

@ -0,0 +1,13 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
class CowBumperNoiseBehavior extends ContactBehavior {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
readProvider<PinballAudioPlayer>().play(PinballAudio.cowMoo);
}
}

@ -48,6 +48,7 @@ class AndroidAcres extends Component {
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
CowBumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(),

@ -6,10 +6,13 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' hide Assets;
import 'package:pinball_ui/pinball_ui.dart';
import 'package:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart';
/// {@template backbox}
/// The [Backbox] of the pinball machine.
@ -18,21 +21,26 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef {
/// {@macro backbox}
Backbox({
required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository,
required List<LeaderboardEntryData>? entries,
}) : _bloc = BackboxBloc(
leaderboardRepository: leaderboardRepository,
initialEntries: entries,
),
_shareRepository = shareRepository,
_platformHelper = PlatformHelper();
/// {@macro backbox}
@visibleForTesting
Backbox.test({
required BackboxBloc bloc,
required ShareRepository shareRepository,
required PlatformHelper platformHelper,
}) : _bloc = bloc,
_shareRepository = shareRepository,
_platformHelper = platformHelper;
final ShareRepository _shareRepository;
late final Component _display;
final BackboxBloc _bloc;
final PlatformHelper _platformHelper;
@ -87,6 +95,8 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef {
),
);
} else if (state is InitialsSuccessState) {
gameRef.overlays.remove(PinballGame.mobileControlsOverlay);
_display.add(
GameOverInfoDisplay(
onShare: () {
@ -94,6 +104,20 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef {
},
),
);
} else if (state is ShareState) {
_display.add(
ShareDisplay(
onShare: (platform) {
final message = readProvider<AppLocalizations>()
.iGotScoreAtPinball(state.score);
final url = _shareRepository.shareText(
value: message,
platform: platform,
);
openLink(url);
},
),
);
} else if (state is InitialsFailureState) {
_display.add(
InitialsSubmissionFailureDisplay(

@ -75,9 +75,7 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
Emitter<BackboxState> emit,
) async {
emit(
ShareState(
score: event.score,
),
ShareState(score: event.score),
);
}

@ -5,3 +5,4 @@ export 'initials_submission_success_display.dart';
export 'leaderboard_display.dart';
export 'leaderboard_failure_display.dart';
export 'loading_display.dart';
export 'share_display.dart';

@ -0,0 +1,189 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
import 'package:share_repository/share_repository.dart';
/// Signature for the callback called when the user tries to share their score
/// on the [ShareDisplay].
typedef OnSocialShareTap = void Function(SharePlatform);
final _descriptionTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.6,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template share_display}
/// Display that allows users to share their score to social networks.
/// {@endtemplate}
class ShareDisplay extends Component with HasGameRef {
/// {@macro share_display}
ShareDisplay({
OnSocialShareTap? onShare,
}) : super(
children: [
_ShareInstructionsComponent(
onShare: onShare,
),
],
);
}
class _ShareInstructionsComponent extends PositionComponent with HasGameRef {
_ShareInstructionsComponent({
OnSocialShareTap? onShare,
}) : super(
anchor: Anchor.center,
position: Vector2(0, -25),
children: [
_DescriptionComponent(),
_SocialNetworksComponent(
onShare: onShare,
),
],
);
}
class _DescriptionComponent extends PositionComponent with HasGameRef {
_DescriptionComponent()
: super(
anchor: Anchor.center,
position: Vector2.zero(),
children: [
_LetEveryoneTextComponent(),
_SharingYourScoreTextComponent(),
_SocialMediaTextComponent(),
],
);
}
class _LetEveryoneTextComponent extends TextComponent with HasGameRef {
_LetEveryoneTextComponent()
: super(
anchor: Anchor.center,
position: Vector2.zero(),
textRenderer: _descriptionTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().letEveryone;
}
}
class _SharingYourScoreTextComponent extends TextComponent with HasGameRef {
_SharingYourScoreTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 2.5),
textRenderer: _descriptionTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().bySharingYourScore;
}
}
class _SocialMediaTextComponent extends TextComponent with HasGameRef {
_SocialMediaTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 5),
textRenderer: _descriptionTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().socialMediaAccount;
}
}
class _SocialNetworksComponent extends PositionComponent with HasGameRef {
_SocialNetworksComponent({
OnSocialShareTap? onShare,
}) : super(
anchor: Anchor.center,
position: Vector2(0, 12),
children: [
FacebookButtonComponent(onTap: onShare),
TwitterButtonComponent(onTap: onShare),
],
);
}
/// {@template facebook_button_component}
/// Button for sharing on Facebook.
/// {@endtemplate}
class FacebookButtonComponent extends SpriteComponent
with HasGameRef, Tappable {
/// {@macro facebook_button_component}
FacebookButtonComponent({
OnSocialShareTap? onTap,
}) : _onTap = onTap,
super(
anchor: Anchor.center,
position: Vector2(-5, 0),
);
final OnSocialShareTap? _onTap;
@override
bool onTapDown(TapDownInfo info) {
_onTap?.call(SharePlatform.facebook);
return true;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(Assets.images.backbox.button.facebook.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 25;
}
}
/// {@template twitter_button_component}
/// Button for sharing on Twitter.
/// {@endtemplate}
class TwitterButtonComponent extends SpriteComponent with HasGameRef, Tappable {
/// {@macro twitter_button_component}
TwitterButtonComponent({
OnSocialShareTap? onTap,
}) : _onTap = onTap,
super(
anchor: Anchor.center,
position: Vector2(5, 0),
);
final OnSocialShareTap? _onTap;
@override
bool onTapDown(TapDownInfo info) {
_onTap?.call(SharePlatform.twitter);
return true;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(Assets.images.backbox.button.twitter.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 25;
}
}

@ -101,6 +101,8 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName),
images.load(components.Assets.images.backbox.marquee.keyName),
images.load(components.Assets.images.backbox.displayDivider.keyName),
images.load(components.Assets.images.backbox.button.facebook.keyName),
images.load(components.Assets.images.backbox.button.twitter.keyName),
images.load(
components.Assets.images.backbox.displayTitleDecoration.keyName,
),
@ -139,13 +141,17 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.skillShot.pin.keyName),
images.load(components.Assets.images.skillShot.lit.keyName),
images.load(components.Assets.images.skillShot.dimmed.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName),
images.load(dinoTheme.leaderboardIcon.keyName),
images.load(androidTheme.background.keyName),
images.load(androidTheme.ball.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(dashTheme.background.keyName),
images.load(dashTheme.ball.keyName),
images.load(dinoTheme.leaderboardIcon.keyName),
images.load(dinoTheme.background.keyName),
images.load(dinoTheme.ball.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.background.keyName),
images.load(sparkyTheme.ball.keyName),
];
}

@ -14,12 +14,14 @@ import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:share_repository/share_repository.dart';
class PinballGame extends PinballForge2DGame
with HasKeyboardHandlerComponents, MultiTouchTapDetector, HasTappables {
PinballGame({
required CharacterThemeCubit characterThemeBloc,
required this.leaderboardRepository,
required this.shareRepository,
required GameBloc gameBloc,
required AppLocalizations l10n,
required PinballAudioPlayer audioPlayer,
@ -51,6 +53,8 @@ class PinballGame extends PinballForge2DGame
final LeaderboardRepository leaderboardRepository;
final ShareRepository shareRepository;
final AppLocalizations _l10n;
final GameBloc _gameBloc;
@ -84,13 +88,14 @@ class PinballGame extends PinballForge2DGame
providers: [
FlameProvider<PinballAudioPlayer>.value(_audioPlayer),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<ShareRepository>.value(shareRepository),
FlameProvider<AppLocalizations>.value(_l10n),
],
children: [
BonusNoiseBehavior(),
GameBlocStatusListener(),
BallSpawningBehavior(),
BallThemingBehavior(),
CharacterSelectionBehavior(),
CameraFocusingBehavior(),
CanvasComponent(
onSpritePainted: (paint) {
@ -101,10 +106,12 @@ class PinballGame extends PinballForge2DGame
children: [
ZCanvasComponent(
children: [
ArcadeBackground(),
BoardBackgroundSpriteComponent(),
Boundaries(),
Backbox(
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: _entries,
),
GoogleWord(position: Vector2(-4.45, 1.8)),
@ -187,6 +194,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({
required CharacterThemeCubit characterThemeBloc,
required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository,
required AppLocalizations l10n,
required PinballAudioPlayer audioPlayer,
required GameBloc gameBloc,
@ -194,6 +202,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
characterThemeBloc: characterThemeBloc,
audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
l10n: l10n,
gameBloc: gameBloc,
);

@ -5,13 +5,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/more_information/more_information.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
import 'package:share_repository/share_repository.dart';
class PinballGamePage extends StatelessWidget {
const PinballGamePage({
@ -26,12 +26,14 @@ class PinballGamePage extends StatelessWidget {
final characterThemeBloc = context.read<CharacterThemeCubit>();
final audioPlayer = context.read<PinballAudioPlayer>();
final leaderboardRepository = context.read<LeaderboardRepository>();
final shareRepository = context.read<ShareRepository>();
final gameBloc = context.read<GameBloc>();
final game = isDebugMode
? DebugPinballGame(
characterThemeBloc: characterThemeBloc,
audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
l10n: context.l10n,
gameBloc: gameBloc,
)
@ -39,18 +41,16 @@ class PinballGamePage extends StatelessWidget {
characterThemeBloc: characterThemeBloc,
audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
l10n: context.l10n,
gameBloc: gameBloc,
);
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: BlocProvider(
create: (_) => AssetsManagerCubit(game, audioPlayer)..load(),
child: PinballGameView(game),
),
return Scaffold(
backgroundColor: PinballColors.black,
body: BlocProvider(
create: (_) => AssetsManagerCubit(game, audioPlayer)..load(),
child: PinballGameView(game),
),
);
}
@ -166,7 +166,7 @@ class _PositionedInfoIcon extends StatelessWidget {
visible: state.status.isGameOver,
child: IconButton(
iconSize: 50,
icon: Assets.images.linkBox.infoIcon.image(),
icon: const Icon(Icons.info, color: PinballColors.white),
onPressed: () => showMoreInformationDialog(context),
),
);

@ -14,7 +14,6 @@ class $AssetsImagesGen {
const $AssetsImagesBonusAnimationGen();
$AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesLoadingGameGen get loadingGame =>
const $AssetsImagesLoadingGameGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
@ -56,14 +55,6 @@ class $AssetsImagesComponentsGen {
const AssetGenImage('assets/images/components/space.png');
}
class $AssetsImagesLinkBoxGen {
const $AssetsImagesLinkBoxGen();
/// File path: assets/images/link_box/info_icon.png
AssetGenImage get infoIcon =>
const AssetGenImage('assets/images/link_box/info_icon.png');
}
class $AssetsImagesLoadingGameGen {
const $AssetsImagesLoadingGameGen();

@ -189,7 +189,7 @@
"description": "Text shown on the mobile controls enter button"
},
"initialsErrorTitle": "Uh-oh... well, that didnt work",
"@enter": {
"@initialsErrorTitle": {
"description": "Title shown when the initials submission fails"
},
"initialsErrorMessage": "Please try a different combination of letters",
@ -200,4 +200,26 @@
"@leaderboardErrorMessage": {
"description": "Text shown when the leaderboard had an error while loading"
}
,
"letEveryone": "Let everyone know about I/O Pinball",
"@letEveryone": {
"description": "Text displayed on share screen for description"
},
"bySharingYourScore": "by sharing your score to your preferred",
"@bySharingYourScore": {
"description": "Text displayed on share screen for description"
},
"socialMediaAccount": "social media account!",
"@socialMediaAccount": {
"description": "Text displayed on share screen for description"
},
"iGotScoreAtPinball": "I got {score} at the #IOPinball machine, can you beat my score? See you at #GoogleIO!",
"@iGotScoreAtPinball": {
"description": "Text to share score on Social Network",
"placeholders": {
"score": {
"type": "int"
}
}
}
}

@ -6,10 +6,13 @@ 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';
import 'package:share_repository/share_repository.dart';
void main() {
bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore);
const shareRepository =
ShareRepository(appUrl: ShareRepository.pinballGameUrl);
final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudioPlayer = PinballAudioPlayer();
unawaited(
@ -20,6 +23,7 @@ void main() {
return App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
pinballAudioPlayer: pinballAudioPlayer,
);
});

@ -17,6 +17,7 @@ class $AssetsSfxGen {
String get android => 'assets/sfx/android.mp3';
String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3';
String get cowMoo => 'assets/sfx/cow_moo.mp3';
String get dash => 'assets/sfx/dash.mp3';
String get dino => 'assets/sfx/dino.mp3';
String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3';

@ -1,32 +1,36 @@
import 'dart:math';
import 'package:audioplayers/audioplayers.dart';
import 'package:clock/clock.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart';
import 'package:flutter/material.dart';
import 'package:pinball_audio/gen/assets.gen.dart';
/// Sounds available for play
/// Sounds available to play.
enum PinballAudio {
/// Google
/// Google.
google,
/// Bumper
/// Bumper.
bumper,
/// Background music
/// Cow moo.
cowMoo,
/// Background music.
backgroundMusic,
/// IO Pinball voice over
/// IO Pinball voice over.
ioPinballVoiceOver,
/// Game over
/// Game over.
gameOverVoiceOver,
/// Launcher
/// Launcher.
launcher,
/// Sparky
/// Sparky.
sparky,
/// Android
@ -145,8 +149,37 @@ class _BumperAudio extends _Audio {
}
}
class _ThrottledAudio extends _Audio {
_ThrottledAudio({
required this.preCacheSingleAudio,
required this.playSingleAudio,
required this.path,
required this.duration,
});
final PreCacheSingleAudio preCacheSingleAudio;
final PlaySingleAudio playSingleAudio;
final String path;
final Duration duration;
DateTime? _lastPlayed;
@override
Future<void> load() => preCacheSingleAudio(prefixFile(path));
@override
void play() {
final now = clock.now();
if (_lastPlayed == null ||
(_lastPlayed != null && now.difference(_lastPlayed!) > duration)) {
_lastPlayed = now;
playSingleAudio(prefixFile(path));
}
}
}
/// {@template pinball_audio_player}
/// Sound manager for the pinball game
/// Sound manager for the pinball game.
/// {@endtemplate}
class PinballAudioPlayer {
/// {@macro pinball_audio_player}
@ -212,6 +245,12 @@ class PinballAudioPlayer {
createAudioPool: _createAudioPool,
seed: _seed,
),
PinballAudio.cowMoo: _ThrottledAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.cowMoo,
duration: const Duration(seconds: 2),
),
PinballAudio.backgroundMusic: _LoopAudio(
preCacheSingleAudio: _preCacheSingleAudio,
loopSingleAudio: _loopSingleAudio,
@ -232,19 +271,19 @@ class PinballAudioPlayer {
final Random _seed;
/// Registered audios on the Player
/// Registered audios on the Player.
@visibleForTesting
// ignore: library_private_types_in_public_api
late final Map<PinballAudio, _Audio> audios;
/// Loads the sounds effects into the memory
/// Loads the sounds effects into the memory.
List<Future<void>> load() {
_configureAudioCache(FlameAudio.audioCache);
return audios.values.map((a) => a.load()).toList();
}
/// Plays the received audio
/// Plays the received audio.
void play(PinballAudio audio) {
assert(
audios.containsKey(audio),

@ -8,6 +8,7 @@ environment:
dependencies:
audioplayers: ^0.20.1
clock: ^1.1.0
flame_audio: ^1.0.1
flutter:
sdk: flutter

@ -2,6 +2,7 @@
import 'dart:math';
import 'package:audioplayers/audioplayers.dart';
import 'package:clock/clock.dart';
import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart';
import 'package:flutter_test/flutter_test.dart';
@ -43,6 +44,8 @@ class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {}
class _MockRandom extends Mock implements Random {}
class _MockClock extends Mock implements Clock {}
void main() {
group('PinballAudio', () {
late _MockCreateAudioPool createAudioPool;
@ -171,6 +174,10 @@ void main() {
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/launcher.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/cow_moo.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'),
@ -227,6 +234,42 @@ void main() {
});
});
group('cow moo', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.cowMoo);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'),
).called(1);
});
test('only plays the sound again after 2 seconds', () async {
final clock = _MockClock();
await withClock(clock, () async {
when(clock.now).thenReturn(DateTime(2022));
await Future.wait(audioPlayer.load());
audioPlayer
..play(PinballAudio.cowMoo)
..play(PinballAudio.cowMoo);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'),
).called(1);
when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 2));
audioPlayer.play(PinballAudio.cowMoo);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'),
).called(1);
});
});
});
group('google', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@ -59,6 +59,9 @@ class $AssetsImagesAndroidGen {
class $AssetsImagesBackboxGen {
const $AssetsImagesBackboxGen();
$AssetsImagesBackboxButtonGen get button =>
const $AssetsImagesBackboxButtonGen();
/// File path: assets/images/backbox/display_divider.png
AssetGenImage get displayDivider =>
const AssetGenImage('assets/images/backbox/display_divider.png');
@ -386,6 +389,18 @@ class $AssetsImagesAndroidSpaceshipGen {
const AssetGenImage('assets/images/android/spaceship/saucer.png');
}
class $AssetsImagesBackboxButtonGen {
const $AssetsImagesBackboxButtonGen();
/// File path: assets/images/backbox/button/facebook.png
AssetGenImage get facebook =>
const AssetGenImage('assets/images/backbox/button/facebook.png');
/// File path: assets/images/backbox/button/twitter.png
AssetGenImage get twitter =>
const AssetGenImage('assets/images/backbox/button/twitter.png');
}
class $AssetsImagesDashBumperGen {
const $AssetsImagesDashBumperGen();

@ -0,0 +1,92 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
export 'cubit/arcade_background_cubit.dart';
/// {@template arcade_background}
/// Background of the arcade that the pinball machine lives in.
/// {@endtemplate}
class ArcadeBackground extends Component with ZIndex {
/// {@macro arcade_background}
ArcadeBackground({String? assetPath})
: this._(
bloc: ArcadeBackgroundCubit(),
assetPath: assetPath,
);
ArcadeBackground._({required this.bloc, String? assetPath})
: super(
children: [
FlameBlocProvider<ArcadeBackgroundCubit,
ArcadeBackgroundState>.value(
value: bloc,
children: [ArcadeBackgroundSpriteComponent(assetPath: assetPath)],
)
],
) {
zIndex = ZIndexes.arcadeBackground;
}
/// Creates an [ArcadeBackground] without any behaviors.
///
/// This can be used for testing [ArcadeBackground]'s behaviors in isolation.
@visibleForTesting
ArcadeBackground.test({
ArcadeBackgroundCubit? bloc,
String? assetPath,
}) : bloc = bloc ?? ArcadeBackgroundCubit(),
super(
children: [
FlameBlocProvider<ArcadeBackgroundCubit,
ArcadeBackgroundState>.value(
value: bloc ?? ArcadeBackgroundCubit(),
children: [ArcadeBackgroundSpriteComponent(assetPath: assetPath)],
)
],
);
/// Bloc to update the arcade background sprite when a new character is
/// selected.
final ArcadeBackgroundCubit bloc;
}
/// {@template arcade_background_sprite_component}
/// [SpriteComponent] for the [ArcadeBackground].
/// {@endtemplate}
@visibleForTesting
class ArcadeBackgroundSpriteComponent extends SpriteComponent
with
FlameBlocListenable<ArcadeBackgroundCubit, ArcadeBackgroundState>,
HasGameRef {
/// {@macro arcade_background_sprite_component}
ArcadeBackgroundSpriteComponent({required String? assetPath})
: _assetPath = assetPath,
super(
anchor: Anchor.bottomCenter,
position: Vector2(0, 72.3),
);
final String? _assetPath;
@override
void onNewState(ArcadeBackgroundState state) {
sprite = Sprite(
gameRef.images.fromCache(state.characterTheme.background.keyName),
);
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images
.fromCache(_assetPath ?? theme.Assets.images.dash.background.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

@ -0,0 +1,15 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball_theme/pinball_theme.dart';
part 'arcade_background_state.dart';
class ArcadeBackgroundCubit extends Cubit<ArcadeBackgroundState> {
ArcadeBackgroundCubit() : super(const ArcadeBackgroundState.initial());
void onCharacterSelected(CharacterTheme characterTheme) {
emit(ArcadeBackgroundState(characterTheme: characterTheme));
}
}

@ -0,0 +1,15 @@
// ignore_for_file: public_member_api_docs
part of 'arcade_background_cubit.dart';
class ArcadeBackgroundState extends Equatable {
const ArcadeBackgroundState({required this.characterTheme});
const ArcadeBackgroundState.initial()
: this(characterTheme: const DashTheme());
final CharacterTheme characterTheme;
@override
List<Object> get props => [characterTheme];
}

@ -7,7 +7,7 @@ part 'ball_state.dart';
class BallCubit extends Cubit<BallState> {
BallCubit() : super(const BallState.initial());
void onThemeChanged(CharacterTheme characterTheme) {
void onCharacterSelected(CharacterTheme characterTheme) {
emit(BallState(characterTheme: characterTheme));
}
}

@ -1,6 +1,7 @@
export 'android_animatronic.dart';
export 'android_bumper/android_bumper.dart';
export 'android_spaceship/android_spaceship.dart';
export 'arcade_background/arcade_background.dart';
export 'ball/ball.dart';
export 'baseboard.dart';
export 'board_background_sprite_component.dart';

@ -18,6 +18,8 @@ abstract class ZIndexes {
// Background
static const arcadeBackground = _below + boardBackground;
static const boardBackground = 5 * _below + _base;
static const decal = _above + boardBackground;

@ -90,6 +90,7 @@ flutter:
- assets/images/multiplier/x6/
- assets/images/score/
- assets/images/backbox/
- assets/images/backbox/button/
- assets/images/flapper/
- assets/images/skill_shot/

@ -0,0 +1,79 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
theme.Assets.images.android.background.keyName,
theme.Assets.images.dash.background.keyName,
theme.Assets.images.dino.background.keyName,
theme.Assets.images.sparky.background.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('ArcadeBackground', () {
test(
'can be instantiated',
() {
expect(ArcadeBackground(), isA<ArcadeBackground>());
expect(ArcadeBackground.test(), isA<ArcadeBackground>());
},
);
flameTester.test(
'loads correctly',
(game) async {
final ball = ArcadeBackground();
await game.ready();
await game.ensureAdd(ball);
expect(game.contains(ball), isTrue);
},
);
flameTester.test(
'has only one SpriteComponent',
(game) async {
final ball = ArcadeBackground();
await game.ready();
await game.ensureAdd(ball);
expect(
ball.descendants().whereType<SpriteComponent>().length,
equals(1),
);
},
);
flameTester.test(
'ArcadeBackgroundSpriteComponent changes sprite onNewState',
(game) async {
final ball = ArcadeBackground();
await game.ready();
await game.ensureAdd(ball);
final ballSprite = ball
.descendants()
.whereType<ArcadeBackgroundSpriteComponent>()
.single;
final originalSprite = ballSprite.sprite;
ballSprite.onNewState(
const ArcadeBackgroundState(characterTheme: theme.DinoTheme()),
);
await game.ready();
final newSprite = ballSprite.sprite;
expect(newSprite != originalSprite, isTrue);
},
);
});
}

@ -0,0 +1,22 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group(
'ArcadeBackgroundCubit',
() {
blocTest<ArcadeBackgroundCubit, ArcadeBackgroundState>(
'onCharacterSelected emits new theme',
build: ArcadeBackgroundCubit.new,
act: (bloc) => bloc.onCharacterSelected(const DinoTheme()),
expect: () => [
const ArcadeBackgroundState(
characterTheme: DinoTheme(),
),
],
);
},
);
}

@ -0,0 +1,32 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group('ArcadeBackgroundState', () {
test('supports value equality', () {
expect(
ArcadeBackgroundState(characterTheme: DashTheme()),
equals(ArcadeBackgroundState(characterTheme: DashTheme())),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
ArcadeBackgroundState(characterTheme: DashTheme()),
isNotNull,
);
});
test('initial contains DashTheme', () {
expect(
ArcadeBackgroundState.initial().characterTheme,
DashTheme(),
);
});
});
});
}

@ -8,9 +8,9 @@ void main() {
'BallCubit',
() {
blocTest<BallCubit, BallState>(
'onThemeChanged emits new theme',
'onCharacterSelected emits new theme',
build: BallCubit.new,
act: (bloc) => bloc.onThemeChanged(const DinoTheme()),
act: (bloc) => bloc.onCharacterSelected(const DinoTheme()),
expect: () => [const BallState(characterTheme: DinoTheme())],
);
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

@ -32,9 +32,9 @@ class $AssetsImagesAndroidGen {
AssetGenImage get animation =>
const AssetGenImage('assets/images/android/animation.png');
/// File path: assets/images/android/background.png
/// File path: assets/images/android/background.jpg
AssetGenImage get background =>
const AssetGenImage('assets/images/android/background.png');
const AssetGenImage('assets/images/android/background.jpg');
/// File path: assets/images/android/ball.png
AssetGenImage get ball =>
@ -56,9 +56,9 @@ class $AssetsImagesDashGen {
AssetGenImage get animation =>
const AssetGenImage('assets/images/dash/animation.png');
/// File path: assets/images/dash/background.png
/// File path: assets/images/dash/background.jpg
AssetGenImage get background =>
const AssetGenImage('assets/images/dash/background.png');
const AssetGenImage('assets/images/dash/background.jpg');
/// File path: assets/images/dash/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/dash/ball.png');
@ -78,9 +78,9 @@ class $AssetsImagesDinoGen {
AssetGenImage get animation =>
const AssetGenImage('assets/images/dino/animation.png');
/// File path: assets/images/dino/background.png
/// File path: assets/images/dino/background.jpg
AssetGenImage get background =>
const AssetGenImage('assets/images/dino/background.png');
const AssetGenImage('assets/images/dino/background.jpg');
/// File path: assets/images/dino/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/dino/ball.png');
@ -100,9 +100,9 @@ class $AssetsImagesSparkyGen {
AssetGenImage get animation =>
const AssetGenImage('assets/images/sparky/animation.png');
/// File path: assets/images/sparky/background.png
/// File path: assets/images/sparky/background.jpg
AssetGenImage get background =>
const AssetGenImage('assets/images/sparky/background.png');
const AssetGenImage('assets/images/sparky/background.jpg');
/// File path: assets/images/sparky/ball.png
AssetGenImage get ball =>

@ -5,6 +5,9 @@ abstract class PinballColors {
/// Color: 0xFFFFFFFF
static const Color white = Color(0xFFFFFFFF);
/// Color: 0xFF000000
static const Color black = Color(0xFF000000);
/// Color: 0xFF0C32A4
static const Color darkBlue = Color(0xFF0C32A4);

@ -8,6 +8,10 @@ void main() {
expect(PinballColors.white, const Color(0xFFFFFFFF));
});
test('black is 0xFF000000', () {
expect(PinballColors.black, const Color(0xFF000000));
});
test('darkBlue is 0xFF0C32A4', () {
expect(PinballColors.darkBlue, const Color(0xFF0C32A4));
});

@ -17,6 +17,9 @@ class ShareRepository {
/// Url to the Google IO Event.
static const googleIOEvent = 'https://events.google.com/io/';
/// Url to the Pinball game.
static const pinballGameUrl = 'https://ashehwkdkdjruejdnensjsjdne.web.app/#/';
/// Returns a url to share the [value] on the given [platform].
///
/// The returned url can be opened using the [url_launcher](https://pub.dev/packages/url_launcher) package.

@ -64,7 +64,6 @@ flutter:
- assets/images/components/
- assets/images/bonus_animation/
- assets/images/score/
- assets/images/link_box/
- assets/images/loading_game/
flutter_gen:

@ -5,6 +5,7 @@ import 'package:mocktail/mocktail.dart';
import 'package:pinball/app/app.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:share_repository/share_repository.dart';
class _MockAuthenticationRepository extends Mock
implements AuthenticationRepository {}
@ -14,15 +15,19 @@ class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
}
class _MockShareRepository extends Mock implements ShareRepository {}
void main() {
group('App', () {
late AuthenticationRepository authenticationRepository;
late LeaderboardRepository leaderboardRepository;
late ShareRepository shareRepository;
late PinballAudioPlayer pinballAudioPlayer;
setUp(() {
authenticationRepository = _MockAuthenticationRepository();
leaderboardRepository = _MockLeaderboardRepository();
shareRepository = _MockShareRepository();
pinballAudioPlayer = _MockPinballAudioPlayer();
when(pinballAudioPlayer.load).thenAnswer((_) => [Future.value()]);
});
@ -32,6 +37,7 @@ void main() {
App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
pinballAudioPlayer: pinballAudioPlayer,
),
);

@ -16,9 +16,7 @@ class _TestGame extends Forge2DGame {
return ensureAdd(
FlameProvider<PinballAudioPlayer>.value(
audioPlayer,
children: [
child,
],
children: [child],
),
);
}
@ -36,26 +34,26 @@ class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('BumperNoiseBehavior', () {});
late PinballAudioPlayer audioPlayer;
final flameTester = FlameTester(_TestGame.new);
setUp(() {
audioPlayer = _MockPinballAudioPlayer();
group('BumperNoiseBehavior', () {
late PinballAudioPlayer audioPlayer;
final flameTester = FlameTester(_TestGame.new);
setUp(() {
audioPlayer = _MockPinballAudioPlayer();
});
flameTester.testGameWidget(
'plays bumper sound',
setUp: (game, _) async {
final behavior = BumperNoiseBehavior();
final parent = _TestBodyComponent();
await game.pump(parent, audioPlayer: audioPlayer);
await parent.ensureAdd(behavior);
behavior.beginContact(Object(), _MockContact());
},
verify: (_, __) async {
verify(() => audioPlayer.play(PinballAudio.bumper)).called(1);
},
);
});
flameTester.testGameWidget(
'plays bumper sound',
setUp: (game, _) async {
final behavior = BumperNoiseBehavior();
final parent = _TestBodyComponent();
await game.pump(parent, audioPlayer: audioPlayer);
await parent.ensureAdd(behavior);
behavior.beginContact(Object(), _MockContact());
},
verify: (_, __) async {
verify(() => audioPlayer.play(PinballAudio.bumper)).called(1);
},
);
}

@ -20,6 +20,8 @@ class _TestGame extends Forge2DGame {
await images.loadAll([
theme.Assets.images.dash.ball.keyName,
theme.Assets.images.dino.ball.keyName,
theme.Assets.images.dash.background.keyName,
theme.Assets.images.dino.background.keyName,
]);
}
@ -38,32 +40,66 @@ class _TestGame extends Forge2DGame {
class _MockBallCubit extends Mock implements BallCubit {}
class _MockArcadeBackgroundCubit extends Mock implements ArcadeBackgroundCubit {
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group(
'BallThemingBehavior',
'CharacterSelectionBehavior',
() {
final flameTester = FlameTester(_TestGame.new);
test('can be instantiated', () {
expect(
BallThemingBehavior(),
isA<BallThemingBehavior>(),
CharacterSelectionBehavior(),
isA<CharacterSelectionBehavior>(),
);
});
flameTester.test(
'loads',
(game) async {
final behavior = BallThemingBehavior();
final behavior = CharacterSelectionBehavior();
await game.pump([behavior]);
expect(game.descendants(), contains(behavior));
},
);
flameTester.test(
'onNewState calls onThemeChanged on the ball bloc',
'onNewState calls onCharacterSelected on the arcade background bloc',
(game) async {
final arcadeBackgroundBloc = _MockArcadeBackgroundCubit();
whenListen(
arcadeBackgroundBloc,
const Stream<ArcadeBackgroundState>.empty(),
initialState: const ArcadeBackgroundState.initial(),
);
final arcadeBackground =
ArcadeBackground.test(bloc: arcadeBackgroundBloc);
final behavior = CharacterSelectionBehavior();
await game.pump([
arcadeBackground,
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
Ball.test(),
]);
const dinoThemeState = CharacterThemeState(theme.DinoTheme());
behavior.onNewState(dinoThemeState);
await game.ready();
verify(
() => arcadeBackgroundBloc
.onCharacterSelected(dinoThemeState.characterTheme),
).called(1);
},
);
flameTester.test(
'onNewState calls onCharacterSelected on the ball bloc',
(game) async {
final ballBloc = _MockBallCubit();
whenListen(
@ -72,20 +108,22 @@ void main() {
initialState: const BallState.initial(),
);
final ball = Ball.test(bloc: ballBloc);
final behavior = BallThemingBehavior();
final behavior = CharacterSelectionBehavior();
await game.pump([
ball,
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
ArcadeBackground.test(),
]);
const dinoThemeState = CharacterThemeState(theme.DinoTheme());
behavior.onNewState(dinoThemeState);
await game.ready();
verify(() => ballBloc.onThemeChanged(dinoThemeState.characterTheme))
.called(1);
verify(
() => ballBloc.onCharacterSelected(dinoThemeState.characterTheme),
).called(1);
},
);
},

@ -0,0 +1,58 @@
// 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:mocktail/mocktail.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame {
Future<void> pump(
_TestBodyComponent child, {
required PinballAudioPlayer audioPlayer,
}) {
return ensureAdd(
FlameProvider<PinballAudioPlayer>.value(
audioPlayer,
children: [child],
),
);
}
}
class _TestBodyComponent extends BodyComponent {
@override
Body createBody() => world.createBody(BodyDef());
}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('CowBumperNoiseBehavior', () {
late PinballAudioPlayer audioPlayer;
final flameTester = FlameTester(_TestGame.new);
setUp(() {
audioPlayer = _MockPinballAudioPlayer();
});
flameTester.testGameWidget(
'plays cow moo sound on contact',
setUp: (game, _) async {
final behavior = CowBumperNoiseBehavior();
final parent = _TestBodyComponent();
await game.pump(parent, audioPlayer: audioPlayer);
await parent.ensureAdd(behavior);
behavior.beginContact(Object(), _MockContact());
},
verify: (_, __) async {
verify(() => audioPlayer.play(PinballAudio.cowMoo)).called(1);
},
);
});
}

@ -4,7 +4,7 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/behaviors/bumper_noise_behavior.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
@ -124,11 +124,26 @@ void main() {
for (final bumper in bumpers) {
expect(
bumper.firstChild<BumperNoiseBehavior>(),
isNotNull,
isA<BumperNoiseBehavior>(),
);
}
},
);
flameTester.test(
'one AndroidBumper with CowBumperNoiseBehavior',
(game) async {
await game.pump(AndroidAcres());
final bumpers = game.descendants().whereType<AndroidBumper>();
expect(
bumpers.singleWhere(
(bumper) => bumper.firstChild<CowBumperNoiseBehavior>() != null,
),
isA<AndroidBumper>(),
);
},
);
});
flameTester.test('adds a FlameBlocProvider', (game) async {

@ -20,7 +20,10 @@ import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import 'package:pinball_ui/pinball_ui.dart';
import 'package:platform_helper/platform_helper.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:share_repository/share_repository.dart';
class _TestGame extends Forge2DGame
with HasKeyboardHandlerComponents, HasTappables {
@ -36,6 +39,8 @@ class _TestGame extends Forge2DGame
character.leaderboardIcon.keyName,
Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName,
Assets.images.backbox.button.facebook.keyName,
Assets.images.backbox.button.twitter.keyName,
Assets.images.backbox.displayTitleDecoration.keyName,
]);
}
@ -75,8 +80,14 @@ class _MockBackboxBloc extends Mock implements BackboxBloc {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
}
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockTapDownInfo extends Mock implements TapDownInfo {}
class _MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {}
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get score => '';
@ -105,6 +116,15 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get loading => '';
@override
String get letEveryone => '';
@override
String get bySharingYourScore => '';
@override
String get socialMediaAccount => '';
@override
String get shareYourScore => '';
@ -134,6 +154,9 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get leaderboardErrorMessage => '';
@override
String iGotScoreAtPinball(int _) => '';
}
void main() {
@ -143,6 +166,7 @@ void main() {
late BackboxBloc bloc;
late PlatformHelper platformHelper;
late UrlLauncherPlatform urlLauncher;
setUp(() {
bloc = _MockBackboxBloc();
@ -161,6 +185,7 @@ void main() {
(game) async {
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -178,6 +203,7 @@ void main() {
await game.pump(
Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
),
);
@ -199,6 +225,7 @@ void main() {
leaderboardRepository: _MockLeaderboardRepository(),
initialEntries: [LeaderboardEntryData.empty],
),
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -230,6 +257,7 @@ void main() {
);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -258,6 +286,7 @@ void main() {
);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -270,7 +299,8 @@ void main() {
);
flameTester.test(
'adds the mobile controls overlay when platform is mobile',
'adds the mobile controls overlay '
'when platform is mobile at InitialsFormState',
(game) async {
final bloc = _MockBackboxBloc();
final platformHelper = _MockPlatformHelper();
@ -286,6 +316,7 @@ void main() {
when(() => platformHelper.isMobile).thenReturn(true);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -297,6 +328,33 @@ void main() {
},
);
flameTester.test(
'remove the mobile controls overlay '
'when InitialsSuccessState',
(game) async {
final bloc = _MockBackboxBloc();
final platformHelper = _MockPlatformHelper();
final state = InitialsSuccessState(score: 10);
whenListen(
bloc,
Stream<BackboxState>.empty(),
initialState: state,
);
when(() => platformHelper.isMobile).thenReturn(true);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
expect(
game.overlays.value,
isNot(contains(PinballGame.mobileControlsOverlay)),
);
},
);
flameTester.test(
'adds InitialsSubmissionSuccessDisplay on InitialsSuccessState',
(game) async {
@ -308,6 +366,7 @@ void main() {
);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -330,6 +389,7 @@ void main() {
);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -359,6 +419,7 @@ void main() {
);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -373,6 +434,145 @@ void main() {
},
);
group('ShareDisplay', () {
setUp(() async {
urlLauncher = _MockUrlLauncher();
UrlLauncherPlatform.instance = urlLauncher;
});
flameTester.test(
'adds ShareDisplay on ShareState',
(game) async {
final state = ShareState(score: 100);
whenListen(
bloc,
const Stream<InitialsSuccessState>.empty(),
initialState: state,
);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
expect(
game.descendants().whereType<ShareDisplay>().length,
equals(1),
);
},
);
flameTester.test(
'opens Facebook link when sharing with Facebook',
(game) async {
when(() => urlLauncher.canLaunch(any()))
.thenAnswer((_) async => true);
when(
() => urlLauncher.launch(
any(),
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
final state = ShareState(score: 100);
whenListen(
bloc,
const Stream<ShareState>.empty(),
initialState: state,
);
final shareRepository = _MockShareRepository();
const fakeUrl = 'http://fakeUrl';
when(
() => shareRepository.shareText(
value: any(named: 'value'),
platform: SharePlatform.facebook,
),
).thenReturn(fakeUrl);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: shareRepository,
platformHelper: platformHelper,
);
await game.pump(backbox);
final facebookButton =
game.descendants().whereType<FacebookButtonComponent>().first;
facebookButton.onTapDown(_MockTapDownInfo());
await game.ready();
verify(
() => shareRepository.shareText(
value: any(named: 'value'),
platform: SharePlatform.facebook,
),
).called(1);
},
);
flameTester.test(
'opens Twitter link when sharing with Twitter',
(game) async {
final state = ShareState(score: 100);
whenListen(
bloc,
Stream.value(state),
initialState: state,
);
final shareRepository = _MockShareRepository();
const fakeUrl = 'http://fakeUrl';
when(
() => shareRepository.shareText(
value: any(named: 'value'),
platform: SharePlatform.twitter,
),
).thenReturn(fakeUrl);
when(() => urlLauncher.canLaunch(any()))
.thenAnswer((_) async => true);
when(
() => urlLauncher.launch(
any(),
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
final backbox = Backbox.test(
bloc: bloc,
shareRepository: shareRepository,
platformHelper: platformHelper,
);
await game.pump(backbox);
final facebookButton =
game.descendants().whereType<TwitterButtonComponent>().first;
facebookButton.onTapDown(_MockTapDownInfo());
await game.ready();
verify(
() => shareRepository.shareText(
value: any(named: 'value'),
platform: SharePlatform.twitter,
),
).called(1);
},
);
});
flameTester.test(
'adds LeaderboardDisplay on LeaderboardSuccessState',
(game) async {
@ -384,6 +584,7 @@ void main() {
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -406,6 +607,7 @@ void main() {
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -429,6 +631,7 @@ void main() {
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);
@ -469,6 +672,7 @@ void main() {
final backbox = Backbox.test(
bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
);
await game.pump(backbox);

@ -137,6 +137,25 @@ void main() {
);
});
group('ShareScoreRequested', () {
blocTest<BackboxBloc, BackboxState>(
'emits ShareState',
setUp: () {
leaderboardRepository = _MockLeaderboardRepository();
},
build: () => BackboxBloc(
leaderboardRepository: leaderboardRepository,
initialEntries: emptyEntries,
),
act: (bloc) => bloc.add(
ShareScoreRequested(score: 100),
),
expect: () => [
ShareState(score: 100),
],
);
});
group('LeaderboardRequested', () {
blocTest<BackboxBloc, BackboxState>(
'adds [LoadingState, LeaderboardSuccessState] when request succeeds',

@ -203,5 +203,23 @@ void main() {
});
});
});
group('ShareState', () {
test('can be instantiated', () {
expect(
ShareState(score: 0),
isNotNull,
);
});
test('supports value comparison', () {
expect(
ShareState(score: 0),
equals(
ShareState(score: 0),
),
);
});
});
});
}

@ -0,0 +1,112 @@
// ignore_for_file: cascade_invocations
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/bloc/game_bloc.dart';
import 'package:pinball/game/components/backbox/displays/share_display.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame with HasTappables {
@override
Future<void> onLoad() async {
await super.onLoad();
images.prefix = '';
await images.loadAll(
[
Assets.images.backbox.button.facebook.keyName,
Assets.images.backbox.button.twitter.keyName,
],
);
}
Future<void> pump(ShareDisplay component) {
return ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(),
children: [
FlameProvider.value(
_MockAppLocalizations(),
children: [component],
),
],
),
);
}
}
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get letEveryone => '';
@override
String get bySharingYourScore => '';
@override
String get socialMediaAccount => '';
}
class _MockTapDownInfo extends Mock implements TapDownInfo {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('ShareDisplay', () {
flameTester.test(
'loads correctly',
(game) async {
final component = ShareDisplay();
await game.pump(component);
expect(game.descendants(), contains(component));
},
);
flameTester.test(
'calls onShare when Facebook button is tapped',
(game) async {
var tapped = false;
final tapDownInfo = _MockTapDownInfo();
final component = ShareDisplay(
onShare: (_) => tapped = true,
);
await game.pump(component);
final facebookButton =
component.descendants().whereType<FacebookButtonComponent>().first;
facebookButton.onTapDown(tapDownInfo);
expect(tapped, isTrue);
},
);
flameTester.test(
'calls onShare when Twitter button is tapped',
(game) async {
var tapped = false;
final tapDownInfo = _MockTapDownInfo();
final component = ShareDisplay(
onShare: (_) => tapped = true,
);
await game.pump(component);
final twitterButton =
component.descendants().whereType<TwitterButtonComponent>().first;
twitterButton.onTapDown(tapDownInfo);
expect(tapped, isTrue);
},
);
});
}

@ -14,6 +14,7 @@ import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import 'package:share_repository/share_repository.dart';
class _TestGame extends Forge2DGame {
@override
@ -65,6 +66,8 @@ class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
}
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockAppLocalizations extends Mock implements AppLocalizations {
@override
String get score => '';
@ -149,9 +152,11 @@ void main() {
'changes the backbox display',
(game) async {
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox(
leaderboardRepository: repository,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [],
);
@ -165,9 +170,11 @@ void main() {
'removes FlipperKeyControllingBehavior from Flipper',
(game) async {
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox(
leaderboardRepository: repository,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [],
);
final flipper = Flipper.test(side: BoardSide.left);
@ -193,9 +200,11 @@ void main() {
(game) async {
final audioPlayer = _MockPinballAudioPlayer();
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox(
leaderboardRepository: repository,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [],
);
await game.pump(
@ -245,9 +254,11 @@ void main() {
'adds key controlling behavior to Flippers when the game is started',
(game) async {
final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository();
final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox(
leaderboardRepository: repository,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [],
);
final flipper = Flipper.test(side: BoardSide.left);

@ -16,12 +16,14 @@ import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/src/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:share_repository/share_repository.dart';
class _TestPinballGame extends PinballGame {
_TestPinballGame()
: super(
characterThemeBloc: CharacterThemeCubit(),
leaderboardRepository: _MockLeaderboardRepository(),
shareRepository: _MockShareRepository(),
gameBloc: GameBloc(),
l10n: _MockAppLocalizations(),
audioPlayer: _MockPinballAudioPlayer(),
@ -41,6 +43,7 @@ class _TestDebugPinballGame extends DebugPinballGame {
: super(
characterThemeBloc: CharacterThemeCubit(),
leaderboardRepository: _MockLeaderboardRepository(),
shareRepository: _MockShareRepository(),
gameBloc: GameBloc(),
l10n: _MockAppLocalizations(),
audioPlayer: _MockPinballAudioPlayer(),
@ -81,6 +84,8 @@ class _MockDragEndInfo extends Mock implements DragEndInfo {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
}
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
void main() {
@ -113,11 +118,11 @@ void main() {
);
flameTester.test(
'has only one BallThemingBehavior',
'has only one CharacterSelectionBehavior',
(game) async {
await game.ready();
expect(
game.descendants().whereType<BallThemingBehavior>().length,
game.descendants().whereType<CharacterSelectionBehavior>().length,
equals(1),
);
},

@ -10,12 +10,12 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/more_information/more_information.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:share_repository/share_repository.dart';
import '../../helpers/helpers.dart';
@ -24,6 +24,7 @@ class _TestPinballGame extends PinballGame {
: super(
characterThemeBloc: CharacterThemeCubit(),
leaderboardRepository: _MockLeaderboardRepository(),
shareRepository: _MockShareRepository(),
gameBloc: GameBloc(),
l10n: _MockAppLocalizations(),
audioPlayer: _MockPinballAudioPlayer(),
@ -60,6 +61,8 @@ class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
}
class _MockShareRepository extends Mock implements ShareRepository {}
void main() {
final game = _TestPinballGame();
@ -312,7 +315,7 @@ void main() {
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
expect(find.image(Assets.images.linkBox.infoIcon), findsOneWidget);
expect(find.byIcon(Icons.info), findsOneWidget);
});
testWidgets('opens MoreInformationDialog when tapped', (tester) async {

@ -12,12 +12,15 @@ import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
import 'package:share_repository/share_repository.dart';
class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
}
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {}
class _MockGameBloc extends Mock implements GameBloc {}
@ -55,6 +58,7 @@ extension PumpApp on WidgetTester {
AssetsManagerCubit? assetsManagerCubit,
CharacterThemeCubit? characterThemeCubit,
LeaderboardRepository? leaderboardRepository,
ShareRepository? shareRepository,
PinballAudioPlayer? pinballAudioPlayer,
}) {
return runAsync(() {
@ -64,6 +68,9 @@ extension PumpApp on WidgetTester {
RepositoryProvider.value(
value: leaderboardRepository ?? _MockLeaderboardRepository(),
),
RepositoryProvider.value(
value: shareRepository ?? _MockShareRepository(),
),
RepositoryProvider.value(
value: pinballAudioPlayer ?? _buildDefaultPinballAudioPlayer(),
),

Loading…
Cancel
Save