Merge branch 'main' into release

release
Tom Arra 4 years ago
commit 80040ae00d

@ -9,6 +9,7 @@ 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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
class App extends StatelessWidget { class App extends StatelessWidget {
@ -18,16 +19,19 @@ class App extends StatelessWidget {
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository, required ShareRepository shareRepository,
required PinballAudioPlayer pinballAudioPlayer, required PinballAudioPlayer pinballAudioPlayer,
required PlatformHelper platformHelper,
}) : _authenticationRepository = authenticationRepository, }) : _authenticationRepository = authenticationRepository,
_leaderboardRepository = leaderboardRepository, _leaderboardRepository = leaderboardRepository,
_shareRepository = shareRepository, _shareRepository = shareRepository,
_pinballAudioPlayer = pinballAudioPlayer, _pinballAudioPlayer = pinballAudioPlayer,
_platformHelper = platformHelper,
super(key: key); super(key: key);
final AuthenticationRepository _authenticationRepository; final AuthenticationRepository _authenticationRepository;
final LeaderboardRepository _leaderboardRepository; final LeaderboardRepository _leaderboardRepository;
final ShareRepository _shareRepository; final ShareRepository _shareRepository;
final PinballAudioPlayer _pinballAudioPlayer; final PinballAudioPlayer _pinballAudioPlayer;
final PlatformHelper _platformHelper;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -37,6 +41,7 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _shareRepository), RepositoryProvider.value(value: _shareRepository),
RepositoryProvider.value(value: _pinballAudioPlayer), RepositoryProvider.value(value: _pinballAudioPlayer),
RepositoryProvider.value(value: _platformHelper),
], ],
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [

@ -2,6 +2,8 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; 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';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:platform_helper/platform_helper.dart';
/// Updates the [ArcadeBackground] and launch [Ball] to reflect character /// Updates the [ArcadeBackground] and launch [Ball] to reflect character
/// selections. /// selections.
@ -11,12 +13,14 @@ class CharacterSelectionBehavior extends Component
HasGameRef { HasGameRef {
@override @override
void onNewState(CharacterThemeState state) { void onNewState(CharacterThemeState state) {
if (!readProvider<PlatformHelper>().isMobile) {
gameRef gameRef
.descendants() .descendants()
.whereType<ArcadeBackground>() .whereType<ArcadeBackground>()
.single .single
.bloc .bloc
.onCharacterSelected(state.characterTheme); .onCharacterSelected(state.characterTheme);
}
gameRef gameRef
.descendants() .descendants()
.whereType<Ball>() .whereType<Ball>()

@ -27,23 +27,19 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef {
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
initialEntries: entries, initialEntries: entries,
), ),
_shareRepository = shareRepository, _shareRepository = shareRepository;
_platformHelper = PlatformHelper();
/// {@macro backbox} /// {@macro backbox}
@visibleForTesting @visibleForTesting
Backbox.test({ Backbox.test({
required BackboxBloc bloc, required BackboxBloc bloc,
required ShareRepository shareRepository, required ShareRepository shareRepository,
required PlatformHelper platformHelper,
}) : _bloc = bloc, }) : _bloc = bloc,
_shareRepository = shareRepository, _shareRepository = shareRepository;
_platformHelper = platformHelper;
final ShareRepository _shareRepository; final ShareRepository _shareRepository;
late final Component _display; late final Component _display;
final BackboxBloc _bloc; final BackboxBloc _bloc;
final PlatformHelper _platformHelper;
late StreamSubscription<BackboxState> _subscription; late StreamSubscription<BackboxState> _subscription;
@override @override
@ -76,7 +72,7 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef {
} else if (state is LeaderboardFailureState) { } else if (state is LeaderboardFailureState) {
_display.add(LeaderboardFailureDisplay()); _display.add(LeaderboardFailureDisplay());
} else if (state is InitialsFormState) { } else if (state is InitialsFormState) {
if (_platformHelper.isMobile) { if (readProvider<PlatformHelper>().isMobile) {
gameRef.overlays.add(PinballGame.mobileControlsOverlay); gameRef.overlays.add(PinballGame.mobileControlsOverlay);
} }
_display.add( _display.add(
@ -109,7 +105,7 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef {
ShareDisplay( ShareDisplay(
onShare: (platform) { onShare: (platform) {
final message = readProvider<AppLocalizations>() final message = readProvider<AppLocalizations>()
.iGotScoreAtPinball(state.score); .iGotScoreAtPinball(state.score.formatScore());
final url = _shareRepository.shareText( final url = _shareRepository.shareText(
value: message, value: message,
platform: platform, platform: platform,

@ -212,7 +212,7 @@ class GoogleIOLinkComponent extends TextComponent with HasGameRef, Tappable {
); );
@override @override
bool onTapDown(TapDownInfo info) { bool onTapUp(TapUpInfo info) {
openLink(ShareRepository.googleIOEvent); openLink(ShareRepository.googleIOEvent);
return true; return true;
} }

@ -1,4 +1,6 @@
// cSpell:ignore sublist
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
@ -23,19 +25,9 @@ final _bodyTextPaint = TextPaint(
), ),
); );
/// {@template leaderboard_display}
/// Component that builds the leaderboard list of the Backbox.
/// {@endtemplate}
class LeaderboardDisplay extends PositionComponent with HasGameRef {
/// {@macro leaderboard_display}
LeaderboardDisplay({required List<LeaderboardEntryData> entries})
: _entries = entries;
final List<LeaderboardEntryData> _entries;
double _calcY(int i) => (i * 3.2) + 3.2; double _calcY(int i) => (i * 3.2) + 3.2;
static const _columns = [-15.0, 0.0, 15.0]; const _columns = [-14.0, 0.0, 14.0];
String _rank(int number) { String _rank(int number) {
switch (number) { switch (number) {
@ -50,6 +42,59 @@ class LeaderboardDisplay extends PositionComponent with HasGameRef {
} }
} }
/// {@template leaderboard_display}
/// Component that builds the leaderboard list of the Backbox.
/// {@endtemplate}
class LeaderboardDisplay extends PositionComponent with HasGameRef {
/// {@macro leaderboard_display}
LeaderboardDisplay({required List<LeaderboardEntryData> entries})
: _entries = entries;
final List<LeaderboardEntryData> _entries;
_MovePageArrow _findArrow({required bool active}) {
return descendants()
.whereType<_MovePageArrow>()
.firstWhere((arrow) => arrow.active == active);
}
void _changePage(List<LeaderboardEntryData> ranking, int offset) {
final current = descendants().whereType<_RankingPage>().single;
final activeArrow = _findArrow(active: true);
final inactiveArrow = _findArrow(active: false);
activeArrow.active = false;
current.add(
ScaleEffect.to(
Vector2(0, 1),
EffectController(
duration: 0.5,
curve: Curves.easeIn,
),
)..onFinishCallback = () {
current.removeFromParent();
inactiveArrow.active = true;
firstChild<PositionComponent>()?.add(
_RankingPage(
ranking: ranking,
offset: offset,
)
..scale = Vector2(0, 1)
..add(
ScaleEffect.to(
Vector2(1, 1),
EffectController(
duration: 0.5,
curve: Curves.easeIn,
),
),
),
);
},
);
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
position = Vector2(0, -30); position = Vector2(0, -30);
@ -60,6 +105,20 @@ class LeaderboardDisplay extends PositionComponent with HasGameRef {
PositionComponent( PositionComponent(
position: Vector2(0, 4), position: Vector2(0, 4),
children: [ children: [
_MovePageArrow(
position: Vector2(20, 9),
onTap: () {
_changePage(_entries.sublist(5), 5);
},
),
_MovePageArrow(
position: Vector2(-20, 9),
direction: ArrowIconDirection.left,
active: false,
onTap: () {
_changePage(_entries.take(5).toList(), 0);
},
),
PositionComponent( PositionComponent(
children: [ children: [
TextComponent( TextComponent(
@ -82,11 +141,33 @@ class LeaderboardDisplay extends PositionComponent with HasGameRef {
), ),
], ],
), ),
_RankingPage(
ranking: ranking,
offset: 0,
),
],
),
);
}
}
class _RankingPage extends PositionComponent with HasGameRef {
_RankingPage({
required this.ranking,
required this.offset,
}) : super(children: []);
final List<LeaderboardEntryData> ranking;
final int offset;
@override
Future<void> onLoad() async {
await addAll([
for (var i = 0; i < ranking.length; i++) for (var i = 0; i < ranking.length; i++)
PositionComponent( PositionComponent(
children: [ children: [
TextComponent( TextComponent(
text: _rank(i + 1), text: _rank(i + 1 + offset),
textRenderer: _bodyTextPaint, textRenderer: _bodyTextPaint,
position: Vector2(_columns[0], _calcY(i)), position: Vector2(_columns[0], _calcY(i)),
anchor: Anchor.center, anchor: Anchor.center,
@ -103,7 +184,7 @@ class LeaderboardDisplay extends PositionComponent with HasGameRef {
), ),
anchor: Anchor.center, anchor: Anchor.center,
size: Vector2(1.8, 1.8), size: Vector2(1.8, 1.8),
position: Vector2(_columns[2] - 2.5, _calcY(i) + .25), position: Vector2(_columns[2] - 3, _calcY(i) + .25),
), ),
TextComponent( TextComponent(
text: ranking[i].playerInitials, text: ranking[i].playerInitials,
@ -113,8 +194,53 @@ class LeaderboardDisplay extends PositionComponent with HasGameRef {
), ),
], ],
), ),
]);
}
}
class _MovePageArrow extends PositionComponent {
_MovePageArrow({
required Vector2 position,
required this.onTap,
this.direction = ArrowIconDirection.right,
bool active = true,
}) : super(
position: position,
children: [
if (active)
ArrowIcon(
position: Vector2.zero(),
direction: direction,
onTap: onTap,
),
SequenceEffect(
[
ScaleEffect.to(
Vector2.all(1.2),
EffectController(duration: 1),
),
ScaleEffect.to(Vector2.all(1), EffectController(duration: 1)),
],
infinite: true,
),
], ],
);
final ArrowIconDirection direction;
final VoidCallback onTap;
bool get active => children.whereType<ArrowIcon>().isNotEmpty;
set active(bool value) {
if (value) {
add(
ArrowIcon(
position: Vector2.zero(),
direction: direction,
onTap: onTap,
), ),
); );
} else {
firstChild<ArrowIcon>()?.removeFromParent();
}
} }
} }

@ -140,7 +140,7 @@ class FacebookButtonComponent extends SpriteComponent
final OnSocialShareTap? _onTap; final OnSocialShareTap? _onTap;
@override @override
bool onTapDown(TapDownInfo info) { bool onTapUp(TapUpInfo info) {
_onTap?.call(SharePlatform.facebook); _onTap?.call(SharePlatform.facebook);
return true; return true;
} }
@ -172,7 +172,7 @@ class TwitterButtonComponent extends SpriteComponent with HasGameRef, Tappable {
final OnSocialShareTap? _onTap; final OnSocialShareTap? _onTap;
@override @override
bool onTapDown(TapDownInfo info) { bool onTapUp(TapUpInfo info) {
_onTap?.call(SharePlatform.twitter); _onTap?.call(SharePlatform.twitter);
return true; return true;
} }

@ -6,7 +6,7 @@ export 'dino_desert/dino_desert.dart';
export 'drain/drain.dart'; export 'drain/drain.dart';
export 'flutter_forest/flutter_forest.dart'; export 'flutter_forest/flutter_forest.dart';
export 'game_bloc_status_listener.dart'; export 'game_bloc_status_listener.dart';
export 'google_word/google_word.dart'; export 'google_gallery/google_gallery.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'multiballs/multiballs.dart'; export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart'; export 'multipliers/multipliers.dart';

@ -0,0 +1,26 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated.
class GoogleWordBonusBehavior extends Component {
@override
Future<void> onLoad() async {
await super.onLoad();
await add(
FlameBlocListener<GoogleWordCubit, GoogleWordState>(
listenWhen: (_, state) => state.letterSpriteStates.values
.every((element) => element == GoogleLetterSpriteState.lit),
onNewState: (state) {
readBloc<GameBloc, GameState>()
.add(const BonusActivated(GameBonus.googleWord));
readBloc<GoogleWordCubit, GoogleWordState>().onBonusAwarded();
add(BonusBallSpawningBehavior());
},
),
);
}
}

@ -0,0 +1,47 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/google_gallery/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template google_gallery}
/// Middle section of the board containing the [GoogleWord] and the
/// [GoogleRollover]s.
/// {@endtemplate}
class GoogleGallery extends Component with ZIndex {
/// {@macro google_gallery}
GoogleGallery()
: super(
children: [
FlameBlocProvider<GoogleWordCubit, GoogleWordState>(
create: GoogleWordCubit.new,
children: [
GoogleRollover(
side: BoardSide.right,
children: [
ScoringContactBehavior(points: Points.fiveThousand),
],
),
GoogleRollover(
side: BoardSide.left,
children: [
ScoringContactBehavior(points: Points.fiveThousand),
],
),
GoogleWord(position: Vector2(-4.45, 1.8)),
GoogleWordBonusBehavior(),
],
),
],
) {
zIndex = ZIndexes.decal;
}
/// Creates a [GoogleGallery] without any children.
///
/// This can be used for testing [GoogleGallery]'s behaviors in isolation.
@visibleForTesting
GoogleGallery.test();
}

@ -1,29 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated.
class GoogleWordBonusBehavior extends Component
with ParentIsA<GoogleWord>, FlameBlocReader<GameBloc, GameState> {
@override
void onMount() {
super.onMount();
final googleLetters = parent.children.whereType<GoogleLetter>();
for (final letter in googleLetters) {
letter.bloc.stream.listen((_) {
final achievedBonus = googleLetters
.every((letter) => letter.bloc.state == GoogleLetterState.lit);
if (achievedBonus) {
bloc.add(const BonusActivated(GameBonus.googleWord));
for (final letter in googleLetters) {
letter.bloc.onReset();
}
}
});
}
}
}

@ -1,52 +0,0 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/scoring_behavior.dart';
import 'package:pinball/game/components/google_word/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template google_word}
/// Loads all [GoogleLetter]s to compose a [GoogleWord].
/// {@endtemplate}
class GoogleWord extends Component with ZIndex {
/// {@macro google_word}
GoogleWord({
required Vector2 position,
}) : super(
children: [
GoogleLetter(
0,
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-13.1, 1.72),
GoogleLetter(
1,
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-8.33, -0.75),
GoogleLetter(
2,
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(-2.88, -1.85),
GoogleLetter(
3,
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(2.88, -1.85),
GoogleLetter(
4,
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(8.33, -0.75),
GoogleLetter(
5,
children: [ScoringContactBehavior(points: Points.fiveThousand)],
)..initialPosition = position + Vector2(13.1, 1.72),
GoogleWordBonusBehavior(),
],
) {
zIndex = ZIndexes.decal;
}
/// Creates a [GoogleWord] without any children.
///
/// This can be used for testing [GoogleWord]'s behaviors in isolation.
@visibleForTesting
GoogleWord.test();
}

@ -11,7 +11,8 @@ class MultiballsBehavior extends Component
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
final hasChanged = previousState?.bonusHistory != newState.bonusHistory; final hasChanged = previousState?.bonusHistory != newState.bonusHistory;
final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty && final lastBonusIsMultiball = newState.bonusHistory.isNotEmpty &&
newState.bonusHistory.last == GameBonus.dashNest; (newState.bonusHistory.last == GameBonus.dashNest ||
newState.bonusHistory.last == GameBonus.googleWord);
return hasChanged && lastBonusIsMultiball; return hasChanged && lastBonusIsMultiball;
} }

@ -12,7 +12,7 @@ extension PinballGameAssetsX on PinballGame {
const androidTheme = AndroidTheme(); const androidTheme = AndroidTheme();
const dinoTheme = DinoTheme(); const dinoTheme = DinoTheme();
return [ final gameAssets = [
images.load(components.Assets.images.boardBackground.keyName), images.load(components.Assets.images.boardBackground.keyName),
images.load(components.Assets.images.ball.flameEffect.keyName), images.load(components.Assets.images.ball.flameEffect.keyName),
images.load(components.Assets.images.signpost.inactive.keyName), images.load(components.Assets.images.signpost.inactive.keyName),
@ -118,6 +118,10 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.googleWord.letter5.dimmed.keyName), images.load(components.Assets.images.googleWord.letter5.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter6.lit.keyName), images.load(components.Assets.images.googleWord.letter6.lit.keyName),
images.load(components.Assets.images.googleWord.letter6.dimmed.keyName), images.load(components.Assets.images.googleWord.letter6.dimmed.keyName),
images.load(components.Assets.images.googleRollover.left.decal.keyName),
images.load(components.Assets.images.googleRollover.left.pin.keyName),
images.load(components.Assets.images.googleRollover.right.decal.keyName),
images.load(components.Assets.images.googleRollover.right.pin.keyName),
images.load(components.Assets.images.multiball.lit.keyName), images.load(components.Assets.images.multiball.lit.keyName),
images.load(components.Assets.images.multiball.dimmed.keyName), images.load(components.Assets.images.multiball.dimmed.keyName),
images.load(components.Assets.images.multiplier.x2.lit.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName),
@ -141,18 +145,24 @@ 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(components.Assets.images.displayArrows.arrowLeft.keyName),
images.load(components.Assets.images.displayArrows.arrowRight.keyName),
images.load(androidTheme.leaderboardIcon.keyName), images.load(androidTheme.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.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.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.leaderboardIcon.keyName),
images.load(sparkyTheme.background.keyName),
images.load(sparkyTheme.ball.keyName), images.load(sparkyTheme.ball.keyName),
]; ];
return (platformHelper.isMobile) ? gameAssets : gameAssets
..addAll([
images.load(androidTheme.background.keyName),
images.load(dashTheme.background.keyName),
images.load(dinoTheme.background.keyName),
images.load(sparkyTheme.background.keyName),
]);
} }
} }

@ -14,6 +14,7 @@ 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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
class PinballGame extends PinballForge2DGame class PinballGame extends PinballForge2DGame
@ -25,6 +26,7 @@ class PinballGame extends PinballForge2DGame
required GameBloc gameBloc, required GameBloc gameBloc,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballAudioPlayer audioPlayer, required PinballAudioPlayer audioPlayer,
required this.platformHelper,
}) : focusNode = FocusNode(), }) : focusNode = FocusNode(),
_gameBloc = gameBloc, _gameBloc = gameBloc,
_audioPlayer = audioPlayer, _audioPlayer = audioPlayer,
@ -57,6 +59,8 @@ class PinballGame extends PinballForge2DGame
final AppLocalizations _l10n; final AppLocalizations _l10n;
final PlatformHelper platformHelper;
final GameBloc _gameBloc; final GameBloc _gameBloc;
List<LeaderboardEntryData>? _entries; List<LeaderboardEntryData>? _entries;
@ -90,6 +94,7 @@ class PinballGame extends PinballForge2DGame
FlameProvider<LeaderboardRepository>.value(leaderboardRepository), FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<ShareRepository>.value(shareRepository), FlameProvider<ShareRepository>.value(shareRepository),
FlameProvider<AppLocalizations>.value(_l10n), FlameProvider<AppLocalizations>.value(_l10n),
FlameProvider<PlatformHelper>.value(platformHelper),
], ],
children: [ children: [
BonusNoiseBehavior(), BonusNoiseBehavior(),
@ -106,7 +111,7 @@ class PinballGame extends PinballForge2DGame
children: [ children: [
ZCanvasComponent( ZCanvasComponent(
children: [ children: [
ArcadeBackground(), if (!platformHelper.isMobile) ArcadeBackground(),
BoardBackgroundSpriteComponent(), BoardBackgroundSpriteComponent(),
Boundaries(), Boundaries(),
Backbox( Backbox(
@ -114,7 +119,7 @@ class PinballGame extends PinballForge2DGame
shareRepository: shareRepository, shareRepository: shareRepository,
entries: _entries, entries: _entries,
), ),
GoogleWord(position: Vector2(-4.45, 1.8)), GoogleGallery(),
Multipliers(), Multipliers(),
Multiballs(), Multiballs(),
SkillShot( SkillShot(
@ -197,6 +202,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
required ShareRepository shareRepository, required ShareRepository shareRepository,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballAudioPlayer audioPlayer, required PinballAudioPlayer audioPlayer,
required PlatformHelper platformHelper,
required GameBloc gameBloc, required GameBloc gameBloc,
}) : super( }) : super(
characterThemeBloc: characterThemeBloc, characterThemeBloc: characterThemeBloc,
@ -204,6 +210,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
l10n: l10n, l10n: l10n,
platformHelper: platformHelper,
gameBloc: gameBloc, gameBloc: gameBloc,
); );

@ -11,6 +11,7 @@ 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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
class PinballGamePage extends StatelessWidget { class PinballGamePage extends StatelessWidget {
@ -27,6 +28,7 @@ class PinballGamePage extends StatelessWidget {
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 shareRepository = context.read<ShareRepository>();
final platformHelper = context.read<PlatformHelper>();
final gameBloc = context.read<GameBloc>(); final gameBloc = context.read<GameBloc>();
final game = isDebugMode final game = isDebugMode
? DebugPinballGame( ? DebugPinballGame(
@ -35,6 +37,7 @@ class PinballGamePage extends StatelessWidget {
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
l10n: context.l10n, l10n: context.l10n,
platformHelper: platformHelper,
gameBloc: gameBloc, gameBloc: gameBloc,
) )
: PinballGame( : PinballGame(
@ -43,6 +46,7 @@ class PinballGamePage extends StatelessWidget {
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
l10n: context.l10n, l10n: context.l10n,
platformHelper: platformHelper,
gameBloc: gameBloc, gameBloc: gameBloc,
); );

@ -50,14 +50,11 @@ extension on Control {
} }
class HowToPlayDialog extends StatefulWidget { class HowToPlayDialog extends StatefulWidget {
HowToPlayDialog({ const HowToPlayDialog({
Key? key, Key? key,
required this.onDismissCallback, required this.onDismissCallback,
@visibleForTesting PlatformHelper? platformHelper, }) : super(key: key);
}) : platformHelper = platformHelper ?? PlatformHelper(),
super(key: key);
final PlatformHelper platformHelper;
final VoidCallback onDismissCallback; final VoidCallback onDismissCallback;
@override @override
@ -85,7 +82,7 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = widget.platformHelper.isMobile; final isMobile = context.read<PlatformHelper>().isMobile;
final l10n = context.l10n; final l10n = context.l10n;
return WillPopScope( return WillPopScope(

@ -218,7 +218,7 @@
"description": "Text to share score on Social Network", "description": "Text to share score on Social Network",
"placeholders": { "placeholders": {
"score": { "score": {
"type": "int" "type": "String"
} }
} }
} }

@ -6,6 +6,7 @@ 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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
void main() { void main() {
@ -15,6 +16,7 @@ void main() {
ShareRepository(appUrl: ShareRepository.pinballGameUrl); ShareRepository(appUrl: ShareRepository.pinballGameUrl);
final authenticationRepository = AuthenticationRepository(firebaseAuth); final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudioPlayer = PinballAudioPlayer(); final pinballAudioPlayer = PinballAudioPlayer();
final platformHelper = PlatformHelper();
unawaited( unawaited(
Firebase.initializeApp().then( Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(), (_) => authenticationRepository.authenticateAnonymously(),
@ -25,6 +27,7 @@ void main() {
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
pinballAudioPlayer: pinballAudioPlayer, pinballAudioPlayer: pinballAudioPlayer,
platformHelper: platformHelper,
); );
}); });
} }

@ -204,7 +204,7 @@ class _MadeWithFlutterAndFirebase extends StatelessWidget {
abstract class _MoreInformationUrl { abstract class _MoreInformationUrl {
static const flutterWebsite = 'https://flutter.dev'; static const flutterWebsite = 'https://flutter.dev';
static const firebaseWebsite = 'https://firebase.google.com'; static const firebaseWebsite = 'https://firebase.google.com';
static const openSourceCode = 'https://github.com/VGVentures/pinball'; static const openSourceCode = 'https://github.com/flutter/pinball';
static const googleIOEvent = 'https://events.google.com/io/'; static const googleIOEvent = 'https://events.google.com/io/';
static const flutterGamesWebsite = 'http://flutter.dev/games'; static const flutterGamesWebsite = 'http://flutter.dev/games';
static const howItsMadeArticle = static const howItsMadeArticle =

@ -59,7 +59,7 @@ typedef CreateAudioPool = Future<AudioPool> Function(
typedef PlaySingleAudio = Future<void> Function(String); typedef PlaySingleAudio = Future<void> Function(String);
/// Defines the contract for looping a single audio. /// Defines the contract for looping a single audio.
typedef LoopSingleAudio = Future<void> Function(String); typedef LoopSingleAudio = Future<void> Function(String, {double volume});
/// Defines the contract for pre fetching an audio. /// Defines the contract for pre fetching an audio.
typedef PreCacheSingleAudio = Future<void> Function(String); typedef PreCacheSingleAudio = Future<void> Function(String);
@ -101,18 +101,44 @@ class _LoopAudio extends _Audio {
required this.preCacheSingleAudio, required this.preCacheSingleAudio,
required this.loopSingleAudio, required this.loopSingleAudio,
required this.path, required this.path,
this.volume,
}); });
final PreCacheSingleAudio preCacheSingleAudio; final PreCacheSingleAudio preCacheSingleAudio;
final LoopSingleAudio loopSingleAudio; final LoopSingleAudio loopSingleAudio;
final String path; final String path;
final double? volume;
@override @override
Future<void> load() => preCacheSingleAudio(prefixFile(path)); Future<void> load() => preCacheSingleAudio(prefixFile(path));
@override @override
void play() { void play() {
loopSingleAudio(prefixFile(path)); loopSingleAudio(prefixFile(path), volume: volume ?? 1);
}
}
class _SingleLoopAudio extends _LoopAudio {
_SingleLoopAudio({
required PreCacheSingleAudio preCacheSingleAudio,
required LoopSingleAudio loopSingleAudio,
required String path,
double? volume,
}) : super(
preCacheSingleAudio: preCacheSingleAudio,
loopSingleAudio: loopSingleAudio,
path: path,
volume: volume,
);
bool _playing = false;
@override
void play() {
if (!_playing) {
super.play();
_playing = true;
}
} }
} }
@ -270,10 +296,11 @@ class PinballAudioPlayer {
path: Assets.sfx.cowMoo, path: Assets.sfx.cowMoo,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
), ),
PinballAudio.backgroundMusic: _LoopAudio( PinballAudio.backgroundMusic: _SingleLoopAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
loopSingleAudio: _loopSingleAudio, loopSingleAudio: _loopSingleAudio,
path: Assets.music.background, path: Assets.music.background,
volume: .6,
), ),
}; };
} }

@ -33,7 +33,7 @@ class _MockPlaySingleAudio extends Mock {
} }
class _MockLoopSingleAudio extends Mock { class _MockLoopSingleAudio extends Mock {
Future<void> onCall(String url); Future<void> onCall(String url, {double volume});
} }
abstract class _PreCacheSingleAudio { abstract class _PreCacheSingleAudio {
@ -77,7 +77,8 @@ void main() {
when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {}); when(() => playSingleAudio.onCall(any())).thenAnswer((_) async {});
loopSingleAudio = _MockLoopSingleAudio(); loopSingleAudio = _MockLoopSingleAudio();
when(() => loopSingleAudio.onCall(any())).thenAnswer((_) async {}); when(() => loopSingleAudio.onCall(any(), volume: any(named: 'volume')))
.thenAnswer((_) async {});
preCacheSingleAudio = _MockPreCacheSingleAudio(); preCacheSingleAudio = _MockPreCacheSingleAudio();
when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {}); when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {});
@ -443,8 +444,24 @@ void main() {
audioPlayer.play(PinballAudio.backgroundMusic); audioPlayer.play(PinballAudio.backgroundMusic);
verify( verify(
() => loopSingleAudio () => loopSingleAudio.onCall(
.onCall('packages/pinball_audio/${Assets.music.background}'), 'packages/pinball_audio/${Assets.music.background}',
volume: .6,
),
).called(1);
});
test('plays only once', () async {
await Future.wait(audioPlayer.load());
audioPlayer
..play(PinballAudio.backgroundMusic)
..play(PinballAudio.backgroundMusic);
verify(
() => loopSingleAudio.onCall(
'packages/pinball_audio/${Assets.music.background}',
volume: .6,
),
).called(1); ).called(1);
}); });
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -23,12 +23,17 @@ class $AssetsImagesGen {
$AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDashGen get dash => const $AssetsImagesDashGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
$AssetsImagesDisplayArrowsGen get displayArrows =>
const $AssetsImagesDisplayArrowsGen();
/// File path: assets/images/error_background.png /// File path: assets/images/error_background.png
AssetGenImage get errorBackground => AssetGenImage get errorBackground =>
const AssetGenImage('assets/images/error_background.png'); const AssetGenImage('assets/images/error_background.png');
$AssetsImagesFlapperGen get flapper => const $AssetsImagesFlapperGen(); $AssetsImagesFlapperGen get flapper => const $AssetsImagesFlapperGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
$AssetsImagesGoogleRolloverGen get googleRollover =>
const $AssetsImagesGoogleRolloverGen();
$AssetsImagesGoogleWordGen get googleWord => $AssetsImagesGoogleWordGen get googleWord =>
const $AssetsImagesGoogleWordGen(); const $AssetsImagesGoogleWordGen();
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
@ -140,6 +145,15 @@ class $AssetsImagesDinoGen {
const AssetGenImage('assets/images/dino/top_wall_tunnel.png'); const AssetGenImage('assets/images/dino/top_wall_tunnel.png');
} }
class $AssetsImagesDisplayArrowsGen {
const $AssetsImagesDisplayArrowsGen();
AssetGenImage get arrowLeft =>
const AssetGenImage('assets/images/display_arrows/arrow_left.png');
AssetGenImage get arrowRight =>
const AssetGenImage('assets/images/display_arrows/arrow_right.png');
}
class $AssetsImagesFlapperGen { class $AssetsImagesFlapperGen {
const $AssetsImagesFlapperGen(); const $AssetsImagesFlapperGen();
@ -168,6 +182,15 @@ class $AssetsImagesFlipperGen {
const AssetGenImage('assets/images/flipper/right.png'); const AssetGenImage('assets/images/flipper/right.png');
} }
class $AssetsImagesGoogleRolloverGen {
const $AssetsImagesGoogleRolloverGen();
$AssetsImagesGoogleRolloverLeftGen get left =>
const $AssetsImagesGoogleRolloverLeftGen();
$AssetsImagesGoogleRolloverRightGen get right =>
const $AssetsImagesGoogleRolloverRightGen();
}
class $AssetsImagesGoogleWordGen { class $AssetsImagesGoogleWordGen {
const $AssetsImagesGoogleWordGen(); const $AssetsImagesGoogleWordGen();
@ -422,6 +445,24 @@ class $AssetsImagesDinoAnimatronicGen {
const AssetGenImage('assets/images/dino/animatronic/mouth.png'); const AssetGenImage('assets/images/dino/animatronic/mouth.png');
} }
class $AssetsImagesGoogleRolloverLeftGen {
const $AssetsImagesGoogleRolloverLeftGen();
AssetGenImage get decal =>
const AssetGenImage('assets/images/google_rollover/left/decal.png');
AssetGenImage get pin =>
const AssetGenImage('assets/images/google_rollover/left/pin.png');
}
class $AssetsImagesGoogleRolloverRightGen {
const $AssetsImagesGoogleRolloverRightGen();
AssetGenImage get decal =>
const AssetGenImage('assets/images/google_rollover/right/decal.png');
AssetGenImage get pin =>
const AssetGenImage('assets/images/google_rollover/right/pin.png');
}
class $AssetsImagesGoogleWordLetter1Gen { class $AssetsImagesGoogleWordLetter1Gen {
const $AssetsImagesGoogleWordLetter1Gen(); const $AssetsImagesGoogleWordLetter1Gen();

@ -0,0 +1,49 @@
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// enum with the available directions for an [ArrowIcon].
enum ArrowIconDirection {
/// Left.
left,
/// Right.
right,
}
/// {@template arrow_icon}
/// A [SpriteComponent] that renders a simple arrow icon.
/// {@endtemplate}
class ArrowIcon extends SpriteComponent with Tappable, HasGameRef {
/// {@macro arrow_icon}
ArrowIcon({
required Vector2 position,
required this.direction,
required this.onTap,
}) : super(position: position);
final ArrowIconDirection direction;
final VoidCallback onTap;
@override
Future<void> onLoad() async {
anchor = Anchor.center;
final sprite = Sprite(
gameRef.images.fromCache(
direction == ArrowIconDirection.left
? Assets.images.displayArrows.arrowLeft.keyName
: Assets.images.displayArrows.arrowRight.keyName,
),
);
size = sprite.originalSize / 20;
this.sprite = sprite;
}
@override
bool onTapUp(TapUpInfo info) {
onTap();
return true;
}
}

@ -2,6 +2,7 @@ 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 'arcade_background/arcade_background.dart';
export 'arrow_icon.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';
@ -16,7 +17,9 @@ export 'dino_walls.dart';
export 'error_component.dart'; export 'error_component.dart';
export 'flapper/flapper.dart'; export 'flapper/flapper.dart';
export 'flipper/flipper.dart'; export 'flipper/flipper.dart';
export 'google_letter/google_letter.dart'; export 'google_letter.dart';
export 'google_rollover/google_rollover.dart';
export 'google_word/google_word.dart';
export 'initial_position.dart'; export 'initial_position.dart';
export 'joint_anchor.dart'; export 'joint_anchor.dart';
export 'kicker/kicker.dart'; export 'kicker/kicker.dart';

@ -0,0 +1,88 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
enum GoogleLetterSpriteState {
lit,
dimmed,
}
/// {@template google_letter}
/// Circular decal that represents a letter in "GOOGLE" for a given index.
/// {@endtemplate}
class GoogleLetter extends SpriteGroupComponent<GoogleLetterSpriteState>
with HasGameRef, FlameBlocListenable<GoogleWordCubit, GoogleWordState> {
/// {@macro google_letter}
GoogleLetter(int index)
: _litAssetPath = _spritePaths[index][GoogleLetterSpriteState.lit]!,
_dimmedAssetPath = _spritePaths[index][GoogleLetterSpriteState.dimmed]!,
_index = index,
super(anchor: Anchor.center);
final String _litAssetPath;
final String _dimmedAssetPath;
final int _index;
@override
bool listenWhen(GoogleWordState previousState, GoogleWordState newState) {
return previousState.letterSpriteStates[_index] !=
newState.letterSpriteStates[_index];
}
@override
void onNewState(GoogleWordState state) =>
current = state.letterSpriteStates[_index];
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = {
GoogleLetterSpriteState.lit: Sprite(
gameRef.images.fromCache(_litAssetPath),
),
GoogleLetterSpriteState.dimmed: Sprite(
gameRef.images.fromCache(_dimmedAssetPath),
),
};
this.sprites = sprites;
current = readBloc<GoogleWordCubit, GoogleWordState>()
.state
.letterSpriteStates[_index];
size = sprites[current]!.originalSize / 10;
}
}
final _spritePaths = <Map<GoogleLetterSpriteState, String>>[
{
GoogleLetterSpriteState.lit: Assets.images.googleWord.letter1.lit.keyName,
GoogleLetterSpriteState.dimmed:
Assets.images.googleWord.letter1.dimmed.keyName,
},
{
GoogleLetterSpriteState.lit: Assets.images.googleWord.letter2.lit.keyName,
GoogleLetterSpriteState.dimmed:
Assets.images.googleWord.letter2.dimmed.keyName,
},
{
GoogleLetterSpriteState.lit: Assets.images.googleWord.letter3.lit.keyName,
GoogleLetterSpriteState.dimmed:
Assets.images.googleWord.letter3.dimmed.keyName,
},
{
GoogleLetterSpriteState.lit: Assets.images.googleWord.letter4.lit.keyName,
GoogleLetterSpriteState.dimmed:
Assets.images.googleWord.letter4.dimmed.keyName,
},
{
GoogleLetterSpriteState.lit: Assets.images.googleWord.letter5.lit.keyName,
GoogleLetterSpriteState.dimmed:
Assets.images.googleWord.letter5.dimmed.keyName,
},
{
GoogleLetterSpriteState.lit: Assets.images.googleWord.letter6.lit.keyName,
GoogleLetterSpriteState.dimmed:
Assets.images.googleWord.letter6.dimmed.keyName,
},
];

@ -1,15 +0,0 @@
import 'package:bloc/bloc.dart';
part 'google_letter_state.dart';
class GoogleLetterCubit extends Cubit<GoogleLetterState> {
GoogleLetterCubit() : super(GoogleLetterState.dimmed);
void onBallContacted() {
emit(GoogleLetterState.lit);
}
void onReset() {
emit(GoogleLetterState.dimmed);
}
}

@ -1,6 +0,0 @@
part of 'google_letter_cubit.dart';
enum GoogleLetterState {
lit,
dimmed,
}

@ -1,133 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/google_letter/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/google_letter_cubit.dart';
final _spritePaths = <Map<GoogleLetterState, String>>[
{
GoogleLetterState.lit: Assets.images.googleWord.letter1.lit.keyName,
GoogleLetterState.dimmed: Assets.images.googleWord.letter1.dimmed.keyName,
},
{
GoogleLetterState.lit: Assets.images.googleWord.letter2.lit.keyName,
GoogleLetterState.dimmed: Assets.images.googleWord.letter2.dimmed.keyName,
},
{
GoogleLetterState.lit: Assets.images.googleWord.letter3.lit.keyName,
GoogleLetterState.dimmed: Assets.images.googleWord.letter3.dimmed.keyName,
},
{
GoogleLetterState.lit: Assets.images.googleWord.letter4.lit.keyName,
GoogleLetterState.dimmed: Assets.images.googleWord.letter4.dimmed.keyName,
},
{
GoogleLetterState.lit: Assets.images.googleWord.letter5.lit.keyName,
GoogleLetterState.dimmed: Assets.images.googleWord.letter5.dimmed.keyName,
},
{
GoogleLetterState.lit: Assets.images.googleWord.letter6.lit.keyName,
GoogleLetterState.dimmed: Assets.images.googleWord.letter6.dimmed.keyName,
},
];
/// {@template google_letter}
/// Circular sensor that represents a letter in "GOOGLE" for a given index.
/// {@endtemplate}
class GoogleLetter extends BodyComponent with InitialPosition {
/// {@macro google_letter}
GoogleLetter(
int index, {
Iterable<Component>? children,
}) : this._(
index,
bloc: GoogleLetterCubit(),
children: children,
);
GoogleLetter._(
int index, {
required this.bloc,
Iterable<Component>? children,
}) : super(
children: [
_GoogleLetterSpriteGroupComponent(
litAssetPath: _spritePaths[index][GoogleLetterState.lit]!,
dimmedAssetPath: _spritePaths[index][GoogleLetterState.dimmed]!,
current: bloc.state,
),
GoogleLetterBallContactBehavior(),
...?children,
],
renderBody: false,
);
/// Creates a [GoogleLetter] without any children.
///
/// This can be used for testing [GoogleLetter]'s behaviors in isolation.
@visibleForTesting
GoogleLetter.test({
required this.bloc,
});
final GoogleLetterCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override
Body createBody() {
final shape = CircleShape()..radius = 1.85;
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _GoogleLetterSpriteGroupComponent
extends SpriteGroupComponent<GoogleLetterState>
with HasGameRef, ParentIsA<GoogleLetter> {
_GoogleLetterSpriteGroupComponent({
required String litAssetPath,
required String dimmedAssetPath,
required GoogleLetterState current,
}) : _litAssetPath = litAssetPath,
_dimmedAssetPath = dimmedAssetPath,
super(
anchor: Anchor.center,
current: current,
);
final String _litAssetPath;
final String _dimmedAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state);
final sprites = {
GoogleLetterState.lit: Sprite(
gameRef.images.fromCache(_litAssetPath),
),
GoogleLetterState.dimmed: Sprite(
gameRef.images.fromCache(_dimmedAssetPath),
),
};
this.sprites = sprites;
size = sprites[current]!.originalSize / 10;
}
}

@ -1,12 +1,15 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.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';
class GoogleLetterBallContactBehavior extends ContactBehavior<GoogleLetter> { class GoogleRolloverBallContactBehavior
extends ContactBehavior<GoogleRollover> {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);
if (other is! Ball) return; if (other is! Ball) return;
parent.bloc.onBallContacted(); readBloc<GoogleWordCubit, GoogleWordState>().onRolloverContacted();
parent.firstChild<SpriteAnimationComponent>()?.playing = true;
} }
} }

@ -0,0 +1,113 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/google_rollover/behaviors/behaviors.dart';
/// {@template google_rollover}
/// Rollover that lights up [GoogleLetter]s.
/// {@endtemplate}
class GoogleRollover extends BodyComponent {
/// {@macro google_rollover}
GoogleRollover({
required BoardSide side,
Iterable<Component>? children,
}) : _side = side,
super(
renderBody: false,
children: [
GoogleRolloverBallContactBehavior(),
_RolloverDecalSpriteComponent(side: side),
_PinSpriteAnimationComponent(side: side),
...?children,
],
);
final BoardSide _side;
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
0.1,
3.4,
Vector2(_side.isLeft ? -14.8 : 5.9, -11),
0.19 * _side.direction,
);
final fixtureDef = FixtureDef(shape, isSensor: true);
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}
class _RolloverDecalSpriteComponent extends SpriteComponent with HasGameRef {
_RolloverDecalSpriteComponent({required BoardSide side})
: _side = side,
super(
anchor: Anchor.center,
position: Vector2(side.isLeft ? -14.8 : 5.9, -11),
angle: 0.18 * side.direction,
);
final BoardSide _side;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
(_side.isLeft)
? Assets.images.googleRollover.left.decal.keyName
: Assets.images.googleRollover.right.decal.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
class _PinSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
_PinSpriteAnimationComponent({required BoardSide side})
: _side = side,
super(
anchor: Anchor.center,
position: Vector2(side.isLeft ? -14.9 : 5.95, -11),
angle: 0,
playing: false,
);
final BoardSide _side;
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
_side.isLeft
? Assets.images.googleRollover.left.pin.keyName
: Assets.images.googleRollover.right.pin.keyName,
);
const amountPerRow = 3;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
loop: false,
),
)..onComplete = () {
animation?.reset();
playing = false;
};
}
}

