diff --git a/lib/game/components/backbox/displays/leaderboard_display.dart b/lib/game/components/backbox/displays/leaderboard_display.dart index ac7a798d..ab418ccc 100644 --- a/lib/game/components/backbox/displays/leaderboard_display.dart +++ b/lib/game/components/backbox/displays/leaderboard_display.dart @@ -1,4 +1,6 @@ +// cSpell:ignore sublist import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; import 'package:flutter/material.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/l10n/l10n.dart'; @@ -23,6 +25,23 @@ final _bodyTextPaint = TextPaint( ), ); +double _calcY(int i) => (i * 3.2) + 3.2; + +const _columns = [-14.0, 0.0, 14.0]; + +String _rank(int number) { + switch (number) { + case 1: + return '${number}st'; + case 2: + return '${number}nd'; + case 3: + return '${number}rd'; + default: + return '${number}th'; + } +} + /// {@template leaderboard_display} /// Component that builds the leaderboard list of the Backbox. /// {@endtemplate} @@ -33,21 +52,47 @@ class LeaderboardDisplay extends PositionComponent with HasGameRef { final List _entries; - double _calcY(int i) => (i * 3.2) + 3.2; + _MovePageArrow _findArrow({required bool active}) { + return descendants() + .whereType<_MovePageArrow>() + .firstWhere((arrow) => arrow.active == active); + } - static const _columns = [-15.0, 0.0, 15.0]; + void _changePage(List ranking, int offset) { + final current = descendants().whereType<_RankingPage>().single; + final activeArrow = _findArrow(active: true); + final inactiveArrow = _findArrow(active: false); - String _rank(int number) { - switch (number) { - case 1: - return '${number}st'; - case 2: - return '${number}nd'; - case 3: - return '${number}rd'; - default: - return '${number}th'; - } + activeArrow.active = false; + + current.add( + ScaleEffect.to( + Vector2(0, 1), + EffectController( + duration: 0.5, + curve: Curves.easeIn, + ), + )..onFinishCallback = () { + current.removeFromParent(); + inactiveArrow.active = true; + firstChild()?.add( + _RankingPage( + ranking: ranking, + offset: offset, + ) + ..scale = Vector2(0, 1) + ..add( + ScaleEffect.to( + Vector2(1, 1), + EffectController( + duration: 0.5, + curve: Curves.easeIn, + ), + ), + ), + ); + }, + ); } @override @@ -60,6 +105,20 @@ class LeaderboardDisplay extends PositionComponent with HasGameRef { PositionComponent( position: Vector2(0, 4), 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( children: [ TextComponent( @@ -82,39 +141,106 @@ class LeaderboardDisplay extends PositionComponent with HasGameRef { ), ], ), - for (var i = 0; i < ranking.length; i++) - PositionComponent( - children: [ - TextComponent( - text: _rank(i + 1), - textRenderer: _bodyTextPaint, - position: Vector2(_columns[0], _calcY(i)), - anchor: Anchor.center, - ), - TextComponent( - text: ranking[i].score.formatScore(), - textRenderer: _bodyTextPaint, - position: Vector2(_columns[1], _calcY(i)), - anchor: Anchor.center, - ), - SpriteComponent.fromImage( - gameRef.images.fromCache( - ranking[i].character.toTheme.leaderboardIcon.keyName, - ), - anchor: Anchor.center, - size: Vector2(1.8, 1.8), - position: Vector2(_columns[2] - 2.5, _calcY(i) + .25), - ), - TextComponent( - text: ranking[i].playerInitials, - textRenderer: _bodyTextPaint, - position: Vector2(_columns[2] + 1, _calcY(i)), - anchor: Anchor.center, - ), - ], - ), + _RankingPage( + ranking: ranking, + offset: 0, + ), ], ), ); } } + +class _RankingPage extends PositionComponent with HasGameRef { + _RankingPage({ + required this.ranking, + required this.offset, + }) : super(children: []); + + final List ranking; + final int offset; + + @override + Future onLoad() async { + await addAll([ + for (var i = 0; i < ranking.length; i++) + PositionComponent( + children: [ + TextComponent( + text: _rank(i + 1 + offset), + textRenderer: _bodyTextPaint, + position: Vector2(_columns[0], _calcY(i)), + anchor: Anchor.center, + ), + TextComponent( + text: ranking[i].score.formatScore(), + textRenderer: _bodyTextPaint, + position: Vector2(_columns[1], _calcY(i)), + anchor: Anchor.center, + ), + SpriteComponent.fromImage( + gameRef.images.fromCache( + ranking[i].character.toTheme.leaderboardIcon.keyName, + ), + anchor: Anchor.center, + size: Vector2(1.8, 1.8), + position: Vector2(_columns[2] - 3, _calcY(i) + .25), + ), + TextComponent( + text: ranking[i].playerInitials, + textRenderer: _bodyTextPaint, + position: Vector2(_columns[2] + 1, _calcY(i)), + anchor: Anchor.center, + ), + ], + ), + ]); + } +} + +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().isNotEmpty; + set active(bool value) { + if (value) { + add( + ArrowIcon( + position: Vector2.zero(), + direction: direction, + onTap: onTap, + ), + ); + } else { + firstChild()?.removeFromParent(); + } + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 324f379a..969ea1ac 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -6,7 +6,7 @@ export 'dino_desert/dino_desert.dart'; export 'drain/drain.dart'; export 'flutter_forest/flutter_forest.dart'; export 'game_bloc_status_listener.dart'; -export 'google_word/google_word.dart'; +export 'google_gallery/google_gallery.dart'; export 'launcher.dart'; export 'multiballs/multiballs.dart'; export 'multipliers/multipliers.dart'; diff --git a/lib/game/components/google_word/behaviors/behaviors.dart b/lib/game/components/google_gallery/behaviors/behaviors.dart similarity index 100% rename from lib/game/components/google_word/behaviors/behaviors.dart rename to lib/game/components/google_gallery/behaviors/behaviors.dart diff --git a/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart b/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart new file mode 100644 index 00000000..abb6de1e --- /dev/null +++ b/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart @@ -0,0 +1,24 @@ +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 { + @override + Future onLoad() async { + await super.onLoad(); + await add( + FlameBlocListener( + listenWhen: (_, state) => state.letterSpriteStates.values + .every((element) => element == GoogleLetterSpriteState.lit), + onNewState: (state) { + readBloc() + .add(const BonusActivated(GameBonus.googleWord)); + readBloc().onBonusAwarded(); + }, + ), + ); + } +} diff --git a/lib/game/components/google_gallery/google_gallery.dart b/lib/game/components/google_gallery/google_gallery.dart new file mode 100644 index 00000000..0b3d4b10 --- /dev/null +++ b/lib/game/components/google_gallery/google_gallery.dart @@ -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( + 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(); +} diff --git a/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart deleted file mode 100644 index c1c14ed5..00000000 --- a/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart +++ /dev/null @@ -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, FlameBlocReader { - @override - void onMount() { - super.onMount(); - - final googleLetters = parent.children.whereType(); - 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(); - } - } - }); - } - } -} diff --git a/lib/game/components/google_word/google_word.dart b/lib/game/components/google_word/google_word.dart deleted file mode 100644 index 76bac244..00000000 --- a/lib/game/components/google_word/google_word.dart +++ /dev/null @@ -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(); -} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 97551414..fccd494e 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -118,6 +118,10 @@ extension PinballGameAssetsX on PinballGame { 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.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.dimmed.keyName), images.load(components.Assets.images.multiplier.x2.lit.keyName), @@ -141,6 +145,8 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.skillShot.pin.keyName), images.load(components.Assets.images.skillShot.lit.keyName), images.load(components.Assets.images.skillShot.dimmed.keyName), + images.load(components.Assets.images.displayArrows.arrowLeft.keyName), + images.load(components.Assets.images.displayArrows.arrowRight.keyName), images.load(androidTheme.leaderboardIcon.keyName), images.load(androidTheme.ball.keyName), images.load(dashTheme.leaderboardIcon.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 24814462..dca26b84 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -119,7 +119,7 @@ class PinballGame extends PinballForge2DGame shareRepository: shareRepository, entries: _entries, ), - GoogleWord(position: Vector2(-4.45, 1.8)), + GoogleGallery(), Multipliers(), Multiballs(), SkillShot( diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 98074fc5..19a5d583 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -116,6 +116,28 @@ class _LoopAudio extends _Audio { } } +class _SingleLoopAudio extends _LoopAudio { + _SingleLoopAudio({ + required PreCacheSingleAudio preCacheSingleAudio, + required LoopSingleAudio loopSingleAudio, + required String path, + }) : super( + preCacheSingleAudio: preCacheSingleAudio, + loopSingleAudio: loopSingleAudio, + path: path, + ); + + bool _playing = false; + + @override + void play() { + if (!_playing) { + super.play(); + _playing = true; + } + } +} + class _RandomABAudio extends _Audio { _RandomABAudio({ required this.createAudioPool, @@ -270,7 +292,7 @@ class PinballAudioPlayer { path: Assets.sfx.cowMoo, duration: const Duration(seconds: 2), ), - PinballAudio.backgroundMusic: _LoopAudio( + PinballAudio.backgroundMusic: _SingleLoopAudio( preCacheSingleAudio: _preCacheSingleAudio, loopSingleAudio: _loopSingleAudio, path: Assets.music.background, diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index df21b1ad..e7592b8f 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -447,6 +447,18 @@ void main() { .onCall('packages/pinball_audio/${Assets.music.background}'), ).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}'), + ).called(1); + }); }); test( diff --git a/packages/pinball_components/assets/images/display_arrows/arrow_left.png b/packages/pinball_components/assets/images/display_arrows/arrow_left.png new file mode 100644 index 00000000..e851bef4 Binary files /dev/null and b/packages/pinball_components/assets/images/display_arrows/arrow_left.png differ diff --git a/packages/pinball_components/assets/images/display_arrows/arrow_right.png b/packages/pinball_components/assets/images/display_arrows/arrow_right.png new file mode 100644 index 00000000..c8d5d2f2 Binary files /dev/null and b/packages/pinball_components/assets/images/display_arrows/arrow_right.png differ diff --git a/packages/pinball_components/assets/images/google_rollover/left/decal.png b/packages/pinball_components/assets/images/google_rollover/left/decal.png new file mode 100644 index 00000000..da503131 Binary files /dev/null and b/packages/pinball_components/assets/images/google_rollover/left/decal.png differ diff --git a/packages/pinball_components/assets/images/google_rollover/left/pin.png b/packages/pinball_components/assets/images/google_rollover/left/pin.png new file mode 100644 index 00000000..50a95a6b Binary files /dev/null and b/packages/pinball_components/assets/images/google_rollover/left/pin.png differ diff --git a/packages/pinball_components/assets/images/google_rollover/right/decal.png b/packages/pinball_components/assets/images/google_rollover/right/decal.png new file mode 100644 index 00000000..68496caf Binary files /dev/null and b/packages/pinball_components/assets/images/google_rollover/right/decal.png differ diff --git a/packages/pinball_components/assets/images/google_rollover/right/pin.png b/packages/pinball_components/assets/images/google_rollover/right/pin.png new file mode 100644 index 00000000..19bba084 Binary files /dev/null and b/packages/pinball_components/assets/images/google_rollover/right/pin.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index f233596c..19b1571a 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -23,12 +23,17 @@ class $AssetsImagesGen { $AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); + $AssetsImagesDisplayArrowsGen get displayArrows => + const $AssetsImagesDisplayArrowsGen(); + /// File path: assets/images/error_background.png AssetGenImage get errorBackground => const AssetGenImage('assets/images/error_background.png'); $AssetsImagesFlapperGen get flapper => const $AssetsImagesFlapperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); + $AssetsImagesGoogleRolloverGen get googleRollover => + const $AssetsImagesGoogleRolloverGen(); $AssetsImagesGoogleWordGen get googleWord => const $AssetsImagesGoogleWordGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); @@ -140,6 +145,15 @@ class $AssetsImagesDinoGen { 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 { const $AssetsImagesFlapperGen(); @@ -168,6 +182,15 @@ class $AssetsImagesFlipperGen { const AssetGenImage('assets/images/flipper/right.png'); } +class $AssetsImagesGoogleRolloverGen { + const $AssetsImagesGoogleRolloverGen(); + + $AssetsImagesGoogleRolloverLeftGen get left => + const $AssetsImagesGoogleRolloverLeftGen(); + $AssetsImagesGoogleRolloverRightGen get right => + const $AssetsImagesGoogleRolloverRightGen(); +} + class $AssetsImagesGoogleWordGen { const $AssetsImagesGoogleWordGen(); @@ -422,6 +445,24 @@ class $AssetsImagesDinoAnimatronicGen { 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 { const $AssetsImagesGoogleWordLetter1Gen(); diff --git a/packages/pinball_components/lib/src/components/arrow_icon.dart b/packages/pinball_components/lib/src/components/arrow_icon.dart new file mode 100644 index 00000000..0dc33b10 --- /dev/null +++ b/packages/pinball_components/lib/src/components/arrow_icon.dart @@ -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 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; + } +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 75ba12dc..1116ee88 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -2,6 +2,7 @@ export 'android_animatronic.dart'; export 'android_bumper/android_bumper.dart'; export 'android_spaceship/android_spaceship.dart'; export 'arcade_background/arcade_background.dart'; +export 'arrow_icon.dart'; export 'ball/ball.dart'; export 'baseboard.dart'; export 'board_background_sprite_component.dart'; @@ -16,7 +17,9 @@ export 'dino_walls.dart'; export 'error_component.dart'; export 'flapper/flapper.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 'joint_anchor.dart'; export 'kicker/kicker.dart'; diff --git a/packages/pinball_components/lib/src/components/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter.dart new file mode 100644 index 00000000..1c63f7ff --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter.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 + with HasGameRef, FlameBlocListenable { + /// {@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 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() + .state + .letterSpriteStates[_index]; + size = sprites[current]!.originalSize / 10; + } +} + +final _spritePaths = >[ + { + 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, + }, +]; diff --git a/packages/pinball_components/lib/src/components/google_letter/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/google_letter/behaviors/behaviors.dart deleted file mode 100644 index df54c1f4..00000000 --- a/packages/pinball_components/lib/src/components/google_letter/behaviors/behaviors.dart +++ /dev/null @@ -1 +0,0 @@ -export 'google_letter_ball_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart deleted file mode 100644 index 99b15702..00000000 --- a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:bloc/bloc.dart'; - -part 'google_letter_state.dart'; - -class GoogleLetterCubit extends Cubit { - GoogleLetterCubit() : super(GoogleLetterState.dimmed); - - void onBallContacted() { - emit(GoogleLetterState.lit); - } - - void onReset() { - emit(GoogleLetterState.dimmed); - } -} diff --git a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart deleted file mode 100644 index 12c7edd0..00000000 --- a/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart +++ /dev/null @@ -1,6 +0,0 @@ -part of 'google_letter_cubit.dart'; - -enum GoogleLetterState { - lit, - dimmed, -} diff --git a/packages/pinball_components/lib/src/components/google_letter/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart deleted file mode 100644 index 9d678e30..00000000 --- a/packages/pinball_components/lib/src/components/google_letter/google_letter.dart +++ /dev/null @@ -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 = >[ - { - 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? children, - }) : this._( - index, - bloc: GoogleLetterCubit(), - children: children, - ); - - GoogleLetter._( - int index, { - required this.bloc, - Iterable? 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 - with HasGameRef, ParentIsA { - _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 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; - } -} diff --git a/packages/pinball_components/lib/src/components/google_rollover/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/google_rollover/behaviors/behaviors.dart new file mode 100644 index 00000000..0cb1ea1a --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_rollover/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'google_rollover_ball_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/google_letter/behaviors/google_letter_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/google_rollover/behaviors/google_rollover_ball_contact_behavior.dart similarity index 53% rename from packages/pinball_components/lib/src/components/google_letter/behaviors/google_letter_ball_contact_behavior.dart rename to packages/pinball_components/lib/src/components/google_rollover/behaviors/google_rollover_ball_contact_behavior.dart index 84a210ef..bef08bb0 100644 --- a/packages/pinball_components/lib/src/components/google_letter/behaviors/google_letter_ball_contact_behavior.dart +++ b/packages/pinball_components/lib/src/components/google_rollover/behaviors/google_rollover_ball_contact_behavior.dart @@ -1,12 +1,15 @@ +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -class GoogleLetterBallContactBehavior extends ContactBehavior { +class GoogleRolloverBallContactBehavior + extends ContactBehavior { @override void beginContact(Object other, Contact contact) { super.beginContact(other, contact); if (other is! Ball) return; - parent.bloc.onBallContacted(); + readBloc().onRolloverContacted(); + parent.firstChild()?.playing = true; } } diff --git a/packages/pinball_components/lib/src/components/google_rollover/google_rollover.dart b/packages/pinball_components/lib/src/components/google_rollover/google_rollover.dart new file mode 100644 index 00000000..e04e9778 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_rollover/google_rollover.dart @@ -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? 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 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 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; + }; + } +} diff --git a/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart b/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart new file mode 100644 index 00000000..197771d6 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart @@ -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 { + 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; + } +} diff --git a/packages/pinball_components/lib/src/components/google_word/cubit/google_word_state.dart b/packages/pinball_components/lib/src/components/google_word/cubit/google_word_state.dart new file mode 100644 index 00000000..a1ee2786 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_word/cubit/google_word_state.dart @@ -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 letterSpriteStates; + + @override + List get props => [...letterSpriteStates.values]; +} diff --git a/packages/pinball_components/lib/src/components/google_word/google_word.dart b/packages/pinball_components/lib/src/components/google_word/google_word.dart new file mode 100644 index 00000000..72126d2c --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_word/google_word.dart @@ -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), + ], + ); +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 3301a0fc..fe52f8b8 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -81,6 +81,8 @@ flutter: - assets/images/google_word/letter4/ - assets/images/google_word/letter5/ - assets/images/google_word/letter6/ + - assets/images/google_rollover/left/ + - assets/images/google_rollover/right/ - assets/images/signpost/ - assets/images/multiball/ - assets/images/multiplier/x2/ @@ -93,6 +95,7 @@ flutter: - assets/images/backbox/button/ - assets/images/flapper/ - assets/images/skill_shot/ + - assets/images/display_arrows/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 714bbee5..fb948a89 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -21,6 +21,7 @@ void main() { addScoreStories(dashbook); addMultiballStories(dashbook); addMultipliersStories(dashbook); + addArrowIconStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/arrow_icon/arrow_icon_game.dart b/packages/pinball_components/sandbox/lib/stories/arrow_icon/arrow_icon_game.dart new file mode 100644 index 00000000..23af63b0 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/arrow_icon/arrow_icon_game.dart @@ -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 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: () {}, + ), + ); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/arrow_icon/stories.dart b/packages/pinball_components/sandbox/lib/stories/arrow_icon/stories.dart new file mode 100644 index 00000000..05a8a8ff --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/arrow_icon/stories.dart @@ -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(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 0a514eb9..8a165693 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,4 +1,5 @@ export 'android_acres/stories.dart'; +export 'arrow_icon/stories.dart'; export 'ball/stories.dart'; export 'bottom_group/stories.dart'; export 'boundaries/stories.dart'; diff --git a/packages/pinball_components/test/helpers/test_game.dart b/packages/pinball_components/test/helpers/test_game.dart index 1f8b9ee6..57c7961c 100644 --- a/packages/pinball_components/test/helpers/test_game.dart +++ b/packages/pinball_components/test/helpers/test_game.dart @@ -1,3 +1,4 @@ +import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; @@ -20,3 +21,7 @@ class TestGame extends Forge2DGame { class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents { KeyboardTestGame([List? assets]) : super(assets); } + +class TappablesTestGame extends TestGame with HasTappables { + TappablesTestGame([List? assets]) : super(assets); +} diff --git a/packages/pinball_components/test/src/components/arrow_icon_test.dart b/packages/pinball_components/test/src/components/arrow_icon_test.dart new file mode 100644 index 00000000..c8a1c5aa --- /dev/null +++ b/packages/pinball_components/test/src/components/arrow_icon_test.dart @@ -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().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(), + 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(), + matchesGoldenFile('golden/arrow_icon_right.png'), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/golden/arrow_icon_left.png b/packages/pinball_components/test/src/components/golden/arrow_icon_left.png new file mode 100644 index 00000000..dab87d41 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/arrow_icon_left.png differ diff --git a/packages/pinball_components/test/src/components/golden/arrow_icon_right.png b/packages/pinball_components/test/src/components/golden/arrow_icon_right.png new file mode 100644 index 00000000..185e9a9a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/arrow_icon_right.png differ diff --git a/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart deleted file mode 100644 index 6a6fd437..00000000 --- a/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart +++ /dev/null @@ -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(), - ); - }); - - flameTester.test( - 'beginContact emits onBallContacted when contacts with a ball', - (game) async { - final behavior = GoogleLetterBallContactBehavior(); - final bloc = _MockGoogleLetterCubit(); - whenListen( - bloc, - const Stream.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); - }, - ); - }, - ); -} diff --git a/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart b/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart deleted file mode 100644 index 812e86de..00000000 --- a/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart +++ /dev/null @@ -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( - 'onBallContacted emits active', - build: GoogleLetterCubit.new, - act: (bloc) => bloc.onBallContacted(), - expect: () => [GoogleLetterState.lit], - ); - - blocTest( - 'onReset emits inactive', - build: GoogleLetterCubit.new, - act: (bloc) => bloc.onReset(), - expect: () => [GoogleLetterState.dimmed], - ); - }, - ); -} diff --git a/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart deleted file mode 100644 index 1ef5e7a7..00000000 --- a/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart +++ /dev/null @@ -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())); - expect(() => GoogleLetter(6), throwsA(isA())); - }); - - flameTester.test('closes bloc when removed', (game) async { - final bloc = _MockGoogleLetterCubit(); - whenListen( - bloc, - const Stream.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() - .single, - isNotNull, - ); - }); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/google_letter_test.dart b/packages/pinball_components/test/src/components/google_letter_test.dart new file mode 100644 index 00000000..7deea645 --- /dev/null +++ b/packages/pinball_components/test/src/components/google_letter_test.dart @@ -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 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 pump(GoogleLetter child) async { + await ensureAdd( + FlameBlocProvider.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()); + }); + + 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())); + expect(() => GoogleLetter(6), throwsA(isA())); + }); + + 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); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/google_rollover/behaviors/google_rollover_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/google_rollover/behaviors/google_rollover_ball_contact_behavior_test.dart new file mode 100644 index 00000000..9d2e6fdf --- /dev/null +++ b/packages/pinball_components/test/src/components/google_rollover/behaviors/google_rollover_ball_contact_behavior_test.dart @@ -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 onLoad() async { + images.prefix = ''; + await images.loadAll([ + Assets.images.googleRollover.left.decal.keyName, + Assets.images.googleRollover.left.pin.keyName, + ]); + } + + Future pump( + GoogleRollover child, { + GoogleWordCubit? bloc, + }) async { + // Not needed once https://github.com/flame-engine/flame/issues/1607 + // is fixed + await onLoad(); + await ensureAdd( + FlameBlocProvider.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(), + ); + }); + + 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()!.playing, + isTrue, + ); + verify(bloc.onRolloverContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/google_rollover/google_rollover_test.dart b/packages/pinball_components/test/src/components/google_rollover/google_rollover_test.dart new file mode 100644 index 00000000..199803a0 --- /dev/null +++ b/packages/pinball_components/test/src/components/google_rollover/google_rollover_test.dart @@ -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(), + ); + }); + + 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() + .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()!; + + pinSpriteAnimationComponent.playing = true; + game.update( + pinSpriteAnimationComponent.animation!.totalDuration() + 0.1, + ); + + expect(pinSpriteAnimationComponent.playing, isFalse); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart b/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart new file mode 100644 index 00000000..08acfae8 --- /dev/null +++ b/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart @@ -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( + '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( + 'onBonusAwarded emits initial state', + build: GoogleWordCubit.new, + act: (bloc) => bloc.onBonusAwarded(), + expect: () => [GoogleWordState.initial()], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/google_word/cubit/google_word_state_test.dart b/packages/pinball_components/test/src/components/google_word/cubit/google_word_state_test.dart new file mode 100644 index 00000000..6195c785 --- /dev/null +++ b/packages/pinball_components/test/src/components/google_word/cubit/google_word_state_test.dart @@ -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)); + }); + }); + }); +} diff --git a/test/game/components/google_word/google_word_test.dart b/packages/pinball_components/test/src/components/google_word/google_word_test.dart similarity index 65% rename from test/game/components/google_word/google_word_test.dart rename to packages/pinball_components/test/src/components/google_word/google_word_test.dart index c0258281..daee7d37 100644 --- a/test/game/components/google_word/google_word_test.dart +++ b/packages/pinball_components/test/src/components/google_word/google_word_test.dart @@ -4,8 +4,6 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/components/google_word/behaviors/behaviors.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; class _TestGame extends Forge2DGame { @@ -28,10 +26,10 @@ class _TestGame extends Forge2DGame { ]); } - Future pump(GoogleWord child, {GameBloc? gameBloc}) { - return ensureAdd( - FlameBlocProvider.value( - value: gameBloc ?? GameBloc(), + Future pump(GoogleWord child) async { + await ensureAdd( + FlameBlocProvider.value( + value: GoogleWordCubit(), children: [child], ), ); @@ -44,25 +42,21 @@ void main() { final flameTester = FlameTester(_TestGame.new); group('GoogleWord', () { + test('can be instantiated', () { + expect(GoogleWord(position: Vector2.zero()), isA()); + }); + flameTester.test( - 'loads the letters correctly', + 'loads letters correctly', (game) async { - const word = 'Google'; final googleWord = GoogleWord(position: Vector2.zero()); await game.pump(googleWord); - final letters = googleWord.children.whereType(); - expect(letters.length, equals(word.length)); + expect( + googleWord.children.whereType().length, + equals(6), + ); }, ); - - flameTester.test('adds a GoogleWordBonusBehavior', (game) async { - final googleWord = GoogleWord(position: Vector2.zero()); - await game.pump(googleWord); - expect( - googleWord.children.whereType().single, - isNotNull, - ); - }); }); } diff --git a/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_state_test.dart b/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_state_test.dart index ee6e3e0d..b5aca68c 100644 --- a/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_state_test.dart +++ b/packages/pinball_components/test/src/components/skill_shot/cubit/skill_shot_state_test.dart @@ -31,7 +31,7 @@ void main() { ); }); - test('initial is idle with mouth closed', () { + test('initial is dimmed and not blinking', () { const initialState = SkillShotState( spriteState: SkillShotSpriteState.dimmed, isBlinking: false, @@ -45,13 +45,13 @@ void main() { 'copies correctly ' 'when no argument specified', () { - const chromeDinoState = SkillShotState( + const skillShotState = SkillShotState( spriteState: SkillShotSpriteState.lit, isBlinking: true, ); expect( - chromeDinoState.copyWith(), - equals(chromeDinoState), + skillShotState.copyWith(), + equals(skillShotState), ); }, ); @@ -60,7 +60,7 @@ void main() { 'copies correctly ' 'when all arguments specified', () { - const chromeDinoState = SkillShotState( + const skillShotState = SkillShotState( spriteState: SkillShotSpriteState.lit, isBlinking: true, ); @@ -68,10 +68,10 @@ void main() { spriteState: SkillShotSpriteState.dimmed, isBlinking: false, ); - expect(chromeDinoState, isNot(equals(otherSkillShotState))); + expect(skillShotState, isNot(equals(otherSkillShotState))); expect( - chromeDinoState.copyWith( + skillShotState.copyWith( spriteState: SkillShotSpriteState.dimmed, isBlinking: false, ), diff --git a/packages/pinball_theme/assets/images/android/background.jpg b/packages/pinball_theme/assets/images/android/background.jpg index bb1a6992..afe2e3c6 100644 Binary files a/packages/pinball_theme/assets/images/android/background.jpg and b/packages/pinball_theme/assets/images/android/background.jpg differ diff --git a/packages/pinball_theme/assets/images/dash/background.jpg b/packages/pinball_theme/assets/images/dash/background.jpg index 667454f4..0b70a795 100644 Binary files a/packages/pinball_theme/assets/images/dash/background.jpg and b/packages/pinball_theme/assets/images/dash/background.jpg differ diff --git a/packages/pinball_theme/assets/images/dino/background.jpg b/packages/pinball_theme/assets/images/dino/background.jpg index 6cf0626e..35deff95 100644 Binary files a/packages/pinball_theme/assets/images/dino/background.jpg and b/packages/pinball_theme/assets/images/dino/background.jpg differ diff --git a/packages/pinball_theme/assets/images/sparky/background.jpg b/packages/pinball_theme/assets/images/sparky/background.jpg index 08ad2dba..9c4fdfc6 100644 Binary files a/packages/pinball_theme/assets/images/sparky/background.jpg and b/packages/pinball_theme/assets/images/sparky/background.jpg differ diff --git a/test/game/components/android_acres/android_acres_test.dart b/test/game/components/android_acres/android_acres_test.dart index 5c750818..14d3d69e 100644 --- a/test/game/components/android_acres/android_acres_test.dart +++ b/test/game/components/android_acres/android_acres_test.dart @@ -62,7 +62,7 @@ void main() { group('loads', () { flameTester.test( - 'an AndroidSpaceship', + 'an AndroidSpaceship', (game) async { await game.pump(AndroidAcres()); expect( diff --git a/test/game/components/backbox/backbox_test.dart b/test/game/components/backbox/backbox_test.dart index 61b0907e..16238e76 100644 --- a/test/game/components/backbox/backbox_test.dart +++ b/test/game/components/backbox/backbox_test.dart @@ -42,14 +42,19 @@ class _TestGame extends Forge2DGame Assets.images.backbox.button.facebook.keyName, Assets.images.backbox.button.twitter.keyName, Assets.images.backbox.displayTitleDecoration.keyName, + Assets.images.displayArrows.arrowLeft.keyName, + Assets.images.displayArrows.arrowRight.keyName, ]); } Future pump( Backbox component, { PlatformHelper? platformHelper, - }) { - return ensureAdd( + }) async { + // Not needed once https://github.com/flame-engine/flame/issues/1607 + // is fixed + await onLoad(); + await ensureAdd( FlameBlocProvider.value( value: GameBloc(), children: [ diff --git a/test/game/components/backbox/displays/leaderboard_display_test.dart b/test/game/components/backbox/displays/leaderboard_display_test.dart index 263222fc..46fe6cdc 100644 --- a/test/game/components/backbox/displays/leaderboard_display_test.dart +++ b/test/game/components/backbox/displays/leaderboard_display_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: cascade_invocations import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_test/flame_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:pinball/game/components/backbox/displays/leaderboard_display.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import 'package:pinball_theme/pinball_theme.dart'; +import 'package:pinball_theme/pinball_theme.dart' hide Assets; class _MockAppLocalizations extends Mock implements AppLocalizations { @override @@ -22,12 +24,16 @@ class _MockAppLocalizations extends Mock implements AppLocalizations { String get name => 'name'; } -class _TestGame extends Forge2DGame { +class _TestGame extends Forge2DGame with HasTappables { @override Future onLoad() async { await super.onLoad(); 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 pump(LeaderboardDisplay component) { @@ -40,6 +46,59 @@ class _TestGame extends Forge2DGame { } } +const leaderboard = [ + LeaderboardEntryData( + playerInitials: 'AAA', + score: 123, + character: CharacterType.android, + ), + LeaderboardEntryData( + playerInitials: 'BBB', + score: 1234, + character: CharacterType.android, + ), + LeaderboardEntryData( + playerInitials: 'CCC', + score: 12345, + character: CharacterType.android, + ), + LeaderboardEntryData( + playerInitials: 'DDD', + score: 12346, + 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(); @@ -57,43 +116,20 @@ void main() { expect(textComponents[2].text, equals('name')); }); - flameTester.test('renders the entries', (game) async { - await game.pump( - LeaderboardDisplay( - entries: const [ - LeaderboardEntryData( - playerInitials: 'AAA', - score: 123, - character: CharacterType.android, - ), - LeaderboardEntryData( - playerInitials: 'BBB', - score: 1234, - character: CharacterType.android, - ), - LeaderboardEntryData( - playerInitials: 'CCC', - score: 12345, - character: CharacterType.android, - ), - LeaderboardEntryData( - playerInitials: 'DDD', - score: 12346, - character: CharacterType.android, - ), - ], - ), - ); + flameTester.test('renders the first 5 entries', (game) async { + await game.pump(LeaderboardDisplay(entries: leaderboard)); for (final text in [ 'AAA', 'BBB', 'CCC', 'DDD', + 'EEE', '1st', '2nd', '3rd', - '4th' + '4th', + '5th', ]) { expect( 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() + .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() + .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() + .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() + .where((textComponent) => textComponent.text == text) + .length, + equals(1), + ); + } + + arrow = game + .descendants() + .whereType() + .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() + .where((textComponent) => textComponent.text == text) + .length, + equals(1), + ); + } + }, + ); }); } diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart index cc1729b8..c1ca9b3e 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: cascade_invocations import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; @@ -16,7 +17,7 @@ import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:share_repository/share_repository.dart'; -class _TestGame extends Forge2DGame { +class _TestGame extends Forge2DGame with HasTappables { @override Future onLoad() async { images.prefix = ''; @@ -25,6 +26,8 @@ class _TestGame extends Forge2DGame { const theme.DashTheme().leaderboardIcon.keyName, Assets.images.backbox.marquee.keyName, Assets.images.backbox.displayDivider.keyName, + Assets.images.displayArrows.arrowLeft.keyName, + Assets.images.displayArrows.arrowRight.keyName, ], ); } diff --git a/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart b/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart new file mode 100644 index 00000000..3d8d2b39 --- /dev/null +++ b/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart @@ -0,0 +1,120 @@ +// 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/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 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 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.value( + value: gameBloc, + ), + FlameBlocProvider.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(); + + 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); + }, + ); + }); +} diff --git a/test/game/components/google_gallery/google_gallery_test.dart b/test/game/components/google_gallery/google_gallery_test.dart new file mode 100644 index 00000000..9551285f --- /dev/null +++ b/test/game/components/google_gallery/google_gallery_test.dart @@ -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 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 pump(GoogleGallery child) async { + await ensureAdd( + FlameBlocProvider.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().length, + equals(2), + ); + }, + ); + + flameTester.test( + 'a GoogleWord', + (game) async { + await game.pump(GoogleGallery()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + }); + + group('adds', () { + flameTester.test( + 'ScoringContactBehavior to GoogleRollovers', + (game) async { + await game.pump(GoogleGallery()); + + game.descendants().whereType().forEach( + (rollover) => expect( + rollover.firstChild(), + isNotNull, + ), + ); + }, + ); + + flameTester.test('a GoogleWordBonusBehavior', (game) async { + final component = GoogleGallery(); + await game.pump(component); + expect( + component.descendants().whereType().single, + isNotNull, + ); + }); + }); + }); +} diff --git a/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart b/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart deleted file mode 100644 index e23c1fd2..00000000 --- a/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart +++ /dev/null @@ -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 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 pump(GoogleWord child, {required GameBloc gameBloc}) async { - await ensureAdd( - FlameBlocProvider.value( - value: gameBloc, - children: [ - FlameProvider.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); - }, - ); - }); -} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 71c8f1da..289fb4fa 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -192,11 +192,11 @@ void main() { ); flameTester.test( - 'one GoogleWord', + 'one GoogleGallery', (game) async { await game.ready(); expect( - game.descendants().whereType().length, + game.descendants().whereType().length, equals(1), ); }, diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index f10c5f5b..8980c72d 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -29,7 +29,7 @@ void main() { expect(find.text('Play'), findsOneWidget); }); - testWidgets('adds PlayTapped event to StartGameBloc when taped', + testWidgets('adds PlayTapped event to StartGameBloc when tapped', (tester) async { await tester.pumpApp( const PlayButtonOverlay(),