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

@ -17,7 +17,9 @@ class AssetsLoadingPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
final headline1 = Theme.of(context).textTheme.headline1; final headline1 = Theme.of(context).textTheme.headline1;
return Center( return Container(
decoration: const CrtBackground(),
child: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -41,6 +43,7 @@ class AssetsLoadingPage extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }

@ -1,7 +1,8 @@
export 'ball_spawning_behavior.dart'; export 'ball_spawning_behavior.dart';
export 'ball_theming_behavior.dart';
export 'bonus_ball_spawning_behavior.dart'; export 'bonus_ball_spawning_behavior.dart';
export 'bonus_noise_behavior.dart'; export 'bonus_noise_behavior.dart';
export 'bumper_noise_behavior.dart'; export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart'; export 'camera_focusing_behavior.dart';
export 'character_selection_behavior.dart';
export 'cow_bumper_noise_behavior.dart';
export 'scoring_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/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// Updates the launch [Ball] to reflect character selections. /// Updates the [ArcadeBackground] and launch [Ball] to reflect character
class BallThemingBehavior extends Component /// selections.
class CharacterSelectionBehavior extends Component
with with
FlameBlocListenable<CharacterThemeCubit, CharacterThemeState>, FlameBlocListenable<CharacterThemeCubit, CharacterThemeState>,
HasGameRef { HasGameRef {
@override @override
void onNewState(CharacterThemeState state) { void onNewState(CharacterThemeState state) {
gameRef
.descendants()
.whereType<ArcadeBackground>()
.single
.bloc
.onCharacterSelected(state.characterTheme);
gameRef gameRef
.descendants() .descendants()
.whereType<Ball>() .whereType<Ball>()
.single .single
.bloc .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: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(), BumperNoiseBehavior(),
CowBumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-20.7, -13), )..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(), 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/bloc/backbox_bloc.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart'; import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' hide Assets; 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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart';
/// {@template backbox} /// {@template backbox}
/// The [Backbox] of the pinball machine. /// The [Backbox] of the pinball machine.
@ -18,21 +21,26 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef {
/// {@macro backbox} /// {@macro backbox}
Backbox({ Backbox({
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository,
required List<LeaderboardEntryData>? entries, required List<LeaderboardEntryData>? entries,
}) : _bloc = BackboxBloc( }) : _bloc = BackboxBloc(
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
initialEntries: entries, initialEntries: entries,
), ),
_shareRepository = shareRepository,
_platformHelper = PlatformHelper(); _platformHelper = PlatformHelper();
/// {@macro backbox} /// {@macro backbox}
@visibleForTesting @visibleForTesting
Backbox.test({ Backbox.test({
required BackboxBloc bloc, required BackboxBloc bloc,
required ShareRepository shareRepository,
required PlatformHelper platformHelper, required PlatformHelper platformHelper,
}) : _bloc = bloc, }) : _bloc = bloc,
_shareRepository = shareRepository,
_platformHelper = platformHelper; _platformHelper = platformHelper;
final ShareRepository _shareRepository;
late final Component _display; late final Component _display;
final BackboxBloc _bloc; final BackboxBloc _bloc;
final PlatformHelper _platformHelper; final PlatformHelper _platformHelper;
@ -87,6 +95,8 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef {
), ),
); );
} else if (state is InitialsSuccessState) { } else if (state is InitialsSuccessState) {
gameRef.overlays.remove(PinballGame.mobileControlsOverlay);
_display.add( _display.add(
GameOverInfoDisplay( GameOverInfoDisplay(
onShare: () { 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) { } else if (state is InitialsFailureState) {
_display.add( _display.add(
InitialsSubmissionFailureDisplay( InitialsSubmissionFailureDisplay(

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

@ -5,3 +5,4 @@ export 'initials_submission_success_display.dart';
export 'leaderboard_display.dart'; export 'leaderboard_display.dart';
export 'leaderboard_failure_display.dart'; export 'leaderboard_failure_display.dart';
export 'loading_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.sparky.bumper.c.dimmed.keyName),
images.load(components.Assets.images.backbox.marquee.keyName), images.load(components.Assets.images.backbox.marquee.keyName),
images.load(components.Assets.images.backbox.displayDivider.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( images.load(
components.Assets.images.backbox.displayTitleDecoration.keyName, 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.pin.keyName),
images.load(components.Assets.images.skillShot.lit.keyName), images.load(components.Assets.images.skillShot.lit.keyName),
images.load(components.Assets.images.skillShot.dimmed.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(androidTheme.leaderboardIcon.keyName),
images.load(dinoTheme.leaderboardIcon.keyName), images.load(androidTheme.background.keyName),
images.load(androidTheme.ball.keyName), images.load(androidTheme.ball.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(dashTheme.background.keyName),
images.load(dashTheme.ball.keyName), images.load(dashTheme.ball.keyName),
images.load(dinoTheme.leaderboardIcon.keyName),
images.load(dinoTheme.background.keyName),
images.load(dinoTheme.ball.keyName), images.load(dinoTheme.ball.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.background.keyName),
images.load(sparkyTheme.ball.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_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:share_repository/share_repository.dart';
class PinballGame extends PinballForge2DGame class PinballGame extends PinballForge2DGame
with HasKeyboardHandlerComponents, MultiTouchTapDetector, HasTappables { with HasKeyboardHandlerComponents, MultiTouchTapDetector, HasTappables {
PinballGame({ PinballGame({
required CharacterThemeCubit characterThemeBloc, required CharacterThemeCubit characterThemeBloc,
required this.leaderboardRepository, required this.leaderboardRepository,
required this.shareRepository,
required GameBloc gameBloc, required GameBloc gameBloc,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballAudioPlayer audioPlayer, required PinballAudioPlayer audioPlayer,
@ -51,6 +53,8 @@ class PinballGame extends PinballForge2DGame
final LeaderboardRepository leaderboardRepository; final LeaderboardRepository leaderboardRepository;
final ShareRepository shareRepository;
final AppLocalizations _l10n; final AppLocalizations _l10n;
final GameBloc _gameBloc; final GameBloc _gameBloc;
@ -84,13 +88,14 @@ class PinballGame extends PinballForge2DGame
providers: [ providers: [
FlameProvider<PinballAudioPlayer>.value(_audioPlayer), FlameProvider<PinballAudioPlayer>.value(_audioPlayer),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository), FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<ShareRepository>.value(shareRepository),
FlameProvider<AppLocalizations>.value(_l10n), FlameProvider<AppLocalizations>.value(_l10n),
], ],
children: [ children: [
BonusNoiseBehavior(), BonusNoiseBehavior(),
GameBlocStatusListener(), GameBlocStatusListener(),
BallSpawningBehavior(), BallSpawningBehavior(),
BallThemingBehavior(), CharacterSelectionBehavior(),
CameraFocusingBehavior(), CameraFocusingBehavior(),
CanvasComponent( CanvasComponent(
onSpritePainted: (paint) { onSpritePainted: (paint) {
@ -101,10 +106,12 @@ class PinballGame extends PinballForge2DGame
children: [ children: [
ZCanvasComponent( ZCanvasComponent(
children: [ children: [
ArcadeBackground(),
BoardBackgroundSpriteComponent(), BoardBackgroundSpriteComponent(),
Boundaries(), Boundaries(),
Backbox( Backbox(
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: _entries, entries: _entries,
), ),
GoogleWord(position: Vector2(-4.45, 1.8)), GoogleWord(position: Vector2(-4.45, 1.8)),
@ -187,6 +194,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({ DebugPinballGame({
required CharacterThemeCubit characterThemeBloc, required CharacterThemeCubit characterThemeBloc,
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballAudioPlayer audioPlayer, required PinballAudioPlayer audioPlayer,
required GameBloc gameBloc, required GameBloc gameBloc,
@ -194,6 +202,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
characterThemeBloc: characterThemeBloc, characterThemeBloc: characterThemeBloc,
audioPlayer: audioPlayer, audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
l10n: l10n, l10n: l10n,
gameBloc: gameBloc, gameBloc: gameBloc,
); );

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

@ -14,7 +14,6 @@ class $AssetsImagesGen {
const $AssetsImagesBonusAnimationGen(); const $AssetsImagesBonusAnimationGen();
$AssetsImagesComponentsGen get components => $AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesLoadingGameGen get loadingGame => $AssetsImagesLoadingGameGen get loadingGame =>
const $AssetsImagesLoadingGameGen(); const $AssetsImagesLoadingGameGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
@ -56,14 +55,6 @@ class $AssetsImagesComponentsGen {
const AssetGenImage('assets/images/components/space.png'); 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 { class $AssetsImagesLoadingGameGen {
const $AssetsImagesLoadingGameGen(); const $AssetsImagesLoadingGameGen();

@ -189,7 +189,7 @@
"description": "Text shown on the mobile controls enter button" "description": "Text shown on the mobile controls enter button"
}, },
"initialsErrorTitle": "Uh-oh... well, that didnt work", "initialsErrorTitle": "Uh-oh... well, that didnt work",
"@enter": { "@initialsErrorTitle": {
"description": "Title shown when the initials submission fails" "description": "Title shown when the initials submission fails"
}, },
"initialsErrorMessage": "Please try a different combination of letters", "initialsErrorMessage": "Please try a different combination of letters",
@ -200,4 +200,26 @@
"@leaderboardErrorMessage": { "@leaderboardErrorMessage": {
"description": "Text shown when the leaderboard had an error while loading" "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/app/app.dart';
import 'package:pinball/bootstrap.dart'; import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:share_repository/share_repository.dart';
void main() { void main() {
bootstrap((firestore, firebaseAuth) async { bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore); final leaderboardRepository = LeaderboardRepository(firestore);
const shareRepository =
ShareRepository(appUrl: ShareRepository.pinballGameUrl);
final authenticationRepository = AuthenticationRepository(firebaseAuth); final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudioPlayer = PinballAudioPlayer(); final pinballAudioPlayer = PinballAudioPlayer();
unawaited( unawaited(
@ -20,6 +23,7 @@ void main() {
return App( return App(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
pinballAudioPlayer: pinballAudioPlayer, pinballAudioPlayer: pinballAudioPlayer,
); );
}); });

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

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

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

@ -2,6 +2,7 @@
import 'dart:math'; import 'dart:math';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:clock/clock.dart';
import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
import 'package:flutter_test/flutter_test.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 _MockRandom extends Mock implements Random {}
class _MockClock extends Mock implements Clock {}
void main() { void main() {
group('PinballAudio', () { group('PinballAudio', () {
late _MockCreateAudioPool createAudioPool; late _MockCreateAudioPool createAudioPool;
@ -171,6 +174,10 @@ void main() {
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/launcher.mp3'), .onCall('packages/pinball_audio/assets/sfx/launcher.mp3'),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/cow_moo.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'), .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', () { group('google', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(audioPlayer.load()); 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 { class $AssetsImagesBackboxGen {
const $AssetsImagesBackboxGen(); const $AssetsImagesBackboxGen();
$AssetsImagesBackboxButtonGen get button =>
const $AssetsImagesBackboxButtonGen();
/// File path: assets/images/backbox/display_divider.png /// File path: assets/images/backbox/display_divider.png
AssetGenImage get displayDivider => AssetGenImage get displayDivider =>
const AssetGenImage('assets/images/backbox/display_divider.png'); const AssetGenImage('assets/images/backbox/display_divider.png');
@ -386,6 +389,18 @@ class $AssetsImagesAndroidSpaceshipGen {
const AssetGenImage('assets/images/android/spaceship/saucer.png'); 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 { class $AssetsImagesDashBumperGen {
const $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> { class BallCubit extends Cubit<BallState> {
BallCubit() : super(const BallState.initial()); BallCubit() : super(const BallState.initial());
void onThemeChanged(CharacterTheme characterTheme) { void onCharacterSelected(CharacterTheme characterTheme) {
emit(BallState(characterTheme: characterTheme)); emit(BallState(characterTheme: characterTheme));
} }
} }

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

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

@ -90,6 +90,7 @@ flutter:
- assets/images/multiplier/x6/ - assets/images/multiplier/x6/
- assets/images/score/ - assets/images/score/
- assets/images/backbox/ - assets/images/backbox/
- assets/images/backbox/button/
- assets/images/flapper/ - assets/images/flapper/
- assets/images/skill_shot/ - 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', 'BallCubit',
() { () {
blocTest<BallCubit, BallState>( blocTest<BallCubit, BallState>(
'onThemeChanged emits new theme', 'onCharacterSelected emits new theme',
build: BallCubit.new, build: BallCubit.new,
act: (bloc) => bloc.onThemeChanged(const DinoTheme()), act: (bloc) => bloc.onCharacterSelected(const DinoTheme()),
expect: () => [const BallState(characterTheme: 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 => AssetGenImage get animation =>
const AssetGenImage('assets/images/android/animation.png'); const AssetGenImage('assets/images/android/animation.png');
/// File path: assets/images/android/background.png /// File path: assets/images/android/background.jpg
AssetGenImage get background => AssetGenImage get background =>
const AssetGenImage('assets/images/android/background.png'); const AssetGenImage('assets/images/android/background.jpg');
/// File path: assets/images/android/ball.png /// File path: assets/images/android/ball.png
AssetGenImage get ball => AssetGenImage get ball =>
@ -56,9 +56,9 @@ class $AssetsImagesDashGen {
AssetGenImage get animation => AssetGenImage get animation =>
const AssetGenImage('assets/images/dash/animation.png'); const AssetGenImage('assets/images/dash/animation.png');
/// File path: assets/images/dash/background.png /// File path: assets/images/dash/background.jpg
AssetGenImage get background => AssetGenImage get background =>
const AssetGenImage('assets/images/dash/background.png'); const AssetGenImage('assets/images/dash/background.jpg');
/// File path: assets/images/dash/ball.png /// File path: assets/images/dash/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/dash/ball.png'); AssetGenImage get ball => const AssetGenImage('assets/images/dash/ball.png');
@ -78,9 +78,9 @@ class $AssetsImagesDinoGen {
AssetGenImage get animation => AssetGenImage get animation =>
const AssetGenImage('assets/images/dino/animation.png'); const AssetGenImage('assets/images/dino/animation.png');
/// File path: assets/images/dino/background.png /// File path: assets/images/dino/background.jpg
AssetGenImage get background => AssetGenImage get background =>
const AssetGenImage('assets/images/dino/background.png'); const AssetGenImage('assets/images/dino/background.jpg');
/// File path: assets/images/dino/ball.png /// File path: assets/images/dino/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/dino/ball.png'); AssetGenImage get ball => const AssetGenImage('assets/images/dino/ball.png');
@ -100,9 +100,9 @@ class $AssetsImagesSparkyGen {
AssetGenImage get animation => AssetGenImage get animation =>
const AssetGenImage('assets/images/sparky/animation.png'); const AssetGenImage('assets/images/sparky/animation.png');
/// File path: assets/images/sparky/background.png /// File path: assets/images/sparky/background.jpg
AssetGenImage get background => AssetGenImage get background =>
const AssetGenImage('assets/images/sparky/background.png'); const AssetGenImage('assets/images/sparky/background.jpg');
/// File path: assets/images/sparky/ball.png /// File path: assets/images/sparky/ball.png
AssetGenImage get ball => AssetGenImage get ball =>

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

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

@ -17,6 +17,9 @@ class ShareRepository {
/// Url to the Google IO Event. /// Url to the Google IO Event.
static const googleIOEvent = 'https://events.google.com/io/'; 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]. /// 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. /// 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/components/
- assets/images/bonus_animation/ - assets/images/bonus_animation/
- assets/images/score/ - assets/images/score/
- assets/images/link_box/
- assets/images/loading_game/ - assets/images/loading_game/
flutter_gen: flutter_gen:

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

@ -16,9 +16,7 @@ class _TestGame extends Forge2DGame {
return ensureAdd( return ensureAdd(
FlameProvider<PinballAudioPlayer>.value( FlameProvider<PinballAudioPlayer>.value(
audioPlayer, audioPlayer,
children: [ children: [child],
child,
],
), ),
); );
} }
@ -36,8 +34,7 @@ class _MockContact extends Mock implements Contact {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
group('BumperNoiseBehavior', () {}); group('BumperNoiseBehavior', () {
late PinballAudioPlayer audioPlayer; late PinballAudioPlayer audioPlayer;
final flameTester = FlameTester(_TestGame.new); final flameTester = FlameTester(_TestGame.new);
@ -58,4 +55,5 @@ void main() {
verify(() => audioPlayer.play(PinballAudio.bumper)).called(1); verify(() => audioPlayer.play(PinballAudio.bumper)).called(1);
}, },
); );
});
} }

@ -20,6 +20,8 @@ class _TestGame extends Forge2DGame {
await images.loadAll([ await images.loadAll([
theme.Assets.images.dash.ball.keyName, theme.Assets.images.dash.ball.keyName,
theme.Assets.images.dino.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 _MockBallCubit extends Mock implements BallCubit {}
class _MockArcadeBackgroundCubit extends Mock implements ArcadeBackgroundCubit {
}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
group( group(
'BallThemingBehavior', 'CharacterSelectionBehavior',
() { () {
final flameTester = FlameTester(_TestGame.new); final flameTester = FlameTester(_TestGame.new);
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
BallThemingBehavior(), CharacterSelectionBehavior(),
isA<BallThemingBehavior>(), isA<CharacterSelectionBehavior>(),
); );
}); });
flameTester.test( flameTester.test(
'loads', 'loads',
(game) async { (game) async {
final behavior = BallThemingBehavior(); final behavior = CharacterSelectionBehavior();
await game.pump([behavior]); await game.pump([behavior]);
expect(game.descendants(), contains(behavior)); expect(game.descendants(), contains(behavior));
}, },
); );
flameTester.test( 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 { (game) async {
final ballBloc = _MockBallCubit(); final ballBloc = _MockBallCubit();
whenListen( whenListen(
@ -72,20 +108,22 @@ void main() {
initialState: const BallState.initial(), initialState: const BallState.initial(),
); );
final ball = Ball.test(bloc: ballBloc); final ball = Ball.test(bloc: ballBloc);
final behavior = BallThemingBehavior(); final behavior = CharacterSelectionBehavior();
await game.pump([ await game.pump([
ball, ball,
behavior, behavior,
ZCanvasComponent(), ZCanvasComponent(),
Plunger.test(compressionDistance: 10), Plunger.test(compressionDistance: 10),
ArcadeBackground.test(),
]); ]);
const dinoThemeState = CharacterThemeState(theme.DinoTheme()); const dinoThemeState = CharacterThemeState(theme.DinoTheme());
behavior.onNewState(dinoThemeState); behavior.onNewState(dinoThemeState);
await game.ready(); await game.ready();
verify(() => ballBloc.onThemeChanged(dinoThemeState.characterTheme)) verify(
.called(1); () => 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_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/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/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -124,11 +124,26 @@ void main() {
for (final bumper in bumpers) { for (final bumper in bumpers) {
expect( expect(
bumper.firstChild<BumperNoiseBehavior>(), 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 { 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_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme; 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: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 class _TestGame extends Forge2DGame
with HasKeyboardHandlerComponents, HasTappables { with HasKeyboardHandlerComponents, HasTappables {
@ -36,6 +39,8 @@ class _TestGame extends Forge2DGame
character.leaderboardIcon.keyName, character.leaderboardIcon.keyName,
Assets.images.backbox.marquee.keyName, Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName, Assets.images.backbox.displayDivider.keyName,
Assets.images.backbox.button.facebook.keyName,
Assets.images.backbox.button.twitter.keyName,
Assets.images.backbox.displayTitleDecoration.keyName, Assets.images.backbox.displayTitleDecoration.keyName,
]); ]);
} }
@ -75,8 +80,14 @@ class _MockBackboxBloc extends Mock implements BackboxBloc {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
} }
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockTapDownInfo extends Mock implements TapDownInfo {} class _MockTapDownInfo extends Mock implements TapDownInfo {}
class _MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {}
class _MockAppLocalizations extends Mock implements AppLocalizations { class _MockAppLocalizations extends Mock implements AppLocalizations {
@override @override
String get score => ''; String get score => '';
@ -105,6 +116,15 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
@override @override
String get loading => ''; String get loading => '';
@override
String get letEveryone => '';
@override
String get bySharingYourScore => '';
@override
String get socialMediaAccount => '';
@override @override
String get shareYourScore => ''; String get shareYourScore => '';
@ -134,6 +154,9 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
@override @override
String get leaderboardErrorMessage => ''; String get leaderboardErrorMessage => '';
@override
String iGotScoreAtPinball(int _) => '';
} }
void main() { void main() {
@ -143,6 +166,7 @@ void main() {
late BackboxBloc bloc; late BackboxBloc bloc;
late PlatformHelper platformHelper; late PlatformHelper platformHelper;
late UrlLauncherPlatform urlLauncher;
setUp(() { setUp(() {
bloc = _MockBackboxBloc(); bloc = _MockBackboxBloc();
@ -161,6 +185,7 @@ void main() {
(game) async { (game) async {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); await game.pump(backbox);
@ -178,6 +203,7 @@ void main() {
await game.pump( await game.pump(
Backbox.test( Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
), ),
); );
@ -199,6 +225,7 @@ void main() {
leaderboardRepository: _MockLeaderboardRepository(), leaderboardRepository: _MockLeaderboardRepository(),
initialEntries: [LeaderboardEntryData.empty], initialEntries: [LeaderboardEntryData.empty],
), ),
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); await game.pump(backbox);
@ -230,6 +257,7 @@ void main() {
); );
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); await game.pump(backbox);
@ -258,6 +286,7 @@ void main() {
); );
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); await game.pump(backbox);
@ -270,7 +299,8 @@ void main() {
); );
flameTester.test( flameTester.test(
'adds the mobile controls overlay when platform is mobile', 'adds the mobile controls overlay '
'when platform is mobile at InitialsFormState',
(game) async { (game) async {
final bloc = _MockBackboxBloc(); final bloc = _MockBackboxBloc();
final platformHelper = _MockPlatformHelper(); final platformHelper = _MockPlatformHelper();
@ -286,6 +316,7 @@ void main() {
when(() => platformHelper.isMobile).thenReturn(true); when(() => platformHelper.isMobile).thenReturn(true);
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); 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( flameTester.test(
'adds InitialsSubmissionSuccessDisplay on InitialsSuccessState', 'adds InitialsSubmissionSuccessDisplay on InitialsSuccessState',
(game) async { (game) async {
@ -308,6 +366,7 @@ void main() {
); );
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); await game.pump(backbox);
@ -330,6 +389,7 @@ void main() {
); );
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); await game.pump(backbox);
@ -359,6 +419,7 @@ void main() {
); );
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); 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( flameTester.test(
'adds LeaderboardDisplay on LeaderboardSuccessState', 'adds LeaderboardDisplay on LeaderboardSuccessState',
(game) async { (game) async {
@ -384,6 +584,7 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); await game.pump(backbox);
@ -406,6 +607,7 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); await game.pump(backbox);
@ -429,6 +631,7 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); await game.pump(backbox);
@ -469,6 +672,7 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(),
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox); 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', () { group('LeaderboardRequested', () {
blocTest<BackboxBloc, BackboxState>( blocTest<BackboxBloc, BackboxState>(
'adds [LoadingState, LeaderboardSuccessState] when request succeeds', '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_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:pinball_theme/pinball_theme.dart' as theme;
import 'package:share_repository/share_repository.dart';
class _TestGame extends Forge2DGame { class _TestGame extends Forge2DGame {
@override @override
@ -65,6 +66,8 @@ class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
} }
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockAppLocalizations extends Mock implements AppLocalizations { class _MockAppLocalizations extends Mock implements AppLocalizations {
@override @override
String get score => ''; String get score => '';
@ -149,9 +152,11 @@ void main() {
'changes the backbox display', 'changes the backbox display',
(game) async { (game) async {
final component = GameBlocStatusListener(); final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository(); final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox( final backbox = Backbox(
leaderboardRepository: repository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [], entries: const [],
); );
@ -165,9 +170,11 @@ void main() {
'removes FlipperKeyControllingBehavior from Flipper', 'removes FlipperKeyControllingBehavior from Flipper',
(game) async { (game) async {
final component = GameBlocStatusListener(); final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository(); final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox( final backbox = Backbox(
leaderboardRepository: repository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [], entries: const [],
); );
final flipper = Flipper.test(side: BoardSide.left); final flipper = Flipper.test(side: BoardSide.left);
@ -193,9 +200,11 @@ void main() {
(game) async { (game) async {
final audioPlayer = _MockPinballAudioPlayer(); final audioPlayer = _MockPinballAudioPlayer();
final component = GameBlocStatusListener(); final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository(); final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox( final backbox = Backbox(
leaderboardRepository: repository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [], entries: const [],
); );
await game.pump( await game.pump(
@ -245,9 +254,11 @@ void main() {
'adds key controlling behavior to Flippers when the game is started', 'adds key controlling behavior to Flippers when the game is started',
(game) async { (game) async {
final component = GameBlocStatusListener(); final component = GameBlocStatusListener();
final repository = _MockLeaderboardRepository(); final leaderboardRepository = _MockLeaderboardRepository();
final shareRepository = _MockShareRepository();
final backbox = Backbox( final backbox = Backbox(
leaderboardRepository: repository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: const [], entries: const [],
); );
final flipper = Flipper.test(side: BoardSide.left); 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/select_character/select_character.dart';
import 'package:pinball_audio/src/pinball_audio.dart'; import 'package:pinball_audio/src/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:share_repository/share_repository.dart';
class _TestPinballGame extends PinballGame { class _TestPinballGame extends PinballGame {
_TestPinballGame() _TestPinballGame()
: super( : super(
characterThemeBloc: CharacterThemeCubit(), characterThemeBloc: CharacterThemeCubit(),
leaderboardRepository: _MockLeaderboardRepository(), leaderboardRepository: _MockLeaderboardRepository(),
shareRepository: _MockShareRepository(),
gameBloc: GameBloc(), gameBloc: GameBloc(),
l10n: _MockAppLocalizations(), l10n: _MockAppLocalizations(),
audioPlayer: _MockPinballAudioPlayer(), audioPlayer: _MockPinballAudioPlayer(),
@ -41,6 +43,7 @@ class _TestDebugPinballGame extends DebugPinballGame {
: super( : super(
characterThemeBloc: CharacterThemeCubit(), characterThemeBloc: CharacterThemeCubit(),
leaderboardRepository: _MockLeaderboardRepository(), leaderboardRepository: _MockLeaderboardRepository(),
shareRepository: _MockShareRepository(),
gameBloc: GameBloc(), gameBloc: GameBloc(),
l10n: _MockAppLocalizations(), l10n: _MockAppLocalizations(),
audioPlayer: _MockPinballAudioPlayer(), audioPlayer: _MockPinballAudioPlayer(),
@ -81,6 +84,8 @@ class _MockDragEndInfo extends Mock implements DragEndInfo {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
} }
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
void main() { void main() {
@ -113,11 +118,11 @@ void main() {
); );
flameTester.test( flameTester.test(
'has only one BallThemingBehavior', 'has only one CharacterSelectionBehavior',
(game) async { (game) async {
await game.ready(); await game.ready();
expect( expect(
game.descendants().whereType<BallThemingBehavior>().length, game.descendants().whereType<CharacterSelectionBehavior>().length,
equals(1), equals(1),
); );
}, },

@ -10,12 +10,12 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/more_information/more_information.dart'; import 'package:pinball/more_information/more_information.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:share_repository/share_repository.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -24,6 +24,7 @@ class _TestPinballGame extends PinballGame {
: super( : super(
characterThemeBloc: CharacterThemeCubit(), characterThemeBloc: CharacterThemeCubit(),
leaderboardRepository: _MockLeaderboardRepository(), leaderboardRepository: _MockLeaderboardRepository(),
shareRepository: _MockShareRepository(),
gameBloc: GameBloc(), gameBloc: GameBloc(),
l10n: _MockAppLocalizations(), l10n: _MockAppLocalizations(),
audioPlayer: _MockPinballAudioPlayer(), audioPlayer: _MockPinballAudioPlayer(),
@ -60,6 +61,8 @@ class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
} }
class _MockShareRepository extends Mock implements ShareRepository {}
void main() { void main() {
final game = _TestPinballGame(); final game = _TestPinballGame();
@ -312,7 +315,7 @@ void main() {
gameBloc: gameBloc, gameBloc: gameBloc,
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
); );
expect(find.image(Assets.images.linkBox.infoIcon), findsOneWidget); expect(find.byIcon(Icons.info), findsOneWidget);
}); });
testWidgets('opens MoreInformationDialog when tapped', (tester) async { 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/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
import 'package:share_repository/share_repository.dart';
class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}
class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
} }
class _MockShareRepository extends Mock implements ShareRepository {}
class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {}
class _MockGameBloc extends Mock implements GameBloc {} class _MockGameBloc extends Mock implements GameBloc {}
@ -55,6 +58,7 @@ extension PumpApp on WidgetTester {
AssetsManagerCubit? assetsManagerCubit, AssetsManagerCubit? assetsManagerCubit,
CharacterThemeCubit? characterThemeCubit, CharacterThemeCubit? characterThemeCubit,
LeaderboardRepository? leaderboardRepository, LeaderboardRepository? leaderboardRepository,
ShareRepository? shareRepository,
PinballAudioPlayer? pinballAudioPlayer, PinballAudioPlayer? pinballAudioPlayer,
}) { }) {
return runAsync(() { return runAsync(() {
@ -64,6 +68,9 @@ extension PumpApp on WidgetTester {
RepositoryProvider.value( RepositoryProvider.value(
value: leaderboardRepository ?? _MockLeaderboardRepository(), value: leaderboardRepository ?? _MockLeaderboardRepository(),
), ),
RepositoryProvider.value(
value: shareRepository ?? _MockShareRepository(),
),
RepositoryProvider.value( RepositoryProvider.value(
value: pinballAudioPlayer ?? _buildDefaultPinballAudioPlayer(), value: pinballAudioPlayer ?? _buildDefaultPinballAudioPlayer(),
), ),

Loading…
Cancel
Save