@ -0,0 +1,30 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball_components/pinball_components.dart';
part 'google_word_state.dart';
class GoogleWordCubit extends Cubit<GoogleWordState> {
GoogleWordCubit() : super(GoogleWordState.initial());
static const _lettersInGoogle = 6;
int _lastLitLetter = 0;
void onRolloverContacted() {
final spriteStatesMap = {...state.letterSpriteStates};
if (_lastLitLetter < _lettersInGoogle) {
spriteStatesMap.update(
_lastLitLetter,
(_) => GoogleLetterSpriteState.lit,
);
emit(GoogleWordState(letterSpriteStates: spriteStatesMap));
_lastLitLetter++;
}
}
void onBonusAwarded() {
emit(GoogleWordState.initial());
_lastLitLetter = 0;
}
}

@ -0,0 +1,17 @@
part of 'google_word_cubit.dart';
class GoogleWordState extends Equatable {
const GoogleWordState({required this.letterSpriteStates});
GoogleWordState.initial()
: this(
letterSpriteStates: {
for (var i = 0; i <= 5; i++) i: GoogleLetterSpriteState.dimmed
},
);
final Map<int, GoogleLetterSpriteState> letterSpriteStates;
@override
List<Object> get props => [...letterSpriteStates.values];
}

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
export 'cubit/google_word_cubit.dart';
/// {@template google_word}
/// Loads all [GoogleLetter]s to compose a [GoogleWord].
/// {@endtemplate}
class GoogleWord extends PositionComponent {
/// {@macro google_word}
GoogleWord({
required Vector2 position,
}) : super(
position: position,
children: [
GoogleLetter(0)..position = Vector2(-13.1, 1.72),
GoogleLetter(1)..position = Vector2(-8.33, -0.75),
GoogleLetter(2)..position = Vector2(-2.88, -1.85),
GoogleLetter(3)..position = Vector2(2.88, -1.85),
GoogleLetter(4)..position = Vector2(8.33, -0.75),
GoogleLetter(5)..position = Vector2(13.1, 1.72),
],
);
}

@ -9,9 +9,9 @@ import 'package:pinball_flame/pinball_flame.dart';
class MultiballBlinkingBehavior extends TimerComponent class MultiballBlinkingBehavior extends TimerComponent
with ParentIsA<Multiball> { with ParentIsA<Multiball> {
/// {@macro multiball_blinking_behavior} /// {@macro multiball_blinking_behavior}
MultiballBlinkingBehavior() : super(period: 0.1); MultiballBlinkingBehavior() : super(period: 0.18);
final _maxBlinks = 10; final _maxBlinks = 28;
int _blinksCounter = 0; int _blinksCounter = 0;

@ -81,6 +81,8 @@ flutter:
- assets/images/google_word/letter4/ - assets/images/google_word/letter4/
- assets/images/google_word/letter5/ - assets/images/google_word/letter5/
- assets/images/google_word/letter6/ - assets/images/google_word/letter6/
- assets/images/google_rollover/left/
- assets/images/google_rollover/right/
- assets/images/signpost/ - assets/images/signpost/
- assets/images/multiball/ - assets/images/multiball/
- assets/images/multiplier/x2/ - assets/images/multiplier/x2/
@ -93,6 +95,7 @@ flutter:
- assets/images/backbox/button/ - assets/images/backbox/button/
- assets/images/flapper/ - assets/images/flapper/
- assets/images/skill_shot/ - assets/images/skill_shot/
- assets/images/display_arrows/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -3,7 +3,7 @@ import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
const _path = const _path =
'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/'; 'https://github.com/flutter/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/';
extension StoryAddGame on Story { extension StoryAddGame on Story {
void addGame({ void addGame({

@ -21,6 +21,7 @@ void main() {
addScoreStories(dashbook); addScoreStories(dashbook);
addMultiballStories(dashbook); addMultiballStories(dashbook);
addMultipliersStories(dashbook); addMultipliersStories(dashbook);
addArrowIconStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -0,0 +1,37 @@
import 'package:flame/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/games.dart';
class ArrowIconGame extends AssetsGame with HasTappables {
ArrowIconGame()
: super(
imagesFileNames: [
Assets.images.displayArrows.arrowLeft.keyName,
Assets.images.displayArrows.arrowRight.keyName,
],
);
static const description = 'Shows how ArrowIcons are rendered.';
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(
ArrowIcon(
position: Vector2.zero(),
direction: ArrowIconDirection.left,
onTap: () {},
),
);
await add(
ArrowIcon(
position: Vector2(0, 20),
direction: ArrowIconDirection.right,
onTap: () {},
),
);
}
}

@ -0,0 +1,11 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/arrow_icon/arrow_icon_game.dart';
void addArrowIconStories(Dashbook dashbook) {
dashbook.storiesOf('ArrowIcon').addGame(
title: 'Basic',
description: ArrowIconGame.description,
gameBuilder: (context) => ArrowIconGame(),
);
}

@ -1,4 +1,5 @@
export 'android_acres/stories.dart'; export 'android_acres/stories.dart';
export 'arrow_icon/stories.dart';
export 'ball/stories.dart'; export 'ball/stories.dart';
export 'bottom_group/stories.dart'; export 'bottom_group/stories.dart';
export 'boundaries/stories.dart'; export 'boundaries/stories.dart';

@ -1,3 +1,4 @@
import 'package:flame/game.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
@ -20,3 +21,7 @@ class TestGame extends Forge2DGame {
class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents { class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents {
KeyboardTestGame([List<String>? assets]) : super(assets); KeyboardTestGame([List<String>? assets]) : super(assets);
} }
class TappablesTestGame extends TestGame with HasTappables {
TappablesTestGame([List<String>? assets]) : super(assets);
}

@ -0,0 +1,96 @@
// ignore_for_file: cascade_invocations, one_member_abstracts
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
abstract class _VoidCallbackStubBase {
void onCall();
}
class _VoidCallbackStub extends Mock implements _VoidCallbackStubBase {}
void main() {
group('ArrowIcon', () {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.displayArrows.arrowLeft.keyName,
Assets.images.displayArrows.arrowRight.keyName,
];
final flameTester = FlameTester(() => TappablesTestGame(assets));
flameTester.testGameWidget(
'is tappable',
setUp: (game, tester) async {
final stub = _VoidCallbackStub();
await game.images.loadAll(assets);
await game.ensureAdd(
ArrowIcon(
position: Vector2.zero(),
direction: ArrowIconDirection.left,
onTap: stub.onCall,
),
);
await tester.pump();
await tester.tapAt(Offset.zero);
await tester.pump();
},
verify: (game, tester) async {
final icon = game.descendants().whereType<ArrowIcon>().single;
verify(icon.onTap).called(1);
},
);
group('left', () {
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
game.camera.followVector2(Vector2.zero());
await game.add(
ArrowIcon(
position: Vector2.zero(),
direction: ArrowIconDirection.left,
onTap: () {},
),
);
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/arrow_icon_left.png'),
);
},
);
});
group('right', () {
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
game.camera.followVector2(Vector2.zero());
await game.add(
ArrowIcon(
position: Vector2.zero(),
direction: ArrowIconDirection.right,
onTap: () {},
),
);
await tester.pump();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/arrow_icon_right.png'),
);
},
);
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@ -1,55 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.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_components/pinball_components.dart';
import 'package:pinball_components/src/components/google_letter/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'GoogleLetterBallContactBehavior',
() {
test('can be instantiated', () {
expect(
GoogleLetterBallContactBehavior(),
isA<GoogleLetterBallContactBehavior>(),
);
});
flameTester.test(
'beginContact emits onBallContacted when contacts with a ball',
(game) async {
final behavior = GoogleLetterBallContactBehavior();
final bloc = _MockGoogleLetterCubit();
whenListen(
bloc,
const Stream<GoogleLetterState>.empty(),
initialState: GoogleLetterState.lit,
);
final googleLetter = GoogleLetter.test(bloc: bloc);
await googleLetter.add(behavior);
await game.ensureAdd(googleLetter);
behavior.beginContact(_MockBall(), _MockContact());
verify(googleLetter.bloc.onBallContacted).called(1);
},
);
},
);
}

@ -1,24 +0,0 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'GoogleLetterCubit',
() {
blocTest<GoogleLetterCubit, GoogleLetterState>(
'onBallContacted emits active',
build: GoogleLetterCubit.new,
act: (bloc) => bloc.onBallContacted(),
expect: () => [GoogleLetterState.lit],
);
blocTest<GoogleLetterCubit, GoogleLetterState>(
'onReset emits inactive',
build: GoogleLetterCubit.new,
act: (bloc) => bloc.onReset(),
expect: () => [GoogleLetterState.dimmed],
);
},
);
}

@ -1,145 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/google_letter/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
class _MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.googleWord.letter1.lit.keyName,
Assets.images.googleWord.letter1.dimmed.keyName,
Assets.images.googleWord.letter2.lit.keyName,
Assets.images.googleWord.letter2.dimmed.keyName,
Assets.images.googleWord.letter3.lit.keyName,
Assets.images.googleWord.letter3.dimmed.keyName,
Assets.images.googleWord.letter4.lit.keyName,
Assets.images.googleWord.letter4.dimmed.keyName,
Assets.images.googleWord.letter5.lit.keyName,
Assets.images.googleWord.letter5.dimmed.keyName,
Assets.images.googleWord.letter6.lit.keyName,
Assets.images.googleWord.letter6.dimmed.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('Google Letter', () {
flameTester.test(
'0th loads correctly',
(game) async {
final googleLetter = GoogleLetter(0);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'1st loads correctly',
(game) async {
final googleLetter = GoogleLetter(1);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'2nd loads correctly',
(game) async {
final googleLetter = GoogleLetter(2);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'3d loads correctly',
(game) async {
final googleLetter = GoogleLetter(3);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'4th loads correctly',
(game) async {
final googleLetter = GoogleLetter(4);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
flameTester.test(
'5th loads correctly',
(game) async {
final googleLetter = GoogleLetter(5);
await game.ready();
await game.ensureAdd(googleLetter);
expect(game.contains(googleLetter), isTrue);
},
);
test('throws error when index out of range', () {
expect(() => GoogleLetter(-1), throwsA(isA<RangeError>()));
expect(() => GoogleLetter(6), throwsA(isA<RangeError>()));
});
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockGoogleLetterCubit();
whenListen(
bloc,
const Stream<GoogleLetterState>.empty(),
initialState: GoogleLetterState.lit,
);
when(bloc.close).thenAnswer((_) async {});
final googleLetter = GoogleLetter.test(bloc: bloc);
await game.ensureAdd(googleLetter);
game.remove(googleLetter);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final googleLetter = GoogleLetter(
1,
children: [component],
);
await game.ensureAdd(googleLetter);
expect(googleLetter.children, contains(component));
});
flameTester.test('a GoogleLetterBallContactBehavior', (game) async {
final googleLetter = GoogleLetter(0);
await game.ensureAdd(googleLetter);
expect(
googleLetter.children
.whereType<GoogleLetterBallContactBehavior>()
.single,
isNotNull,
);
});
});
});
}

@ -0,0 +1,159 @@
// ignore_for_file: cascade_invocations
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_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.googleWord.letter1.lit.keyName,
Assets.images.googleWord.letter1.dimmed.keyName,
Assets.images.googleWord.letter2.lit.keyName,
Assets.images.googleWord.letter2.dimmed.keyName,
Assets.images.googleWord.letter3.lit.keyName,
Assets.images.googleWord.letter3.dimmed.keyName,
Assets.images.googleWord.letter4.lit.keyName,
Assets.images.googleWord.letter4.dimmed.keyName,
Assets.images.googleWord.letter5.lit.keyName,
Assets.images.googleWord.letter5.dimmed.keyName,
Assets.images.googleWord.letter6.lit.keyName,
Assets.images.googleWord.letter6.dimmed.keyName,
]);
}
Future<void> pump(GoogleLetter child) async {
await ensureAdd(
FlameBlocProvider<GoogleWordCubit, GoogleWordState>.value(
value: GoogleWordCubit(),
children: [child],
),
);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('Google Letter', () {
test('can be instantiated', () {
expect(GoogleLetter(0), isA<GoogleLetter>());
});
flameTester.test(
'0th loads correctly',
(game) async {
final googleLetter = GoogleLetter(0);
await game.pump(googleLetter);
expect(game.descendants().contains(googleLetter), isTrue);
},
);
flameTester.test(
'1st loads correctly',
(game) async {
final googleLetter = GoogleLetter(1);
await game.pump(googleLetter);
expect(game.descendants().contains(googleLetter), isTrue);
},
);
flameTester.test(
'2nd loads correctly',
(game) async {
final googleLetter = GoogleLetter(2);
await game.pump(googleLetter);
expect(game.descendants().contains(googleLetter), isTrue);
},
);
flameTester.test(
'3d loads correctly',
(game) async {
final googleLetter = GoogleLetter(3);
await game.pump(googleLetter);
expect(game.descendants().contains(googleLetter), isTrue);
},
);
flameTester.test(
'4th loads correctly',
(game) async {
final googleLetter = GoogleLetter(4);
await game.pump(googleLetter);
expect(game.descendants().contains(googleLetter), isTrue);
},
);
flameTester.test(
'5th loads correctly',
(game) async {
final googleLetter = GoogleLetter(5);
await game.pump(googleLetter);
expect(game.descendants().contains(googleLetter), isTrue);
},
);
test('throws error when index out of range', () {
expect(() => GoogleLetter(-1), throwsA(isA<RangeError>()));
expect(() => GoogleLetter(6), throwsA(isA<RangeError>()));
});
group('sprite', () {
const firstLetterLitState = GoogleWordState(
letterSpriteStates: {
0: GoogleLetterSpriteState.lit,
1: GoogleLetterSpriteState.dimmed,
2: GoogleLetterSpriteState.dimmed,
3: GoogleLetterSpriteState.dimmed,
4: GoogleLetterSpriteState.dimmed,
5: GoogleLetterSpriteState.dimmed,
},
);
flameTester.test(
"listens when its index's state changes",
(game) async {
final googleLetter = GoogleLetter(0);
await game.pump(googleLetter);
expect(
googleLetter.listenWhen(
GoogleWordState.initial(),
firstLetterLitState,
),
isTrue,
);
},
);
flameTester.test(
'changes current sprite onNewState',
(game) async {
final googleLetter = GoogleLetter(0);
await game.pump(googleLetter);
final originalSprite = googleLetter.current;
googleLetter.onNewState(firstLetterLitState);
await game.ready();
final newSprite = googleLetter.current;
expect(newSprite != originalSprite, isTrue);
},
);
});
});
}

@ -0,0 +1,81 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.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_components/pinball_components.dart';
import 'package:pinball_components/src/components/google_rollover/behaviors/behaviors.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.googleRollover.left.decal.keyName,
Assets.images.googleRollover.left.pin.keyName,
]);
}
Future<void> pump(
GoogleRollover child, {
GoogleWordCubit? bloc,
}) async {
// Not needed once https://github.com/flame-engine/flame/issues/1607
// is fixed
await onLoad();
await ensureAdd(
FlameBlocProvider<GoogleWordCubit, GoogleWordState>.value(
value: bloc ?? GoogleWordCubit(),
children: [child],
),
);
}
}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group(
'GoogleRolloverBallContactBehavior',
() {
test('can be instantiated', () {
expect(
GoogleRolloverBallContactBehavior(),
isA<GoogleRolloverBallContactBehavior>(),
);
});
flameTester.testGameWidget(
'beginContact animates pin and calls onRolloverContacted '
'when contacts with a ball',
setUp: (game, tester) async {
final behavior = GoogleRolloverBallContactBehavior();
final bloc = _MockGoogleWordCubit();
final googleRollover = GoogleRollover(side: BoardSide.left);
await googleRollover.add(behavior);
await game.pump(googleRollover, bloc: bloc);
behavior.beginContact(_MockBall(), _MockContact());
await tester.pump();
expect(
googleRollover.firstChild<SpriteAnimationComponent>()!.playing,
isTrue,
);
verify(bloc.onRolloverContacted).called(1);
},
);
},
);
}

@ -0,0 +1,82 @@
// 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_components/src/components/google_rollover/behaviors/behaviors.dart';
import '../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.googleRollover.left.decal.keyName,
Assets.images.googleRollover.left.pin.keyName,
Assets.images.googleRollover.right.decal.keyName,
Assets.images.googleRollover.right.pin.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('GoogleRollover', () {
test('can be instantiated', () {
expect(
GoogleRollover(side: BoardSide.left),
isA<GoogleRollover>(),
);
});
flameTester.test('left loads correctly', (game) async {
final googleRollover = GoogleRollover(side: BoardSide.left);
await game.ensureAdd(googleRollover);
expect(game.contains(googleRollover), isTrue);
});
flameTester.test('right loads correctly', (game) async {
final googleRollover = GoogleRollover(side: BoardSide.right);
await game.ensureAdd(googleRollover);
expect(game.contains(googleRollover), isTrue);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final googleRollover = GoogleRollover(
side: BoardSide.left,
children: [component],
);
await game.ensureAdd(googleRollover);
expect(googleRollover.children, contains(component));
});
flameTester.test('a GoogleRolloverBallContactBehavior', (game) async {
final googleRollover = GoogleRollover(side: BoardSide.left);
await game.ensureAdd(googleRollover);
expect(
googleRollover.children
.whereType<GoogleRolloverBallContactBehavior>()
.single,
isNotNull,
);
});
});
flameTester.test(
'pin stops animating after animation completes',
(game) async {
final googleRollover = GoogleRollover(side: BoardSide.left);
await game.ensureAdd(googleRollover);
final pinSpriteAnimationComponent =
googleRollover.firstChild<SpriteAnimationComponent>()!;
pinSpriteAnimationComponent.playing = true;
game.update(
pinSpriteAnimationComponent.animation!.totalDuration() + 0.1,
);
expect(pinSpriteAnimationComponent.playing, isFalse);
},
);
});
}

@ -0,0 +1,35 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group(
'GoogleWordCubit',
() {
blocTest<GoogleWordCubit, GoogleWordState>(
'onRolloverContacted emits first letter lit',
build: GoogleWordCubit.new,
act: (bloc) => bloc.onRolloverContacted(),
expect: () => [
const GoogleWordState(
letterSpriteStates: {
0: GoogleLetterSpriteState.lit,
1: GoogleLetterSpriteState.dimmed,
2: GoogleLetterSpriteState.dimmed,
3: GoogleLetterSpriteState.dimmed,
4: GoogleLetterSpriteState.dimmed,
5: GoogleLetterSpriteState.dimmed,
},
),
],
);
blocTest<GoogleWordCubit, GoogleWordState>(
'onBonusAwarded emits initial state',
build: GoogleWordCubit.new,
act: (bloc) => bloc.onBonusAwarded(),
expect: () => [GoogleWordState.initial()],
);
},
);
}

@ -0,0 +1,58 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('GoogleWordState', () {
test('supports value equality', () {
expect(
GoogleWordState(
letterSpriteStates: const {
0: GoogleLetterSpriteState.dimmed,
1: GoogleLetterSpriteState.dimmed,
2: GoogleLetterSpriteState.dimmed,
3: GoogleLetterSpriteState.dimmed,
4: GoogleLetterSpriteState.dimmed,
5: GoogleLetterSpriteState.dimmed,
},
),
equals(
GoogleWordState(
letterSpriteStates: const {
0: GoogleLetterSpriteState.dimmed,
1: GoogleLetterSpriteState.dimmed,
2: GoogleLetterSpriteState.dimmed,
3: GoogleLetterSpriteState.dimmed,
4: GoogleLetterSpriteState.dimmed,
5: GoogleLetterSpriteState.dimmed,
},
),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(
const GoogleWordState(letterSpriteStates: {}),
isNotNull,
);
});
test('initial has all dimmed sprite states', () {
const initialState = GoogleWordState(
letterSpriteStates: {
0: GoogleLetterSpriteState.dimmed,
1: GoogleLetterSpriteState.dimmed,
2: GoogleLetterSpriteState.dimmed,
3: GoogleLetterSpriteState.dimmed,
4: GoogleLetterSpriteState.dimmed,
5: GoogleLetterSpriteState.dimmed,
},
);
expect(GoogleWordState.initial(), equals(initialState));
});
});
});
}

@ -4,8 +4,6 @@ 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/components/google_word/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame { class _TestGame extends Forge2DGame {
@ -28,10 +26,10 @@ class _TestGame extends Forge2DGame {
]); ]);
} }
Future<void> pump(GoogleWord child, {GameBloc? gameBloc}) { Future<void> pump(GoogleWord child) async {
return ensureAdd( await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value( FlameBlocProvider<GoogleWordCubit, GoogleWordState>.value(
value: gameBloc ?? GameBloc(), value: GoogleWordCubit(),
children: [child], children: [child],
), ),
); );
@ -44,25 +42,21 @@ void main() {
final flameTester = FlameTester(_TestGame.new); final flameTester = FlameTester(_TestGame.new);
group('GoogleWord', () { group('GoogleWord', () {
test('can be instantiated', () {
expect(GoogleWord(position: Vector2.zero()), isA<GoogleWord>());
});
flameTester.test( flameTester.test(
'loads the letters correctly', 'loads letters correctly',
(game) async { (game) async {
const word = 'Google';
final googleWord = GoogleWord(position: Vector2.zero()); final googleWord = GoogleWord(position: Vector2.zero());
await game.pump(googleWord); await game.pump(googleWord);
final letters = googleWord.children.whereType<GoogleLetter>();
expect(letters.length, equals(word.length));
},
);
flameTester.test('adds a GoogleWordBonusBehavior', (game) async {
final googleWord = GoogleWord(position: Vector2.zero());
await game.pump(googleWord);
expect( expect(
googleWord.children.whereType<GoogleWordBonusBehavior>().single, googleWord.children.whereType<GoogleLetter>().length,
isNotNull, equals(6),
);
},
); );
});
}); });
} }

@ -21,7 +21,7 @@ void main() {
'MultiballBlinkingBehavior', 'MultiballBlinkingBehavior',
() { () {
flameTester.testGameWidget( flameTester.testGameWidget(
'calls onBlink every 0.1 seconds when animation state is animated', 'calls onBlink every 0.18 seconds when animation state is animated',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior(); final behavior = MultiballBlinkingBehavior();
final bloc = _MockMultiballCubit(); final bloc = _MockMultiballCubit();
@ -48,7 +48,7 @@ void main() {
verify(bloc.onBlink).called(1); verify(bloc.onBlink).called(1);
await tester.pump(); await tester.pump();
game.update(0.1); game.update(0.18);
await streamController.close(); await streamController.close();
verify(bloc.onBlink).called(1); verify(bloc.onBlink).called(1);
@ -124,7 +124,7 @@ void main() {
); );
flameTester.testGameWidget( flameTester.testGameWidget(
'onTick stops after 10 blinks repetitions', 'onTick stops after 28 blinks',
setUp: (game, tester) async { setUp: (game, tester) async {
final behavior = MultiballBlinkingBehavior(); final behavior = MultiballBlinkingBehavior();
final bloc = _MockMultiballCubit(); final bloc = _MockMultiballCubit();
@ -148,7 +148,7 @@ void main() {
); );
await tester.pump(); await tester.pump();
for (var i = 0; i < 10; i++) { for (var i = 0; i < 28; i++) {
behavior.onTick(); behavior.onTick();
} }

@ -31,7 +31,7 @@ void main() {
); );
}); });
test('initial is idle with mouth closed', () { test('initial is dimmed and not blinking', () {
const initialState = SkillShotState( const initialState = SkillShotState(
spriteState: SkillShotSpriteState.dimmed, spriteState: SkillShotSpriteState.dimmed,
isBlinking: false, isBlinking: false,
@ -45,13 +45,13 @@ void main() {
'copies correctly ' 'copies correctly '
'when no argument specified', 'when no argument specified',
() { () {
const chromeDinoState = SkillShotState( const skillShotState = SkillShotState(
spriteState: SkillShotSpriteState.lit, spriteState: SkillShotSpriteState.lit,
isBlinking: true, isBlinking: true,
); );
expect( expect(
chromeDinoState.copyWith(), skillShotState.copyWith(),
equals(chromeDinoState), equals(skillShotState),
); );
}, },
); );
@ -60,7 +60,7 @@ void main() {
'copies correctly ' 'copies correctly '
'when all arguments specified', 'when all arguments specified',
() { () {
const chromeDinoState = SkillShotState( const skillShotState = SkillShotState(
spriteState: SkillShotSpriteState.lit, spriteState: SkillShotSpriteState.lit,
isBlinking: true, isBlinking: true,
); );
@ -68,10 +68,10 @@ void main() {
spriteState: SkillShotSpriteState.dimmed, spriteState: SkillShotSpriteState.dimmed,
isBlinking: false, isBlinking: false,
); );
expect(chromeDinoState, isNot(equals(otherSkillShotState))); expect(skillShotState, isNot(equals(otherSkillShotState)));
expect( expect(
chromeDinoState.copyWith( skillShotState.copyWith(
spriteState: SkillShotSpriteState.dimmed, spriteState: SkillShotSpriteState.dimmed,
isBlinking: false, isBlinking: false,
), ),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 663 KiB

@ -12,13 +12,13 @@ class ShareRepository {
final String _appUrl; final String _appUrl;
/// Url to the Github Open Source Pinball project. /// Url to the Github Open Source Pinball project.
static const openSourceCode = 'https://github.com/VGVentures/pinball'; static const openSourceCode = 'https://github.com/flutter/pinball';
/// 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. /// Url to the Pinball game.
static const pinballGameUrl = 'https://ashehwkdkdjruejdnensjsjdne.web.app/#/'; static const pinballGameUrl = 'https://pinball.flutter.dev';
/// Returns a url to share the [value] on the given [platform]. /// Returns a url to share the [value] on the given [platform].
/// ///

@ -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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
class _MockAuthenticationRepository extends Mock class _MockAuthenticationRepository extends Mock
@ -17,18 +18,25 @@ class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
class _MockShareRepository extends Mock implements ShareRepository {} class _MockShareRepository extends Mock implements ShareRepository {}
class _MockPlatformHelper extends Mock implements PlatformHelper {
@override
bool get isMobile => false;
}
void main() { void main() {
group('App', () { group('App', () {
late AuthenticationRepository authenticationRepository; late AuthenticationRepository authenticationRepository;
late LeaderboardRepository leaderboardRepository; late LeaderboardRepository leaderboardRepository;
late ShareRepository shareRepository; late ShareRepository shareRepository;
late PinballAudioPlayer pinballAudioPlayer; late PinballAudioPlayer pinballAudioPlayer;
late PlatformHelper platformHelper;
setUp(() { setUp(() {
authenticationRepository = _MockAuthenticationRepository(); authenticationRepository = _MockAuthenticationRepository();
leaderboardRepository = _MockLeaderboardRepository(); leaderboardRepository = _MockLeaderboardRepository();
shareRepository = _MockShareRepository(); shareRepository = _MockShareRepository();
pinballAudioPlayer = _MockPinballAudioPlayer(); pinballAudioPlayer = _MockPinballAudioPlayer();
platformHelper = _MockPlatformHelper();
when(pinballAudioPlayer.load).thenAnswer((_) => [Future.value()]); when(pinballAudioPlayer.load).thenAnswer((_) => [Future.value()]);
}); });
@ -39,6 +47,7 @@ void main() {
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
pinballAudioPlayer: pinballAudioPlayer, pinballAudioPlayer: pinballAudioPlayer,
platformHelper: platformHelper,
), ),
); );
await tester.pump(const Duration(milliseconds: 1100)); await tester.pump(const Duration(milliseconds: 1100));

@ -12,6 +12,7 @@ import 'package:pinball/select_character/select_character.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:platform_helper/platform_helper.dart';
class _TestGame extends Forge2DGame { class _TestGame extends Forge2DGame {
@override @override
@ -28,12 +29,18 @@ class _TestGame extends Forge2DGame {
Future<void> pump( Future<void> pump(
List<Component> children, { List<Component> children, {
CharacterThemeCubit? characterThemeBloc, CharacterThemeCubit? characterThemeBloc,
PlatformHelper? platformHelper,
}) async { }) async {
await ensureAdd( await ensureAdd(
FlameBlocProvider<CharacterThemeCubit, CharacterThemeState>.value( FlameBlocProvider<CharacterThemeCubit, CharacterThemeState>.value(
value: characterThemeBloc ?? CharacterThemeCubit(), value: characterThemeBloc ?? CharacterThemeCubit(),
children: [
FlameProvider.value(
platformHelper ?? _MockPlatformHelper(),
children: children, children: children,
), ),
],
),
); );
} }
} }
@ -43,6 +50,8 @@ class _MockBallCubit extends Mock implements BallCubit {}
class _MockArcadeBackgroundCubit extends Mock implements ArcadeBackgroundCubit { class _MockArcadeBackgroundCubit extends Mock implements ArcadeBackgroundCubit {
} }
class _MockPlatformHelper extends Mock implements PlatformHelper {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -68,8 +77,45 @@ void main() {
); );
flameTester.test( flameTester.test(
'onNewState calls onCharacterSelected on the arcade background bloc', 'onNewState does not call onCharacterSelected on the arcade background '
'bloc when platform is mobile',
(game) async { (game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenAnswer((_) => true);
final arcadeBackgroundBloc = _MockArcadeBackgroundCubit();
whenListen(
arcadeBackgroundBloc,
const Stream<ArcadeBackgroundState>.empty(),
initialState: const ArcadeBackgroundState.initial(),
);
final behavior = CharacterSelectionBehavior();
await game.pump(
[
behavior,
ZCanvasComponent(),
Plunger.test(compressionDistance: 10),
Ball.test(),
],
platformHelper: platformHelper,
);
const dinoThemeState = CharacterThemeState(theme.DinoTheme());
behavior.onNewState(dinoThemeState);
await game.ready();
verifyNever(
() => arcadeBackgroundBloc
.onCharacterSelected(dinoThemeState.characterTheme),
);
},
);
flameTester.test(
'onNewState calls onCharacterSelected on the arcade background '
'bloc when platform is not mobile',
(game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenAnswer((_) => false);
final arcadeBackgroundBloc = _MockArcadeBackgroundCubit(); final arcadeBackgroundBloc = _MockArcadeBackgroundCubit();
whenListen( whenListen(
arcadeBackgroundBloc, arcadeBackgroundBloc,
@ -79,13 +125,16 @@ void main() {
final arcadeBackground = final arcadeBackground =
ArcadeBackground.test(bloc: arcadeBackgroundBloc); ArcadeBackground.test(bloc: arcadeBackgroundBloc);
final behavior = CharacterSelectionBehavior(); final behavior = CharacterSelectionBehavior();
await game.pump([ await game.pump(
[
arcadeBackground, arcadeBackground,
behavior, behavior,
ZCanvasComponent(), ZCanvasComponent(),
Plunger.test(compressionDistance: 10), Plunger.test(compressionDistance: 10),
Ball.test(), Ball.test(),
]); ],
platformHelper: platformHelper,
);
const dinoThemeState = CharacterThemeState(theme.DinoTheme()); const dinoThemeState = CharacterThemeState(theme.DinoTheme());
behavior.onNewState(dinoThemeState); behavior.onNewState(dinoThemeState);
@ -101,6 +150,8 @@ void main() {
flameTester.test( flameTester.test(
'onNewState calls onCharacterSelected on the ball bloc', 'onNewState calls onCharacterSelected on the ball bloc',
(game) async { (game) async {
final platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenAnswer((_) => false);
final ballBloc = _MockBallCubit(); final ballBloc = _MockBallCubit();
whenListen( whenListen(
ballBloc, ballBloc,
@ -109,13 +160,16 @@ void main() {
); );
final ball = Ball.test(bloc: ballBloc); final ball = Ball.test(bloc: ballBloc);
final behavior = CharacterSelectionBehavior(); 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(), ArcadeBackground.test(),
]); ],
platformHelper: platformHelper,
);
const dinoThemeState = CharacterThemeState(theme.DinoTheme()); const dinoThemeState = CharacterThemeState(theme.DinoTheme());
behavior.onNewState(dinoThemeState); behavior.onNewState(dinoThemeState);

@ -42,16 +42,31 @@ class _TestGame extends Forge2DGame
Assets.images.backbox.button.facebook.keyName, Assets.images.backbox.button.facebook.keyName,
Assets.images.backbox.button.twitter.keyName, Assets.images.backbox.button.twitter.keyName,
Assets.images.backbox.displayTitleDecoration.keyName, Assets.images.backbox.displayTitleDecoration.keyName,
Assets.images.displayArrows.arrowLeft.keyName,
Assets.images.displayArrows.arrowRight.keyName,
]); ]);
} }
Future<void> pump(Backbox component) { Future<void> pump(
return ensureAdd( Backbox component, {
PlatformHelper? platformHelper,
}) async {
// Not needed once https://github.com/flame-engine/flame/issues/1607
// is fixed
await onLoad();
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value( FlameBlocProvider<GameBloc, GameState>.value(
value: GameBloc(), value: GameBloc(),
children: [ children: [
FlameProvider.value( MultiFlameProvider(
providers: [
FlameProvider<AppLocalizations>.value(
_MockAppLocalizations(), _MockAppLocalizations(),
),
FlameProvider<PlatformHelper>.value(
platformHelper ?? _MockPlatformHelper(),
),
],
children: [component], children: [component],
), ),
], ],
@ -84,6 +99,8 @@ class _MockShareRepository extends Mock implements ShareRepository {}
class _MockTapDownInfo extends Mock implements TapDownInfo {} class _MockTapDownInfo extends Mock implements TapDownInfo {}
class _MockTapUpInfo extends Mock implements TapUpInfo {}
class _MockUrlLauncher extends Mock class _MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {} implements UrlLauncherPlatform {}
@ -156,7 +173,7 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
String get leaderboardErrorMessage => ''; String get leaderboardErrorMessage => '';
@override @override
String iGotScoreAtPinball(int _) => ''; String iGotScoreAtPinball(String _) => '';
} }
void main() { void main() {
@ -186,9 +203,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
expect(game.descendants(), contains(backbox)); expect(game.descendants(), contains(backbox));
}, },
); );
@ -204,8 +223,8 @@ void main() {
Backbox.test( Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
platformHelper: platformHelper,
), ),
platformHelper: platformHelper,
); );
await tester.pump(); await tester.pump();
}, },
@ -226,9 +245,11 @@ void main() {
initialEntries: [LeaderboardEntryData.empty], initialEntries: [LeaderboardEntryData.empty],
), ),
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
backbox.requestInitials( backbox.requestInitials(
score: 0, score: 0,
character: game.character, character: game.character,
@ -258,9 +279,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {}); game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {});
verify( verify(
@ -287,9 +310,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
expect( expect(
game.descendants().whereType<GameOverInfoDisplay>().length, game.descendants().whereType<GameOverInfoDisplay>().length,
@ -317,9 +342,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
expect( expect(
game.overlays.value, game.overlays.value,
@ -344,9 +371,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
expect( expect(
game.overlays.value, game.overlays.value,
@ -367,9 +396,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
expect( expect(
game.descendants().whereType<GameOverInfoDisplay>().length, game.descendants().whereType<GameOverInfoDisplay>().length,
@ -390,9 +421,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
final shareLink = final shareLink =
game.descendants().whereType<ShareLinkComponent>().first; game.descendants().whereType<ShareLinkComponent>().first;
@ -420,9 +453,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
expect( expect(
game game
@ -452,9 +487,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
expect( expect(
game.descendants().whereType<ShareDisplay>().length, game.descendants().whereType<ShareDisplay>().length,
@ -499,13 +536,15 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: shareRepository, shareRepository: shareRepository,
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
final facebookButton = final facebookButton =
game.descendants().whereType<FacebookButtonComponent>().first; game.descendants().whereType<FacebookButtonComponent>().first;
facebookButton.onTapDown(_MockTapDownInfo()); facebookButton.onTapUp(_MockTapUpInfo());
await game.ready(); await game.ready();
@ -553,13 +592,15 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: shareRepository, shareRepository: shareRepository,
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
final facebookButton = final facebookButton =
game.descendants().whereType<TwitterButtonComponent>().first; game.descendants().whereType<TwitterButtonComponent>().first;
facebookButton.onTapDown(_MockTapDownInfo()); facebookButton.onTapUp(_MockTapUpInfo());
await game.ready(); await game.ready();
@ -585,9 +626,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
expect( expect(
game.descendants().whereType<LeaderboardDisplay>().length, game.descendants().whereType<LeaderboardDisplay>().length,
@ -608,9 +651,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
expect( expect(
game.descendants().whereType<LeaderboardFailureDisplay>().length, game.descendants().whereType<LeaderboardFailureDisplay>().length,
@ -632,9 +677,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
backbox.removeFromParent(); backbox.removeFromParent();
await game.ready(); await game.ready();
@ -673,9 +720,11 @@ void main() {
final backbox = Backbox.test( final backbox = Backbox.test(
bloc: bloc, bloc: bloc,
shareRepository: _MockShareRepository(), shareRepository: _MockShareRepository(),
);
await game.pump(
backbox,
platformHelper: platformHelper, platformHelper: platformHelper,
); );
await game.pump(backbox);
game.update(4); game.update(4);
verify( verify(

@ -68,6 +68,8 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
class _MockTapDownInfo extends Mock implements TapDownInfo {} class _MockTapDownInfo extends Mock implements TapDownInfo {}
class _MockTapUpInfo extends Mock implements TapUpInfo {}
class _MockUrlLauncher extends Mock class _MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {} implements UrlLauncherPlatform {}
@ -135,7 +137,7 @@ void main() {
final googleLink = final googleLink =
component.descendants().whereType<GoogleIOLinkComponent>().first; component.descendants().whereType<GoogleIOLinkComponent>().first;
googleLink.onTapDown(_MockTapDownInfo()); googleLink.onTapUp(_MockTapUpInfo());
await game.ready(); await game.ready();

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_forge2d/forge2d_game.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';
@ -8,8 +9,9 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/backbox/displays/leaderboard_display.dart'; import 'package:pinball/game/components/backbox/displays/leaderboard_display.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.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'; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class _MockAppLocalizations extends Mock implements AppLocalizations { class _MockAppLocalizations extends Mock implements AppLocalizations {
@override @override
@ -22,12 +24,16 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
String get name => 'name'; String get name => 'name';
} }
class _TestGame extends Forge2DGame { class _TestGame extends Forge2DGame with HasTappables {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
images.prefix = ''; images.prefix = '';
await images.load(const AndroidTheme().leaderboardIcon.keyName); await images.loadAll([
const AndroidTheme().leaderboardIcon.keyName,
Assets.images.displayArrows.arrowLeft.keyName,
Assets.images.displayArrows.arrowRight.keyName,
]);
} }
Future<void> pump(LeaderboardDisplay component) { Future<void> pump(LeaderboardDisplay component) {
@ -40,27 +46,7 @@ class _TestGame extends Forge2DGame {
} }
} }
void main() { const leaderboard = [
group('LeaderboardDisplay', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
flameTester.test('renders the titles', (game) async {
await game.pump(LeaderboardDisplay(entries: const []));
final textComponents =
game.descendants().whereType<TextComponent>().toList();
expect(textComponents.length, equals(3));
expect(textComponents[0].text, equals('rank'));
expect(textComponents[1].text, equals('score'));
expect(textComponents[2].text, equals('name'));
});
flameTester.test('renders the entries', (game) async {
await game.pump(
LeaderboardDisplay(
entries: const [
LeaderboardEntryData( LeaderboardEntryData(
playerInitials: 'AAA', playerInitials: 'AAA',
score: 123, score: 123,
@ -81,19 +67,69 @@ void main() {
score: 12346, score: 12346,
character: CharacterType.android, character: CharacterType.android,
), ),
], LeaderboardEntryData(
playerInitials: 'EEE',
score: 123467,
character: CharacterType.android,
), ),
); LeaderboardEntryData(
playerInitials: 'FFF',
score: 123468,
character: CharacterType.android,
),
LeaderboardEntryData(
playerInitials: 'GGG',
score: 1234689,
character: CharacterType.android,
),
LeaderboardEntryData(
playerInitials: 'HHH',
score: 12346891,
character: CharacterType.android,
),
LeaderboardEntryData(
playerInitials: 'III',
score: 123468912,
character: CharacterType.android,
),
LeaderboardEntryData(
playerInitials: 'JJJ',
score: 1234689121,
character: CharacterType.android,
),
];
void main() {
group('LeaderboardDisplay', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
flameTester.test('renders the titles', (game) async {
await game.pump(LeaderboardDisplay(entries: const []));
final textComponents =
game.descendants().whereType<TextComponent>().toList();
expect(textComponents.length, equals(3));
expect(textComponents[0].text, equals('rank'));
expect(textComponents[1].text, equals('score'));
expect(textComponents[2].text, equals('name'));
});
flameTester.test('renders the first 5 entries', (game) async {
await game.pump(LeaderboardDisplay(entries: leaderboard));
for (final text in [ for (final text in [
'AAA', 'AAA',
'BBB', 'BBB',
'CCC', 'CCC',
'DDD', 'DDD',
'EEE',
'1st', '1st',
'2nd', '2nd',
'3rd', '3rd',
'4th' '4th',
'5th',
]) { ]) {
expect( expect(
game game
@ -105,5 +141,120 @@ void main() {
); );
} }
}); });
flameTester.test('can open the second page', (game) async {
final display = LeaderboardDisplay(entries: leaderboard);
await game.pump(display);
final arrow = game
.descendants()
.whereType<ArrowIcon>()
.where((arrow) => arrow.direction == ArrowIconDirection.right)
.single;
// Tap the arrow
arrow.onTap();
// Wait for the transition to finish
display.updateTree(5);
await game.ready();
for (final text in [
'FFF',
'GGG',
'HHH',
'III',
'JJJ',
'6th',
'7th',
'8th',
'9th',
'10th',
]) {
expect(
game
.descendants()
.whereType<TextComponent>()
.where((textComponent) => textComponent.text == text)
.length,
equals(1),
);
}
});
flameTester.test(
'can open the second page and go back to the first',
(game) async {
final display = LeaderboardDisplay(entries: leaderboard);
await game.pump(display);
var arrow = game
.descendants()
.whereType<ArrowIcon>()
.where((arrow) => arrow.direction == ArrowIconDirection.right)
.single;
// Tap the arrow
arrow.onTap();
// Wait for the transition to finish
display.updateTree(5);
await game.ready();
for (final text in [
'FFF',
'GGG',
'HHH',
'III',
'JJJ',
'6th',
'7th',
'8th',
'9th',
'10th',
]) {
expect(
game
.descendants()
.whereType<TextComponent>()
.where((textComponent) => textComponent.text == text)
.length,
equals(1),
);
}
arrow = game
.descendants()
.whereType<ArrowIcon>()
.where((arrow) => arrow.direction == ArrowIconDirection.left)
.single;
// Tap the arrow
arrow.onTap();
// Wait for the transition to finish
display.updateTree(5);
await game.ready();
for (final text in [
'AAA',
'BBB',
'CCC',
'DDD',
'EEE',
'1st',
'2nd',
'3rd',
'4th',
'5th',
]) {
expect(
game
.descendants()
.whereType<TextComponent>()
.where((textComponent) => textComponent.text == text)
.length,
equals(1),
);
}
},
);
}); });
} }

@ -52,7 +52,7 @@ class _MockAppLocalizations extends Mock implements AppLocalizations {
String get socialMediaAccount => ''; String get socialMediaAccount => '';
} }
class _MockTapDownInfo extends Mock implements TapDownInfo {} class _MockTapUpInfo extends Mock implements TapUpInfo {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -74,7 +74,7 @@ void main() {
(game) async { (game) async {
var tapped = false; var tapped = false;
final tapDownInfo = _MockTapDownInfo(); final tapUpInfo = _MockTapUpInfo();
final component = ShareDisplay( final component = ShareDisplay(
onShare: (_) => tapped = true, onShare: (_) => tapped = true,
); );
@ -83,7 +83,7 @@ void main() {
final facebookButton = final facebookButton =
component.descendants().whereType<FacebookButtonComponent>().first; component.descendants().whereType<FacebookButtonComponent>().first;
facebookButton.onTapDown(tapDownInfo); facebookButton.onTapUp(tapUpInfo);
expect(tapped, isTrue); expect(tapped, isTrue);
}, },
@ -94,7 +94,7 @@ void main() {
(game) async { (game) async {
var tapped = false; var tapped = false;
final tapDownInfo = _MockTapDownInfo(); final tapUpInfo = _MockTapUpInfo();
final component = ShareDisplay( final component = ShareDisplay(
onShare: (_) => tapped = true, onShare: (_) => tapped = true,
); );
@ -103,7 +103,7 @@ void main() {
final twitterButton = final twitterButton =
component.descendants().whereType<TwitterButtonComponent>().first; component.descendants().whereType<TwitterButtonComponent>().first;
twitterButton.onTapDown(tapDownInfo); twitterButton.onTapUp(tapUpInfo);
expect(tapped, isTrue); expect(tapped, isTrue);
}, },

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_bloc/flame_bloc.dart'; 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';
@ -14,9 +15,10 @@ 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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
class _TestGame extends Forge2DGame { class _TestGame extends Forge2DGame with HasTappables {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
images.prefix = ''; images.prefix = '';
@ -25,6 +27,8 @@ class _TestGame extends Forge2DGame {
const theme.DashTheme().leaderboardIcon.keyName, const theme.DashTheme().leaderboardIcon.keyName,
Assets.images.backbox.marquee.keyName, Assets.images.backbox.marquee.keyName,
Assets.images.backbox.displayDivider.keyName, Assets.images.backbox.displayDivider.keyName,
Assets.images.displayArrows.arrowLeft.keyName,
Assets.images.displayArrows.arrowRight.keyName,
], ],
); );
} }
@ -52,6 +56,9 @@ class _TestGame extends Forge2DGame {
FlameProvider<AppLocalizations>.value( FlameProvider<AppLocalizations>.value(
_MockAppLocalizations(), _MockAppLocalizations(),
), ),
FlameProvider<PlatformHelper>.value(
_MockPlatformHelper(),
),
], ],
children: children, children: children,
), ),
@ -68,6 +75,11 @@ class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
class _MockShareRepository extends Mock implements ShareRepository {} class _MockShareRepository extends Mock implements ShareRepository {}
class _MockPlatformHelper extends Mock implements PlatformHelper {
@override
bool get isMobile => false;
}
class _MockAppLocalizations extends Mock implements AppLocalizations { class _MockAppLocalizations extends Mock implements AppLocalizations {
@override @override
String get score => ''; String get score => '';

@ -0,0 +1,167 @@
// ignore_for_file: cascade_invocations
import 'dart:async';
import 'package:bloc_test/bloc_test.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/behaviors/behaviors.dart';
import 'package:pinball/game/components/google_gallery/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.googleWord.letter1.lit.keyName,
Assets.images.googleWord.letter1.dimmed.keyName,
Assets.images.googleWord.letter2.lit.keyName,
Assets.images.googleWord.letter2.dimmed.keyName,
Assets.images.googleWord.letter3.lit.keyName,
Assets.images.googleWord.letter3.dimmed.keyName,
Assets.images.googleWord.letter4.lit.keyName,
Assets.images.googleWord.letter4.dimmed.keyName,
Assets.images.googleWord.letter5.lit.keyName,
Assets.images.googleWord.letter5.dimmed.keyName,
Assets.images.googleWord.letter6.lit.keyName,
Assets.images.googleWord.letter6.dimmed.keyName,
]);
}
Future<void> pump(
GoogleGallery child, {
required GameBloc gameBloc,
required GoogleWordCubit googleWordBloc,
}) async {
// Not needed once https://github.com/flame-engine/flame/issues/1607
// is fixed
await onLoad();
await ensureAdd(
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
),
FlameBlocProvider<GoogleWordCubit, GoogleWordState>.value(
value: googleWordBloc,
),
],
children: [child],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('GoogleWordBonusBehavior', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
final flameTester = FlameTester(_TestGame.new);
flameTester.testGameWidget(
'adds GameBonus.googleWord to the game when all letters '
'in google word are activated and calls onBonusAwarded',
setUp: (game, tester) async {
final behavior = GoogleWordBonusBehavior();
final parent = GoogleGallery.test();
final googleWord = GoogleWord(position: Vector2.zero());
final googleWordBloc = _MockGoogleWordCubit();
final streamController = StreamController<GoogleWordState>();
whenListen(
googleWordBloc,
streamController.stream,
initialState: GoogleWordState.initial(),
);
await parent.add(googleWord);
await game.pump(
parent,
gameBloc: gameBloc,
googleWordBloc: googleWordBloc,
);
await parent.ensureAdd(behavior);
streamController.add(
const GoogleWordState(
letterSpriteStates: {
0: GoogleLetterSpriteState.lit,
1: GoogleLetterSpriteState.lit,
2: GoogleLetterSpriteState.lit,
3: GoogleLetterSpriteState.lit,
4: GoogleLetterSpriteState.lit,
5: GoogleLetterSpriteState.lit,
},
),
);
await tester.pump();
verify(
() => gameBloc.add(const BonusActivated(GameBonus.googleWord)),
).called(1);
verify(googleWordBloc.onBonusAwarded).called(1);
},
);
flameTester.testGameWidget(
'adds BonusBallSpawningBehavior to the game when all letters '
'in google word are activated',
setUp: (game, tester) async {
final behavior = GoogleWordBonusBehavior();
final parent = GoogleGallery.test();
final googleWord = GoogleWord(position: Vector2.zero());
final googleWordBloc = _MockGoogleWordCubit();
final streamController = StreamController<GoogleWordState>();
whenListen(
googleWordBloc,
streamController.stream,
initialState: GoogleWordState.initial(),
);
await parent.add(googleWord);
await game.pump(
parent,
gameBloc: gameBloc,
googleWordBloc: googleWordBloc,
);
await parent.ensureAdd(behavior);
streamController.add(
const GoogleWordState(
letterSpriteStates: {
0: GoogleLetterSpriteState.lit,
1: GoogleLetterSpriteState.lit,
2: GoogleLetterSpriteState.lit,
3: GoogleLetterSpriteState.lit,
4: GoogleLetterSpriteState.lit,
5: GoogleLetterSpriteState.lit,
},
),
);
await tester.pump();
await game.ready();
expect(
game.descendants().whereType<BonusBallSpawningBehavior>().length,
equals(1),
);
},
);
});
}

@ -0,0 +1,110 @@
// ignore_for_file: cascade_invocations
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/behaviors/behaviors.dart';
import 'package:pinball/game/components/google_gallery/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.googleWord.letter1.lit.keyName,
Assets.images.googleWord.letter1.dimmed.keyName,
Assets.images.googleWord.letter2.lit.keyName,
Assets.images.googleWord.letter2.dimmed.keyName,
Assets.images.googleWord.letter3.lit.keyName,
Assets.images.googleWord.letter3.dimmed.keyName,
Assets.images.googleWord.letter4.lit.keyName,
Assets.images.googleWord.letter4.dimmed.keyName,
Assets.images.googleWord.letter5.lit.keyName,
Assets.images.googleWord.letter5.dimmed.keyName,
Assets.images.googleWord.letter6.lit.keyName,
Assets.images.googleWord.letter6.dimmed.keyName,
Assets.images.googleRollover.left.decal.keyName,
Assets.images.googleRollover.left.pin.keyName,
Assets.images.googleRollover.right.decal.keyName,
Assets.images.googleRollover.right.pin.keyName,
]);
}
Future<void> pump(GoogleGallery child) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: _MockGameBloc(),
children: [child],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(_TestGame.new);
group('GoogleGallery', () {
flameTester.test('loads correctly', (game) async {
final component = GoogleGallery();
await game.pump(component);
expect(game.descendants(), contains(component));
});
group('loads', () {
flameTester.test(
'two GoogleRollovers',
(game) async {
await game.pump(GoogleGallery());
expect(
game.descendants().whereType<GoogleRollover>().length,
equals(2),
);
},
);
flameTester.test(
'a GoogleWord',
(game) async {
await game.pump(GoogleGallery());
expect(
game.descendants().whereType<GoogleWord>().length,
equals(1),
);
},
);
});
group('adds', () {
flameTester.test(
'ScoringContactBehavior to GoogleRollovers',
(game) async {
await game.pump(GoogleGallery());
game.descendants().whereType<GoogleRollover>().forEach(
(rollover) => expect(
rollover.firstChild<ScoringContactBehavior>(),
isNotNull,
),
);
},
);
flameTester.test('a GoogleWordBonusBehavior', (game) async {
final component = GoogleGallery();
await game.pump(component);
expect(
component.descendants().whereType<GoogleWordBonusBehavior>().single,
isNotNull,
);
});
});
});
}

@ -1,94 +0,0 @@
// ignore_for_file: cascade_invocations
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/components/google_word/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.googleWord.letter1.lit.keyName,
Assets.images.googleWord.letter1.dimmed.keyName,
Assets.images.googleWord.letter2.lit.keyName,
Assets.images.googleWord.letter2.dimmed.keyName,
Assets.images.googleWord.letter3.lit.keyName,
Assets.images.googleWord.letter3.dimmed.keyName,
Assets.images.googleWord.letter4.lit.keyName,
Assets.images.googleWord.letter4.dimmed.keyName,
Assets.images.googleWord.letter5.lit.keyName,
Assets.images.googleWord.letter5.dimmed.keyName,
Assets.images.googleWord.letter6.lit.keyName,
Assets.images.googleWord.letter6.dimmed.keyName,
]);
}
Future<void> pump(GoogleWord child, {required GameBloc gameBloc}) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
children: [
FlameProvider<PinballAudioPlayer>.value(
_MockPinballAudioPlayer(),
children: [child],
)
],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('GoogleWordBonusBehaviors', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
final flameTester = FlameTester(_TestGame.new);
flameTester.testGameWidget(
'adds GameBonus.googleWord to the game when all letters are activated',
setUp: (game, tester) async {
await game.onLoad();
final behavior = GoogleWordBonusBehavior();
final parent = GoogleWord.test();
final letters = [
GoogleLetter(0),
GoogleLetter(1),
GoogleLetter(2),
GoogleLetter(3),
GoogleLetter(4),
GoogleLetter(5),
];
await parent.addAll(letters);
await game.pump(parent, gameBloc: gameBloc);
await parent.ensureAdd(behavior);
for (final letter in letters) {
letter.bloc.onBallContacted();
}
await tester.pump();
verify(
() => gameBloc.add(const BonusActivated(GameBonus.googleWord)),
).called(1);
},
);
});
}

@ -78,8 +78,24 @@ void main() {
); );
test( test(
'is false when the bonusHistory has changed ' 'is true when the bonusHistory has changed '
'with a bonus different than GameBonus.dashNest', () { 'with a new GameBonus.googleWord',
() {
final previous = GameState.initial();
final state = previous.copyWith(
bonusHistory: [GameBonus.googleWord],
);
expect(
MultiballsBehavior().listenWhen(previous, state),
isTrue,
);
},
);
test(
'is false when the bonusHistory has changed with a bonus other than '
'GameBonus.dashNest or GameBonus.googleWord', () {
final previous = final previous =
GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]); GameState.initial().copyWith(bonusHistory: [GameBonus.dashNest]);
final state = previous.copyWith( final state = previous.copyWith(

@ -16,6 +16,7 @@ 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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
class _TestPinballGame extends PinballGame { class _TestPinballGame extends PinballGame {
@ -27,6 +28,7 @@ class _TestPinballGame extends PinballGame {
gameBloc: GameBloc(), gameBloc: GameBloc(),
l10n: _MockAppLocalizations(), l10n: _MockAppLocalizations(),
audioPlayer: _MockPinballAudioPlayer(), audioPlayer: _MockPinballAudioPlayer(),
platformHelper: _MockPlatformHelper(),
); );
@override @override
@ -47,6 +49,7 @@ class _TestDebugPinballGame extends DebugPinballGame {
gameBloc: GameBloc(), gameBloc: GameBloc(),
l10n: _MockAppLocalizations(), l10n: _MockAppLocalizations(),
audioPlayer: _MockPinballAudioPlayer(), audioPlayer: _MockPinballAudioPlayer(),
platformHelper: _MockPlatformHelper(),
); );
@override @override
@ -88,6 +91,11 @@ class _MockShareRepository extends Mock implements ShareRepository {}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockPlatformHelper extends Mock implements PlatformHelper {
@override
bool get isMobile => false;
}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -184,11 +192,11 @@ void main() {
); );
flameTester.test( flameTester.test(
'one GoogleWord', 'one GoogleGallery',
(game) async { (game) async {
await game.ready(); await game.ready();
expect( expect(
game.descendants().whereType<GoogleWord>().length, game.descendants().whereType<GoogleGallery>().length,
equals(1), equals(1),
); );
}, },

@ -15,6 +15,7 @@ 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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -28,6 +29,7 @@ class _TestPinballGame extends PinballGame {
gameBloc: GameBloc(), gameBloc: GameBloc(),
l10n: _MockAppLocalizations(), l10n: _MockAppLocalizations(),
audioPlayer: _MockPinballAudioPlayer(), audioPlayer: _MockPinballAudioPlayer(),
platformHelper: _MockPlatformHelper(),
); );
@override @override
@ -63,6 +65,11 @@ class _MockLeaderboardRepository extends Mock implements LeaderboardRepository {
class _MockShareRepository extends Mock implements ShareRepository {} class _MockShareRepository extends Mock implements ShareRepository {}
class _MockPlatformHelper extends Mock implements PlatformHelper {
@override
bool get isMobile => false;
}
void main() { void main() {
final game = _TestPinballGame(); final game = _TestPinballGame();

@ -29,7 +29,7 @@ void main() {
expect(find.text('Play'), findsOneWidget); expect(find.text('Play'), findsOneWidget);
}); });
testWidgets('adds PlayTapped event to StartGameBloc when taped', testWidgets('adds PlayTapped event to StartGameBloc when tapped',
(tester) async { (tester) async {
await tester.pumpApp( await tester.pumpApp(
const PlayButtonOverlay(), const PlayButtonOverlay(),

@ -12,6 +12,7 @@ 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:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}
@ -29,6 +30,8 @@ class _MockStartGameBloc extends Mock implements StartGameBloc {}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockPlatformHelper extends Mock implements PlatformHelper {}
PinballAudioPlayer _buildDefaultPinballAudioPlayer() { PinballAudioPlayer _buildDefaultPinballAudioPlayer() {
final audioPlayer = _MockPinballAudioPlayer(); final audioPlayer = _MockPinballAudioPlayer();
when(audioPlayer.load).thenAnswer((_) => [Future.value()]); when(audioPlayer.load).thenAnswer((_) => [Future.value()]);
@ -60,6 +63,7 @@ extension PumpApp on WidgetTester {
LeaderboardRepository? leaderboardRepository, LeaderboardRepository? leaderboardRepository,
ShareRepository? shareRepository, ShareRepository? shareRepository,
PinballAudioPlayer? pinballAudioPlayer, PinballAudioPlayer? pinballAudioPlayer,
PlatformHelper? platformHelper,
}) { }) {
return runAsync(() { return runAsync(() {
return pumpWidget( return pumpWidget(
@ -74,6 +78,9 @@ extension PumpApp on WidgetTester {
RepositoryProvider.value( RepositoryProvider.value(
value: pinballAudioPlayer ?? _buildDefaultPinballAudioPlayer(), value: pinballAudioPlayer ?? _buildDefaultPinballAudioPlayer(),
), ),
RepositoryProvider.value(
value: platformHelper ?? _MockPlatformHelper(),
),
], ],
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [

@ -17,27 +17,23 @@ void main() {
setUp(() async { setUp(() async {
l10n = await AppLocalizations.delegate.load(const Locale('en')); l10n = await AppLocalizations.delegate.load(const Locale('en'));
platformHelper = _MockPlatformHelper(); platformHelper = _MockPlatformHelper();
when(() => platformHelper.isMobile).thenAnswer((_) => false);
}); });
testWidgets( test('can be instantiated', () {
'can be instantiated without passing in a platform helper', expect(
(tester) async { HowToPlayDialog(onDismissCallback: () {}),
await tester.pumpApp( isA<HowToPlayDialog>(),
HowToPlayDialog(
onDismissCallback: () {},
),
);
expect(find.byType(HowToPlayDialog), findsOneWidget);
},
); );
});
testWidgets('displays content for desktop', (tester) async { testWidgets('displays content for desktop', (tester) async {
when(() => platformHelper.isMobile).thenAnswer((_) => false); when(() => platformHelper.isMobile).thenAnswer((_) => false);
await tester.pumpApp( await tester.pumpApp(
HowToPlayDialog( HowToPlayDialog(
platformHelper: platformHelper,
onDismissCallback: () {}, onDismissCallback: () {},
), ),
platformHelper: platformHelper,
); );
expect(find.text(l10n.howToPlay), findsOneWidget); expect(find.text(l10n.howToPlay), findsOneWidget);
expect(find.text(l10n.tipsForFlips), findsOneWidget); expect(find.text(l10n.tipsForFlips), findsOneWidget);
@ -50,9 +46,9 @@ void main() {
when(() => platformHelper.isMobile).thenAnswer((_) => true); when(() => platformHelper.isMobile).thenAnswer((_) => true);
await tester.pumpApp( await tester.pumpApp(
HowToPlayDialog( HowToPlayDialog(
platformHelper: platformHelper,
onDismissCallback: () {}, onDismissCallback: () {},
), ),
platformHelper: platformHelper,
); );
expect(find.text(l10n.howToPlay), findsOneWidget); expect(find.text(l10n.howToPlay), findsOneWidget);
expect(find.text(l10n.tipsForFlips), findsOneWidget); expect(find.text(l10n.tipsForFlips), findsOneWidget);
@ -75,6 +71,7 @@ void main() {
); );
}, },
), ),
platformHelper: platformHelper,
); );
expect(find.byType(HowToPlayDialog), findsNothing); expect(find.byType(HowToPlayDialog), findsNothing);
await tester.tap(find.text('test')); await tester.tap(find.text('test'));
@ -100,6 +97,7 @@ void main() {
); );
}, },
), ),
platformHelper: platformHelper,
); );
expect(find.byType(HowToPlayDialog), findsNothing); expect(find.byType(HowToPlayDialog), findsNothing);
await tester.tap(find.text('test')); await tester.tap(find.text('test'));

@ -138,7 +138,7 @@ void main() {
); );
<String, String>{ <String, String>{
'Open Source Code': 'https://github.com/VGVentures/pinball', 'Open Source Code': 'https://github.com/flutter/pinball',
'Google I/O': 'https://events.google.com/io/', 'Google I/O': 'https://events.google.com/io/',
'Flutter Games': 'http://flutter.dev/games', 'Flutter Games': 'http://flutter.dev/games',
'How it’s made': 'How it’s made':

@ -7,6 +7,7 @@ import 'package:pinball/how_to_play/how_to_play.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:platform_helper/platform_helper.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -18,10 +19,16 @@ class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {}
class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {}
class _MockPlatformHelper extends Mock implements PlatformHelper {
@override
bool get isMobile => false;
}
void main() { void main() {
late StartGameBloc startGameBloc; late StartGameBloc startGameBloc;
late PinballAudioPlayer pinballAudioPlayer; late PinballAudioPlayer pinballAudioPlayer;
late CharacterThemeCubit characterThemeCubit; late CharacterThemeCubit characterThemeCubit;
late PlatformHelper platformHelper;
group('StartGameListener', () { group('StartGameListener', () {
setUp(() async { setUp(() async {
@ -30,6 +37,7 @@ void main() {
startGameBloc = _MockStartGameBloc(); startGameBloc = _MockStartGameBloc();
pinballAudioPlayer = _MockPinballAudioPlayer(); pinballAudioPlayer = _MockPinballAudioPlayer();
characterThemeCubit = _MockCharacterThemeCubit(); characterThemeCubit = _MockCharacterThemeCubit();
platformHelper = _MockPlatformHelper();
}); });
group('on selectCharacter status', () { group('on selectCharacter status', () {
@ -121,6 +129,7 @@ void main() {
child: SizedBox.shrink(), child: SizedBox.shrink(),
), ),
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
platformHelper: platformHelper,
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@ -213,6 +222,7 @@ void main() {
child: SizedBox.shrink(), child: SizedBox.shrink(),
), ),
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
platformHelper: platformHelper,
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@ -244,6 +254,7 @@ void main() {
), ),
startGameBloc: startGameBloc, startGameBloc: startGameBloc,
pinballAudioPlayer: pinballAudioPlayer, pinballAudioPlayer: pinballAudioPlayer,
platformHelper: platformHelper,
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();

Loading…
Cancel
Save