diff --git a/.github/workflows/authentication_repository.yaml b/.github/workflows/authentication_repository.yaml new file mode 100644 index 00000000..74c81d10 --- /dev/null +++ b/.github/workflows/authentication_repository.yaml @@ -0,0 +1,22 @@ +name: authentication_repository + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/authentication_repository/**" + - ".github/workflows/authentication_repository.yaml" + + pull_request: + paths: + - "packages/authentication_repository/**" + - ".github/workflows/authentication_repository.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/authentication_repository diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 00000000..c8a41a4b --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,25 @@ +name: deploy + +on: + push: + branches: + - main + +jobs: + deploy-dev: + runs-on: ubuntu-latest + name: Deploy Development + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter packages get + - run: flutter build web --target lib/main_development.dart --web-renderer canvaskit --release + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}" + channelId: live + projectId: pinball-dev + target: ashehwkdkdjruejdnensjsjdne \ No newline at end of file diff --git a/.github/workflows/geometry.yaml b/.github/workflows/geometry.yaml index 8bf55107..ccd41914 100644 --- a/.github/workflows/geometry.yaml +++ b/.github/workflows/geometry.yaml @@ -1,5 +1,9 @@ name: geometry +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/leaderboard_repository.yaml b/.github/workflows/leaderboard_repository.yaml index 6eddf283..327f70b3 100644 --- a/.github/workflows/leaderboard_repository.yaml +++ b/.github/workflows/leaderboard_repository.yaml @@ -1,5 +1,9 @@ name: leaderboard_repository +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f3e3fd99..8ed906a2 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,5 +1,9 @@ name: pinball +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: [pull_request, push] jobs: diff --git a/.github/workflows/pinball_audio.yaml b/.github/workflows/pinball_audio.yaml index 7a43413a..6ba3adde 100644 --- a/.github/workflows/pinball_audio.yaml +++ b/.github/workflows/pinball_audio.yaml @@ -1,5 +1,9 @@ name: pinball_audio +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/pinball_components.yaml b/.github/workflows/pinball_components.yaml index d75553f5..19f13044 100644 --- a/.github/workflows/pinball_components.yaml +++ b/.github/workflows/pinball_components.yaml @@ -1,5 +1,9 @@ name: pinball_components +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/pinball_flame.yaml b/.github/workflows/pinball_flame.yaml index 2263bb5a..297b792e 100644 --- a/.github/workflows/pinball_flame.yaml +++ b/.github/workflows/pinball_flame.yaml @@ -1,5 +1,9 @@ name: pinball_flame +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/pinball_theme.yaml b/.github/workflows/pinball_theme.yaml index 83206de5..15280761 100644 --- a/.github/workflows/pinball_theme.yaml +++ b/.github/workflows/pinball_theme.yaml @@ -1,5 +1,9 @@ name: pinball_theme +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: paths: diff --git a/.github/workflows/pinball_ui.yaml b/.github/workflows/pinball_ui.yaml new file mode 100644 index 00000000..98643ffa --- /dev/null +++ b/.github/workflows/pinball_ui.yaml @@ -0,0 +1,23 @@ +name: pinball_ui + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/pinball_ui/**" + - ".github/workflows/pinball_ui.yaml" + + pull_request: + paths: + - "packages/pinball_ui/**" + - ".github/workflows/pinball_ui.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/pinball_ui + coverage_excludes: "lib/gen/*.dart" diff --git a/.github/workflows/platform_helper.yaml b/.github/workflows/platform_helper.yaml new file mode 100644 index 00000000..0c1c61e7 --- /dev/null +++ b/.github/workflows/platform_helper.yaml @@ -0,0 +1,23 @@ +name: platform_helper + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/platform_helper/**" + - ".github/workflows/platform_helper.yaml" + + pull_request: + paths: + - "packages/platform_helper/**" + - ".github/workflows/platform_helper.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/platform_helper + coverage_excludes: "lib/gen/*.dart" diff --git a/assets/images/bonus_animation/android.png b/assets/images/bonus_animation/android_spaceship.png similarity index 100% rename from assets/images/bonus_animation/android.png rename to assets/images/bonus_animation/android_spaceship.png diff --git a/assets/images/bonus_animation/dino.png b/assets/images/bonus_animation/dino_chomp.png similarity index 100% rename from assets/images/bonus_animation/dino.png rename to assets/images/bonus_animation/dino_chomp.png diff --git a/assets/images/bonus_animation/google.png b/assets/images/bonus_animation/google_word.png similarity index 100% rename from assets/images/bonus_animation/google.png rename to assets/images/bonus_animation/google_word.png diff --git a/assets/images/components/key.png b/assets/images/components/key.png new file mode 100644 index 00000000..588c2b89 Binary files /dev/null and b/assets/images/components/key.png differ diff --git a/assets/images/components/space.png b/assets/images/components/space.png new file mode 100644 index 00000000..9949b383 Binary files /dev/null and b/assets/images/components/space.png differ diff --git a/assets/images/score/mini_score_background.png b/assets/images/score/mini_score_background.png new file mode 100644 index 00000000..781f7349 Binary files /dev/null and b/assets/images/score/mini_score_background.png differ diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 2780b608..528954a6 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -7,24 +7,29 @@ // ignore_for_file: public_member_api_docs +import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_ui/pinball_ui.dart'; class App extends StatelessWidget { const App({ Key? key, + required AuthenticationRepository authenticationRepository, required LeaderboardRepository leaderboardRepository, required PinballAudio pinballAudio, - }) : _leaderboardRepository = leaderboardRepository, + }) : _authenticationRepository = authenticationRepository, + _leaderboardRepository = leaderboardRepository, _pinballAudio = pinballAudio, super(key: key); + final AuthenticationRepository _authenticationRepository; final LeaderboardRepository _leaderboardRepository; final PinballAudio _pinballAudio; @@ -32,19 +37,21 @@ class App extends StatelessWidget { Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ + RepositoryProvider.value(value: _authenticationRepository), RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _pinballAudio), ], child: BlocProvider( - create: (context) => ThemeCubit(), - child: const MaterialApp( + create: (context) => CharacterThemeCubit(), + child: MaterialApp( title: 'I/O Pinball', - localizationsDelegates: [ + theme: PinballTheme.standard, + localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, - home: PinballGamePage(), + home: const PinballGamePage(), ), ), ); diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index c0fa9240..bbd87f0c 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -12,6 +12,7 @@ import 'dart:developer'; import 'package:bloc/bloc.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/widgets.dart'; class AppBlocObserver extends BlocObserver { @@ -28,9 +29,12 @@ class AppBlocObserver extends BlocObserver { } } -Future bootstrap( - Future Function(FirebaseFirestore firestore) builder, -) async { +typedef BootstrapBuilder = Future Function( + FirebaseFirestore firestore, + FirebaseAuth firebaseAuth, +); + +Future bootstrap(BootstrapBuilder builder) async { WidgetsFlutterBinding.ensureInitialized(); FlutterError.onError = (details) { log(details.exceptionAsString(), stackTrace: details.stack); @@ -39,7 +43,12 @@ Future bootstrap( await runZonedGuarded( () async { await BlocOverrides.runZoned( - () async => runApp(await builder(FirebaseFirestore.instance)), + () async => runApp( + await builder( + FirebaseFirestore.instance, + FirebaseAuth.instance, + ), + ), blocObserver: AppBlocObserver(), ); }, diff --git a/lib/footer/footer.dart b/lib/footer/footer.dart new file mode 100644 index 00000000..df3dbd2f --- /dev/null +++ b/lib/footer/footer.dart @@ -0,0 +1,76 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template footer} +/// Footer widget with links to the main tech stack. +/// {@endtemplate} +class Footer extends StatelessWidget { + /// {@macro footer} + const Footer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(50, 0, 50, 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + _MadeWithFlutterAndFirebase(), + _GoogleIO(), + ], + ), + ); + } +} + +class _GoogleIO extends StatelessWidget { + const _GoogleIO({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final theme = Theme.of(context); + return Text( + l10n.footerGoogleIOText, + style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white), + ); + } +} + +class _MadeWithFlutterAndFirebase extends StatelessWidget { + const _MadeWithFlutterAndFirebase({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final theme = Theme.of(context); + return RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: l10n.footerMadeWithText, + style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white), + children: [ + TextSpan( + text: l10n.footerFlutterLinkText, + recognizer: TapGestureRecognizer() + ..onTap = () => openLink('https://flutter.dev'), + style: const TextStyle( + decoration: TextDecoration.underline, + ), + ), + const TextSpan(text: ' & '), + TextSpan( + text: l10n.footerFirebaseLinkText, + recognizer: TapGestureRecognizer() + ..onTap = () => openLink('https://firebase.google.com'), + style: const TextStyle( + decoration: TextDecoration.underline, + ), + ), + ], + ), + ); + } +} diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 4ba63092..49f40d1f 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -1,5 +1,5 @@ // ignore_for_file: public_member_api_docs - +import 'dart:math' as math; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; @@ -9,19 +9,41 @@ part 'game_state.dart'; class GameBloc extends Bloc { GameBloc() : super(const GameState.initial()) { - on(_onBallLost); + on(_onRoundLost); on(_onScored); + on(_onIncreasedMultiplier); on(_onBonusActivated); on(_onSparkyTurboChargeActivated); } - void _onBallLost(BallLost event, Emitter emit) { - emit(state.copyWith(balls: state.balls - 1)); + void _onRoundLost(RoundLost event, Emitter emit) { + final score = state.score * state.multiplier; + final roundsLeft = math.max(state.rounds - 1, 0); + + emit( + state.copyWith( + score: score, + multiplier: 1, + rounds: roundsLeft, + ), + ); } void _onScored(Scored event, Emitter emit) { if (!state.isGameOver) { - emit(state.copyWith(score: state.score + event.points)); + emit( + state.copyWith(score: state.score + event.points), + ); + } + } + + void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) { + if (!state.isGameOver) { + emit( + state.copyWith( + multiplier: math.min(state.multiplier + 1, 6), + ), + ); } } diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index bbb89028..c81ce526 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -7,12 +7,12 @@ abstract class GameEvent extends Equatable { const GameEvent(); } -/// {@template ball_lost_game_event} -/// Event added when a user drops a ball off the screen. +/// {@template round_lost_game_event} +/// Event added when a user drops all balls off the screen and loses a round. /// {@endtemplate} -class BallLost extends GameEvent { - /// {@macro ball_lost_game_event} - const BallLost(); +class RoundLost extends GameEvent { + /// {@macro round_lost_game_event} + const RoundLost(); @override List get props => []; @@ -48,3 +48,14 @@ class SparkyTurboChargeActivated extends GameEvent { @override List get props => []; } + +/// {@template multiplier_increased_game_event} +/// Added when a multiplier is gained. +/// {@endtemplate} +class MultiplierIncreased extends GameEvent { + /// {@macro multiplier_increased_game_event} + const MultiplierIncreased(); + + @override + List get props => []; +} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index c57eedb4..4ce9042d 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -12,6 +12,12 @@ enum GameBonus { /// Bonus achieved when a ball enters Sparky's computer. sparkyTurboCharge, + + /// Bonus achieved when the ball goes in the dino mouth. + dinoChomp, + + /// Bonus achieved when a ball enters the android spaceship. + androidSpaceship, } /// {@template game_state} @@ -21,34 +27,42 @@ class GameState extends Equatable { /// {@macro game_state} const GameState({ required this.score, - required this.balls, + required this.multiplier, + required this.rounds, required this.bonusHistory, }) : assert(score >= 0, "Score can't be negative"), - assert(balls >= 0, "Number of balls can't be negative"); + assert(multiplier > 0, 'Multiplier must be greater than zero'), + assert(rounds >= 0, "Number of rounds can't be negative"); const GameState.initial() : score = 0, - balls = 3, + multiplier = 1, + rounds = 3, bonusHistory = const []; /// The current score of the game. final int score; - /// The number of balls left in the game. + /// The current multiplier for the score. + final int multiplier; + + /// The number of rounds left in the game. /// - /// When the number of balls is 0, the game is over. - final int balls; + /// When the number of rounds is 0, the game is over. + final int rounds; /// Holds the history of all the [GameBonus]es earned by the player during a /// PinballGame. final List bonusHistory; /// Determines when the game is over. - bool get isGameOver => balls == 0; + bool get isGameOver => rounds == 0; GameState copyWith({ int? score, + int? multiplier, int? balls, + int? rounds, List? bonusHistory, }) { assert( @@ -58,7 +72,8 @@ class GameState extends Equatable { return GameState( score: score ?? this.score, - balls: balls ?? this.balls, + multiplier: multiplier ?? this.multiplier, + rounds: rounds ?? this.rounds, bonusHistory: bonusHistory ?? this.bonusHistory, ); } @@ -66,7 +81,8 @@ class GameState extends Equatable { @override List get props => [ score, - balls, + multiplier, + rounds, bonusHistory, ]; } diff --git a/lib/game/components/alien_zone.dart b/lib/game/components/alien_zone.dart deleted file mode 100644 index 720c1180..00000000 --- a/lib/game/components/alien_zone.dart +++ /dev/null @@ -1,60 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template alien_zone} -/// Area positioned below [Spaceship] where the [Ball] -/// can bounce off [AlienBumper]s. -/// -/// When a [Ball] hits an [AlienBumper], the bumper animates. -/// {@endtemplate} -class AlienZone extends Component with HasGameRef { - /// {@macro alien_zone} - AlienZone(); - - @override - Future onLoad() async { - await super.onLoad(); - - gameRef.addContactCallback(AlienBumperBallContactCallback()); - - final lowerBumper = _AlienBumper.a() - ..initialPosition = Vector2(-32.52, -9.1); - final upperBumper = _AlienBumper.b() - ..initialPosition = Vector2(-22.89, -17.35); - - await addAll([ - lowerBumper, - upperBumper, - ]); - } -} - -// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D -// ContactCallback process is enhanced. -class _AlienBumper extends AlienBumper with ScorePoints { - _AlienBumper.a() : super.a(); - - _AlienBumper.b() : super.b(); - - @override - int get points => 20; -} - -/// Listens when a [Ball] bounces against an [AlienBumper]. -@visibleForTesting -class AlienBumperBallContactCallback - extends ContactCallback { - @override - void begin( - AlienBumper alienBumper, - Ball _, - Contact __, - ) { - alienBumper.animate(); - } -} diff --git a/lib/game/components/android_acres.dart b/lib/game/components/android_acres.dart new file mode 100644 index 00000000..489dc2e5 --- /dev/null +++ b/lib/game/components/android_acres.dart @@ -0,0 +1,36 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template android_acres} +/// Area positioned on the left side of the board containing the +/// [AndroidSpaceship], [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s. +/// {@endtemplate} +class AndroidAcres extends Component { + /// {@macro android_acres} + AndroidAcres() + : super( + children: [ + SpaceshipRamp(), + SpaceshipRail(), + AndroidSpaceship(position: Vector2(-26.5, -28.5)), + AndroidBumper.a( + children: [ + ScoringBehavior(points: 20000), + ], + )..initialPosition = Vector2(-25, 1.3), + AndroidBumper.b( + children: [ + ScoringBehavior(points: 20000), + ], + )..initialPosition = Vector2(-32.8, -9.2), + AndroidBumper.cow( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(-20.5, -13.8), + ], + ); +} diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart deleted file mode 100644 index 8ee4128f..00000000 --- a/lib/game/components/board.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template board} -/// The main flat surface of the [PinballGame]. -/// {endtemplate} -class Board extends Component { - /// {@macro board} - // TODO(alestiago): Make Board a Blueprint and sort out priorities. - Board() : super(priority: 1); - - @override - Future onLoad() async { - // TODO(allisonryan0002): add bottom group and flutter forest to pinball - // game directly. Then remove board. - final bottomGroup = _BottomGroup(); - - final flutterForest = FlutterForest(); - - // TODO(alestiago): adjust positioning to real design. - // TODO(alestiago): add dino in pinball game. - final dino = ChromeDino() - ..initialPosition = Vector2( - BoardDimensions.bounds.center.dx + 25, - BoardDimensions.bounds.center.dy - 10, - ); - - await addAll([ - bottomGroup, - dino, - flutterForest, - ]); - } -} - -/// {@template bottom_group} -/// Grouping of the board's bottom [Component]s. -/// -/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [Kicker]s. -/// {@endtemplate} -// TODO(alestiago): Consider renaming once entire Board is defined. -class _BottomGroup extends Component { - /// {@macro bottom_group} - _BottomGroup() : super(priority: RenderPriority.bottomGroup); - - @override - Future onLoad() async { - final rightSide = _BottomGroupSide( - side: BoardSide.right, - ); - final leftSide = _BottomGroupSide( - side: BoardSide.left, - ); - - await addAll([rightSide, leftSide]); - } -} - -/// {@template bottom_group_side} -/// Group with one side of [_BottomGroup]'s symmetric [Component]s. -/// -/// For example, [Flipper]s are symmetric components. -/// {@endtemplate} -class _BottomGroupSide extends Component { - /// {@macro bottom_group_side} - _BottomGroupSide({ - required BoardSide side, - }) : _side = side; - - final BoardSide _side; - - @override - Future onLoad() async { - final direction = _side.direction; - final centerXAdjustment = _side.isLeft ? 0 : -6.5; - - final flipper = ControlledFlipper( - side: _side, - )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); - final baseboard = Baseboard(side: _side) - ..initialPosition = Vector2( - (25.58 * direction) + centerXAdjustment, - 28.69, - ); - final kicker = Kicker( - side: _side, - )..initialPosition = Vector2( - (22.4 * direction) + centerXAdjustment, - 25, - ); - - await addAll([flipper, baseboard, kicker]); - } -} diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart new file mode 100644 index 00000000..b4a888f4 --- /dev/null +++ b/lib/game/components/bottom_group.dart @@ -0,0 +1,63 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template bottom_group} +/// Grouping of the board's symmetrical bottom [Component]s. +/// +/// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s. +/// {@endtemplate} +// TODO(allisonryan0002): Consider renaming. +class BottomGroup extends Component with ZIndex { + /// {@macro bottom_group} + BottomGroup() + : super( + children: [ + _BottomGroupSide(side: BoardSide.right), + _BottomGroupSide(side: BoardSide.left), + ], + ) { + zIndex = ZIndexes.bottomGroup; + } +} + +/// {@template bottom_group_side} +/// Group with one side of [BottomGroup]'s symmetric [Component]s. +/// +/// For example, [Flipper]s are symmetric components. +/// {@endtemplate} +class _BottomGroupSide extends Component { + /// {@macro bottom_group_side} + _BottomGroupSide({ + required BoardSide side, + }) : _side = side; + + final BoardSide _side; + + @override + Future onLoad() async { + final direction = _side.direction; + final centerXAdjustment = _side.isLeft ? 0 : -6.66; + + final flipper = ControlledFlipper( + side: _side, + )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); + final baseboard = Baseboard(side: _side) + ..initialPosition = Vector2( + (25.58 * direction) + centerXAdjustment, + 28.71, + ); + final kicker = Kicker( + side: _side, + children: [ + ScoringBehavior(points: 5000)..applyTo(['bouncy_edge']), + ], + )..initialPosition = Vector2( + (22.64 * direction) + centerXAdjustment, + 25.1, + ); + + await addAll([flipper, baseboard, kicker]); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 7d4b23f7..5af4efc0 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,13 +1,15 @@ -export 'alien_zone.dart'; -export 'board.dart'; +export 'android_acres.dart'; +export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; export 'controlled_plunger.dart'; -export 'flutter_forest.dart'; +export 'dino_desert.dart'; +export 'drain.dart'; +export 'flutter_forest/flutter_forest.dart'; export 'game_flow_controller.dart'; -export 'google_word.dart'; +export 'google_word/google_word.dart'; export 'launcher.dart'; -export 'score_points.dart'; -export 'sparky_fire_zone.dart'; -export 'wall.dart'; +export 'multipliers/multipliers.dart'; +export 'scoring_behavior.dart'; +export 'sparky_scorch.dart'; diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 4f089a7c..4103bb81 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,5 +1,6 @@ +// ignore_for_file: avoid_renaming_method_parameters + import 'package:flame/components.dart'; -import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -8,36 +9,34 @@ import 'package:pinball_theme/pinball_theme.dart'; /// {@template controlled_ball} /// A [Ball] with a [BallController] attached. +/// +/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is +/// not over, a new [Ball] will be spawned. /// {@endtemplate} class ControlledBall extends Ball with Controls { /// A [Ball] that launches from the [Plunger]. - /// - /// When a launched [Ball] is lost, it will decrease the [GameState.balls] - /// count, and a new [Ball] is spawned. ControlledBall.launch({ - required PinballTheme theme, - }) : super(baseColor: theme.characterTheme.ballColor) { + required CharacterTheme characterTheme, + }) : super(baseColor: characterTheme.ballColor) { controller = BallController(this); - priority = RenderPriority.ballOnLaunchRamp; layer = Layer.launcher; + zIndex = ZIndexes.ballOnLaunchRamp; } /// {@template bonus_ball} /// {@macro controlled_ball} - /// - /// When a bonus [Ball] is lost, the [GameState.balls] doesn't change. /// {@endtemplate} ControlledBall.bonus({ - required PinballTheme theme, - }) : super(baseColor: theme.characterTheme.ballColor) { + required CharacterTheme characterTheme, + }) : super(baseColor: characterTheme.ballColor) { controller = BallController(this); - priority = RenderPriority.ballOnBoard; + zIndex = ZIndexes.ballOnBoard; } /// [Ball] used in [DebugPinballGame]. ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { - controller = DebugBallController(this); - priority = RenderPriority.ballOnBoard; + controller = BallController(this); + zIndex = ZIndexes.ballOnBoard; } } @@ -49,10 +48,8 @@ class BallController extends ComponentController /// {@macro ball_controller} BallController(Ball ball) : super(ball); - /// Removes the [Ball] from a [PinballGame]. - /// - /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into - /// a [BottomWall]. + /// Event triggered when the ball is lost. + // TODO(alestiago): Refactor using behaviors. void lost() { component.shouldRemove = true; } @@ -76,15 +73,9 @@ class BallController extends ComponentController @override void onRemove() { super.onRemove(); - gameRef.read().add(const BallLost()); + final noBallsLeft = gameRef.descendants().whereType().isEmpty; + if (noBallsLeft) { + gameRef.read().add(const RoundLost()); + } } } - -/// {@macro ball_controller} -class DebugBallController extends BallController { - /// {@macro ball_controller} - DebugBallController(Ball component) : super(component); - - @override - void onRemove() {} -} diff --git a/lib/game/components/dino_desert.dart b/lib/game/components/dino_desert.dart new file mode 100644 index 00000000..799274f9 --- /dev/null +++ b/lib/game/components/dino_desert.dart @@ -0,0 +1,21 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template dino_desert} +/// Area located next to the [Launcher] containing the [ChromeDino] and +/// [DinoWalls]. +/// {@endtemplate} +// TODO(allisonryan0002): use a controller to initiate dino bonus when dino is +// fully implemented. +class DinoDesert extends Component { + /// {@macro dino_desert} + DinoDesert() + : super( + children: [ + ChromeDino()..initialPosition = Vector2(12.3, -6.9), + DinoWalls(), + Slingshots(), + ], + ); +} diff --git a/lib/game/components/drain.dart b/lib/game/components/drain.dart new file mode 100644 index 00000000..1dc3e211 --- /dev/null +++ b/lib/game/components/drain.dart @@ -0,0 +1,34 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template drain} +/// Area located at the bottom of the board to detect when a [Ball] is lost. +/// {@endtemplate} +// TODO(allisonryan0002): move to components package when possible. +class Drain extends BodyComponent with ContactCallbacks { + /// {@macro drain} + Drain() : super(renderBody: false); + + @override + Body createBody() { + final shape = EdgeShape() + ..set( + BoardDimensions.bounds.bottomLeft.toVector2(), + BoardDimensions.bounds.bottomRight.toVector2(), + ); + final fixtureDef = FixtureDef(shape, isSensor: true); + final bodyDef = BodyDef(userData: this); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + +// TODO(allisonryan0002): move this to ball.dart when BallLost is removed. + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! ControlledBall) return; + other.controller.lost(); + } +} diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart deleted file mode 100644 index 9c8ab309..00000000 --- a/lib/game/components/flutter_forest.dart +++ /dev/null @@ -1,102 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template flutter_forest} -/// Area positioned at the top right of the [Board] where the [Ball] -/// can bounce off [DashNestBumper]s. -/// -/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] -/// is awarded, and the [DashNestBumper.main] releases a new [Ball]. -/// {@endtemplate} -class FlutterForest extends Component - with Controls<_FlutterForestController>, HasGameRef { - /// {@macro flutter_forest} - FlutterForest() { - controller = _FlutterForestController(this); - } - - @override - Future onLoad() async { - await super.onLoad(); - gameRef.addContactCallback(_DashNestBumperBallContactCallback()); - - final signpost = Signpost()..initialPosition = Vector2(8.35, -58.3); - - final bigNest = _DashNestBumper.main() - ..initialPosition = Vector2(18.55, -59.35); - final smallLeftNest = _DashNestBumper.a() - ..initialPosition = Vector2(8.95, -51.95); - final smallRightNest = _DashNestBumper.b() - ..initialPosition = Vector2(23.3, -46.75); - final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66); - - await addAll([ - signpost, - smallLeftNest, - smallRightNest, - bigNest, - dashAnimatronic, - ]); - } -} - -class _FlutterForestController extends ComponentController - with HasGameRef { - _FlutterForestController(FlutterForest flutterForest) : super(flutterForest); - - final _activatedBumpers = {}; - - void activateBumper(DashNestBumper dashNestBumper) { - if (!_activatedBumpers.add(dashNestBumper)) return; - - dashNestBumper.activate(); - - final activatedBonus = _activatedBumpers.length == 3; - if (activatedBonus) { - _addBonusBall(); - - gameRef.read().add(const BonusActivated(GameBonus.dashNest)); - _activatedBumpers - ..forEach((bumper) => bumper.deactivate()) - ..clear(); - - component.firstChild()?.playing = true; - } - } - - Future _addBonusBall() async { - await gameRef.add( - ControlledBall.bonus(theme: gameRef.theme) - ..initialPosition = Vector2(17.2, -52.7), - ); - } -} - -// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D -// ContactCallback process is enhanced. -class _DashNestBumper extends DashNestBumper with ScorePoints { - _DashNestBumper.main() : super.main(); - - _DashNestBumper.a() : super.a(); - - _DashNestBumper.b() : super.b(); - - @override - int get points => 20; -} - -class _DashNestBumperBallContactCallback - extends ContactCallback { - @override - void begin(DashNestBumper dashNestBumper, _, __) { - final parent = dashNestBumper.parent; - if (parent is FlutterForest) { - parent.controller.activateBumper(dashNestBumper); - } - } -} diff --git a/lib/game/components/flutter_forest/behaviors/behaviors.dart b/lib/game/components/flutter_forest/behaviors/behaviors.dart new file mode 100644 index 00000000..c0f39810 --- /dev/null +++ b/lib/game/components/flutter_forest/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'flutter_forest_bonus_behavior.dart'; diff --git a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart new file mode 100644 index 00000000..949fead1 --- /dev/null +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -0,0 +1,41 @@ +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest] +/// is awarded, and the [DashNestBumper.main] releases a new [Ball]. +class FlutterForestBonusBehavior extends Component + with ParentIsA, HasGameRef { + @override + void onMount() { + super.onMount(); + + final bumpers = parent.children.whereType(); + for (final bumper in bumpers) { + // TODO(alestiago): Refactor subscription management once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1538 + bumper.bloc.stream.listen((state) { + final achievedBonus = bumpers.every( + (bumper) => bumper.bloc.state == DashNestBumperState.active, + ); + + if (achievedBonus) { + gameRef + .read() + .add(const BonusActivated(GameBonus.dashNest)); + gameRef.add( + ControlledBall.bonus(characterTheme: gameRef.characterTheme) + ..initialPosition = Vector2(17.2, -52.7), + ); + parent.firstChild()?.playing = true; + + for (final bumper in bumpers) { + bumper.bloc.onReset(); + } + } + }); + } + } +} diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart new file mode 100644 index 00000000..92c69048 --- /dev/null +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -0,0 +1,51 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template flutter_forest} +/// Area positioned at the top right of the board where the [Ball] can bounce +/// off [DashNestBumper]s. +/// {@endtemplate} +class FlutterForest extends Component with ZIndex { + /// {@macro flutter_forest} + FlutterForest() + : super( + children: [ + Signpost( + children: [ + ScoringBehavior(points: 20), + ], + )..initialPosition = Vector2(8.35, -58.3), + DashNestBumper.main( + children: [ + ScoringBehavior(points: 200000), + ], + )..initialPosition = Vector2(18.55, -59.35), + DashNestBumper.a( + children: [ + ScoringBehavior(points: 20000), + ], + )..initialPosition = Vector2(8.95, -51.95), + DashNestBumper.b( + children: [ + ScoringBehavior(points: 20000), + ], + )..initialPosition = Vector2(23.3, -46.75), + DashAnimatronic()..position = Vector2(20, -66), + FlutterForestBonusBehavior(), + ], + ) { + zIndex = ZIndexes.flutterForest; + } + + /// Creates a [FlutterForest] without any children. + /// + /// This can be used for testing [FlutterForest]'s behaviors in isolation. + @visibleForTesting + FlutterForest.test(); +} diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart index 77afebe6..48dd5518 100644 --- a/lib/game/components/game_flow_controller.dart +++ b/lib/game/components/game_flow_controller.dart @@ -32,8 +32,7 @@ class GameFlowController extends ComponentController // next page component.firstChild()?.gameOverMode( score: state?.score ?? 0, - characterIconPath: - component.theme.characterTheme.leaderboardIcon.keyName, + characterIconPath: component.characterTheme.leaderboardIcon.keyName, ); component.firstChild()?.focusOnBackboard(); } diff --git a/lib/game/components/google_word.dart b/lib/game/components/google_word.dart deleted file mode 100644 index ed4f6513..00000000 --- a/lib/game/components/google_word.dart +++ /dev/null @@ -1,101 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'dart:async'; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.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 HasGameRef, Controls<_GoogleWordController> { - /// {@macro google_word} - GoogleWord({ - required Vector2 position, - }) : _position = position { - controller = _GoogleWordController(this); - } - - final Vector2 _position; - - @override - Future onLoad() async { - await super.onLoad(); - gameRef.addContactCallback(_GoogleLetterBallContactCallback()); - - final offsets = [ - Vector2(-12.92, 1.82), - Vector2(-8.33, -0.65), - Vector2(-2.88, -1.75), - Vector2(2.88, -1.75), - Vector2(8.33, -0.65), - Vector2(12.92, 1.82), - ]; - - final letters = []; - for (var index = 0; index < offsets.length; index++) { - letters.add( - GoogleLetter(index)..initialPosition = _position + offsets[index], - ); - } - - await addAll(letters); - } -} - -class _GoogleWordController extends ComponentController - with HasGameRef { - _GoogleWordController(GoogleWord googleWord) : super(googleWord); - - final _activatedLetters = {}; - - Future activate(GoogleLetter googleLetter) async { - if (!_activatedLetters.add(googleLetter)) return; - - googleLetter.activate(); - - final activatedBonus = _activatedLetters.length == 6; - if (activatedBonus) { - gameRef.audio.googleBonus(); - gameRef.read().add(const BonusActivated(GameBonus.googleWord)); - await _bonusAnimation(); - _activatedLetters.clear(); - } - } - - Future _bonusAnimation() async { - const blinkDuration = Duration(milliseconds: 300); - const blinkCount = 4; - final googleLetters = component.children.whereType(); - var shouldActivate = false; - - await Future.delayed(blinkDuration); - for (var i = 1; i < blinkCount * 2; i++) { - for (final letter in googleLetters) { - if (shouldActivate) { - letter.activate(); - } else { - letter.deactivate(); - } - } - shouldActivate = !shouldActivate; - await Future.delayed(blinkDuration); - } - } -} - -/// Activates a [GoogleLetter] when it contacts with a [Ball]. -class _GoogleLetterBallContactCallback - extends ContactCallback { - @override - void begin(GoogleLetter googleLetter, _, __) { - final parent = googleLetter.parent; - if (parent is GoogleWord) { - parent.controller.activate(googleLetter); - } - } -} diff --git a/lib/game/components/google_word/behaviors/behaviors.dart b/lib/game/components/google_word/behaviors/behaviors.dart new file mode 100644 index 00000000..4ebf817c --- /dev/null +++ b/lib/game/components/google_word/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'google_word_bonus_behavior.dart'; 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 new file mode 100644 index 00000000..92664531 --- /dev/null +++ b/lib/game/components/google_word/behaviors/google_word_bonus_behavior.dart @@ -0,0 +1,34 @@ +import 'package:flame/components.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 HasGameRef, ParentIsA { + @override + void onMount() { + super.onMount(); + + final googleLetters = parent.children.whereType(); + for (final letter in googleLetters) { + // TODO(alestiago): Refactor subscription management once the following is + // merged: + // https://github.com/flame-engine/flame/pull/1538 + letter.bloc.stream.listen((_) { + final achievedBonus = googleLetters + .every((letter) => letter.bloc.state == GoogleLetterState.active); + + if (achievedBonus) { + gameRef.audio.googleBonus(); + gameRef + .read() + .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 new file mode 100644 index 00000000..79e1e6e5 --- /dev/null +++ b/lib/game/components/google_word/google_word.dart @@ -0,0 +1,52 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.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_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: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(-12.92, 1.82), + GoogleLetter( + 1, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(-8.33, -0.65), + GoogleLetter( + 2, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(-2.88, -1.75), + GoogleLetter( + 3, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(2.88, -1.75), + GoogleLetter( + 4, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(8.33, -0.65), + GoogleLetter( + 5, + children: [ScoringBehavior(points: 5000)], + )..initialPosition = position + Vector2(12.92, 1.82), + 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/components/launcher.dart b/lib/game/components/launcher.dart index 7aef09d2..2663dfd4 100644 --- a/lib/game/components/launcher.dart +++ b/lib/game/components/launcher.dart @@ -1,21 +1,20 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame/components.dart'; import 'package:pinball/game/components/components.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; -import 'package:pinball_flame/pinball_flame.dart'; /// {@template launcher} -/// A [Blueprint] which creates the [Plunger], [RocketSpriteComponent] and -/// [LaunchRamp]. +/// Channel on the right side of the board containing the [LaunchRamp], +/// [Plunger], and [RocketSpriteComponent]. /// {@endtemplate} -class Launcher extends Blueprint { +class Launcher extends Component { /// {@macro launcher} Launcher() : super( - components: [ - ControlledPlunger(compressionDistance: 14) - ..initialPosition = Vector2(40.7, 38), - RocketSpriteComponent()..position = Vector2(43, 62), + children: [ + LaunchRamp(), + ControlledPlunger(compressionDistance: 10.5) + ..initialPosition = Vector2(41.1, 43), + RocketSpriteComponent()..position = Vector2(43, 62.3), ], - blueprints: [LaunchRamp()], ); } diff --git a/lib/game/components/multipliers/behaviors/behaviors.dart b/lib/game/components/multipliers/behaviors/behaviors.dart new file mode 100644 index 00000000..70703bba --- /dev/null +++ b/lib/game/components/multipliers/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'multipliers_behavior.dart'; diff --git a/lib/game/components/multipliers/behaviors/multipliers_behavior.dart b/lib/game/components/multipliers/behaviors/multipliers_behavior.dart new file mode 100644 index 00000000..33a59a08 --- /dev/null +++ b/lib/game/components/multipliers/behaviors/multipliers_behavior.dart @@ -0,0 +1,25 @@ +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'; + +/// Toggle each [Multiplier] when GameState.multiplier changes. +class MultipliersBehavior extends Component + with + HasGameRef, + ParentIsA, + BlocComponent { + @override + bool listenWhen(GameState? previousState, GameState newState) { + return previousState?.multiplier != newState.multiplier; + } + + @override + void onNewState(GameState state) { + final multipliers = parent.children.whereType(); + for (final multiplier in multipliers) { + multiplier.bloc.next(state.multiplier); + } + } +} diff --git a/lib/game/components/multipliers/multipliers.dart b/lib/game/components/multipliers/multipliers.dart new file mode 100644 index 00000000..8e9df1ff --- /dev/null +++ b/lib/game/components/multipliers/multipliers.dart @@ -0,0 +1,47 @@ +import 'dart:math' as math; +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template multipliers} +/// A group for the multipliers on the board. +/// {@endtemplate} +class Multipliers extends Component with ZIndex { + /// {@macro multipliers} + Multipliers() + : super( + children: [ + Multiplier.x2( + position: Vector2(-19.5, -2), + angle: -15 * math.pi / 180, + ), + Multiplier.x3( + position: Vector2(13, -9.4), + angle: 15 * math.pi / 180, + ), + Multiplier.x4( + position: Vector2(0, -21.2), + angle: 0, + ), + Multiplier.x5( + position: Vector2(-8.5, -28), + angle: -3 * math.pi / 180, + ), + Multiplier.x6( + position: Vector2(10, -30.7), + angle: 8 * math.pi / 180, + ), + MultipliersBehavior(), + ], + ) { + zIndex = ZIndexes.decal; + } + + /// Creates [Multipliers] without any children. + /// + /// This can be used for testing [Multipliers]'s behaviors in isolation. + @visibleForTesting + Multipliers.test(); +} diff --git a/lib/game/components/score_points.dart b/lib/game/components/score_points.dart deleted file mode 100644 index 8a76680d..00000000 --- a/lib/game/components/score_points.dart +++ /dev/null @@ -1,47 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template score_points} -/// Specifies the amount of points received on [Ball] collision. -/// {@endtemplate} -mixin ScorePoints on BodyComponent { - /// {@macro score_points} - int get points; - - @override - Future onLoad() async { - await super.onLoad(); - body.userData = this; - } -} - -/// {@template ball_score_points_callbacks} -/// Adds points to the score when a [Ball] collides with a [BodyComponent] that -/// implements [ScorePoints]. -/// {@endtemplate} -class BallScorePointsCallback extends ContactCallback { - /// {@macro ball_score_points_callbacks} - BallScorePointsCallback(PinballGame game) : _gameRef = game; - - final PinballGame _gameRef; - - @override - void begin( - Ball ball, - ScorePoints scorePoints, - Contact _, - ) { - _gameRef.read().add(Scored(points: scorePoints.points)); - _gameRef.audio.score(); - - _gameRef.add( - ScoreText( - text: scorePoints.points.toString(), - position: ball.body.position, - ), - ); - } -} diff --git a/lib/game/components/scoring_behavior.dart b/lib/game/components/scoring_behavior.dart new file mode 100644 index 00000000..3e757eab --- /dev/null +++ b/lib/game/components/scoring_behavior.dart @@ -0,0 +1,34 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template scoring_behavior} +/// Adds points to the score when the ball contacts the [parent]. +/// {@endtemplate} +class ScoringBehavior extends ContactBehavior with HasGameRef { + /// {@macro scoring_behavior} + ScoringBehavior({ + required int points, + }) : _points = points; + + final int _points; + + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + gameRef.read().add(Scored(points: _points)); + gameRef.audio.score(); + gameRef.firstChild()!.add( + ScoreText( + text: _points.toString(), + position: other.body.position, + ), + ); + } +} diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart deleted file mode 100644 index a5450761..00000000 --- a/lib/game/components/sparky_fire_zone.dart +++ /dev/null @@ -1,110 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template sparky_fire_zone} -/// Area positioned at the top left of the [Board] where the [Ball] -/// can bounce off [SparkyBumper]s. -/// -/// When a [Ball] hits [SparkyBumper]s, the bumper animates. -/// {@endtemplate} -class SparkyFireZone extends Blueprint { - /// {@macro sparky_fire_zone} - SparkyFireZone() - : super( - components: [ - _SparkyBumper.a()..initialPosition = Vector2(-22.9, -41.65), - _SparkyBumper.b()..initialPosition = Vector2(-21.25, -57.9), - _SparkyBumper.c()..initialPosition = Vector2(-3.3, -52.55), - SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8), - SparkyAnimatronic()..position = Vector2(-13.8, -58.2), - ], - blueprints: [ - SparkyComputer(), - ], - ); -} - -// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D -// ContactCallback process is enhanced. -class _SparkyBumper extends SparkyBumper with ScorePoints { - _SparkyBumper.a() : super.a(); - - _SparkyBumper.b() : super.b(); - - _SparkyBumper.c() : super.c(); - - @override - int get points => 20; - - @override - Future onLoad() async { - await super.onLoad(); - // TODO(alestiago): Revisit once this has been merged: - // https://github.com/flame-engine/flame/pull/1547 - gameRef.addContactCallback(SparkyBumperBallContactCallback()); - } -} - -/// Listens when a [Ball] bounces bounces against a [SparkyBumper]. -@visibleForTesting -class SparkyBumperBallContactCallback - extends ContactCallback { - @override - void begin( - SparkyBumper sparkyBumper, - Ball _, - Contact __, - ) { - sparkyBumper.animate(); - } -} - -/// {@template sparky_computer_sensor} -/// Small sensor body used to detect when a ball has entered the -/// [SparkyComputer]. -/// {@endtemplate} -// TODO(alestiago): Revisit once this has been merged: -// https://github.com/flame-engine/flame/pull/1547 -class SparkyComputerSensor extends BodyComponent with InitialPosition { - /// {@macro sparky_computer_sensor} - SparkyComputerSensor() { - renderBody = false; - } - - @override - Body createBody() { - final shape = CircleShape()..radius = 0.1; - final fixtureDef = FixtureDef(shape, isSensor: true); - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - - @override - Future onLoad() async { - await super.onLoad(); - // TODO(alestiago): Revisit once this has been merged: - // https://github.com/flame-engine/flame/pull/1547 - gameRef.addContactCallback(SparkyComputerSensorBallContactCallback()); - } -} - -@visibleForTesting -// TODO(alestiago): Revisit once this has been merged: -// https://github.com/flame-engine/flame/pull/1547 -// ignore: public_member_api_docs -class SparkyComputerSensorBallContactCallback - extends ContactCallback { - @override - void begin(_, ControlledBall controlledBall, __) { - controlledBall.controller.turboCharge(); - controlledBall.gameRef.firstChild()?.playing = true; - } -} diff --git a/lib/game/components/sparky_scorch.dart b/lib/game/components/sparky_scorch.dart new file mode 100644 index 00000000..271e3527 --- /dev/null +++ b/lib/game/components/sparky_scorch.dart @@ -0,0 +1,73 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template sparky_scorch} +/// Area positioned at the top left of the board containing the +/// [SparkyComputer], [SparkyAnimatronic], and [SparkyBumper]s. +/// {@endtemplate} +class SparkyScorch extends Component { + /// {@macro sparky_scorch} + SparkyScorch() + : super( + children: [ + SparkyBumper.a( + children: [ + ScoringBehavior(points: 20000), + ], + )..initialPosition = Vector2(-22.9, -41.65), + SparkyBumper.b( + children: [ + ScoringBehavior(points: 20000), + ], + )..initialPosition = Vector2(-21.25, -57.9), + SparkyBumper.c( + children: [ + ScoringBehavior(points: 20000), + ], + )..initialPosition = Vector2(-3.3, -52.55), + SparkyComputerSensor()..initialPosition = Vector2(-13, -49.8), + SparkyAnimatronic()..position = Vector2(-13.8, -58.2), + SparkyComputer(), + ], + ); +} + +/// {@template sparky_computer_sensor} +/// Small sensor body used to detect when a ball has entered the +/// [SparkyComputer]. +/// {@endtemplate} +class SparkyComputerSensor extends BodyComponent + with InitialPosition, ContactCallbacks { + /// {@macro sparky_computer_sensor} + SparkyComputerSensor() + : super( + renderBody: false, + children: [ + ScoringBehavior(points: 200000), + ], + ); + + @override + Body createBody() { + final shape = CircleShape()..radius = 0.1; + final fixtureDef = FixtureDef(shape, isSensor: true); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + ); + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! ControlledBall) return; + + other.controller.turboCharge(); + gameRef.firstChild()?.playing = true; + } +} diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart deleted file mode 100644 index aaae1d23..00000000 --- a/lib/game/components/wall.dart +++ /dev/null @@ -1,66 +0,0 @@ -// ignore_for_file: avoid_renaming_method_parameters - -import 'package:flame/extensions.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; - -/// {@template wall} -/// A continuous generic and [BodyType.static] barrier that divides a game area. -/// {@endtemplate} -// TODO(alestiago): Remove [Wall] for [Pathway.straight]. -class Wall extends BodyComponent { - /// {@macro wall} - Wall({ - required this.start, - required this.end, - }); - - /// The [start] of the [Wall]. - final Vector2 start; - - /// The [end] of the [Wall]. - final Vector2 end; - - @override - Body createBody() { - final shape = EdgeShape()..set(start, end); - - final fixtureDef = FixtureDef(shape) - ..restitution = 0.1 - ..friction = 0; - - final bodyDef = BodyDef() - ..userData = this - ..position = Vector2.zero() - ..type = BodyType.static; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -/// {@template bottom_wall} -/// [Wall] located at the bottom of the board. -/// -/// Collisions with [BottomWall] are listened by -/// [BottomWallBallContactCallback]. -/// {@endtemplate} -class BottomWall extends Wall { - /// {@macro bottom_wall} - BottomWall() - : super( - start: BoardDimensions.bounds.bottomLeft.toVector2(), - end: BoardDimensions.bounds.bottomRight.toVector2(), - ); -} - -/// {@template bottom_wall_ball_contact_callback} -/// Listens when a [ControlledBall] falls into a [BottomWall]. -/// {@endtemplate} -class BottomWallBallContactCallback - extends ContactCallback { - @override - void begin(ControlledBall ball, BottomWall wall, Contact contact) { - ball.controller.lost(); - } -} diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 87fbfcda..6bdb5a91 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -13,6 +13,7 @@ extension PinballGameAssetsX on PinballGame { const dinoTheme = DinoTheme(); return [ + images.load(components.Assets.images.boardBackground.keyName), images.load(components.Assets.images.ball.ball.keyName), images.load(components.Assets.images.ball.flameEffect.keyName), images.load(components.Assets.images.signpost.inactive.keyName), @@ -23,8 +24,10 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.flipper.right.keyName), images.load(components.Assets.images.baseboard.left.keyName), images.load(components.Assets.images.baseboard.right.keyName), - images.load(components.Assets.images.kicker.left.keyName), - images.load(components.Assets.images.kicker.right.keyName), + images.load(components.Assets.images.kicker.left.lit.keyName), + images.load(components.Assets.images.kicker.left.dimmed.keyName), + images.load(components.Assets.images.kicker.right.lit.keyName), + images.load(components.Assets.images.kicker.right.dimmed.keyName), images.load(components.Assets.images.slingshot.upper.keyName), images.load(components.Assets.images.slingshot.lower.keyName), images.load(components.Assets.images.launchRamp.ramp.keyName), @@ -34,8 +37,10 @@ extension PinballGameAssetsX on PinballGame { images.load( components.Assets.images.launchRamp.backgroundRailing.keyName, ), - images.load(components.Assets.images.dino.dinoLandTop.keyName), - images.load(components.Assets.images.dino.dinoLandBottom.keyName), + images.load(components.Assets.images.dino.bottomWall.keyName), + images.load(components.Assets.images.dino.topWall.keyName), + images.load(components.Assets.images.dino.animatronic.head.keyName), + images.load(components.Assets.images.dino.animatronic.mouth.keyName), images.load(components.Assets.images.dash.animatronic.keyName), images.load(components.Assets.images.dash.bumper.a.active.keyName), images.load(components.Assets.images.dash.bumper.a.inactive.keyName), @@ -48,50 +53,52 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.boundary.bottom.keyName), images.load(components.Assets.images.boundary.outer.keyName), images.load(components.Assets.images.boundary.outerBottom.keyName), - images.load(components.Assets.images.spaceship.saucer.keyName), - images.load(components.Assets.images.spaceship.bridge.keyName), - images.load(components.Assets.images.spaceship.ramp.boardOpening.keyName), + images.load(components.Assets.images.android.spaceship.saucer.keyName), + images + .load(components.Assets.images.android.spaceship.animatronic.keyName), + images.load(components.Assets.images.android.spaceship.lightBeam.keyName), + images.load(components.Assets.images.android.ramp.boardOpening.keyName), images.load( - components.Assets.images.spaceship.ramp.railingForeground.keyName, + components.Assets.images.android.ramp.railingForeground.keyName, ), images.load( - components.Assets.images.spaceship.ramp.railingBackground.keyName, + components.Assets.images.android.ramp.railingBackground.keyName, ), - images.load(components.Assets.images.spaceship.ramp.main.keyName), - images - .load(components.Assets.images.spaceship.ramp.arrow.inactive.keyName), + images.load(components.Assets.images.android.ramp.main.keyName), + images.load(components.Assets.images.android.ramp.arrow.inactive.keyName), images.load( - components.Assets.images.spaceship.ramp.arrow.active1.keyName, + components.Assets.images.android.ramp.arrow.active1.keyName, ), images.load( - components.Assets.images.spaceship.ramp.arrow.active2.keyName, + components.Assets.images.android.ramp.arrow.active2.keyName, ), images.load( - components.Assets.images.spaceship.ramp.arrow.active3.keyName, + components.Assets.images.android.ramp.arrow.active3.keyName, ), images.load( - components.Assets.images.spaceship.ramp.arrow.active4.keyName, + components.Assets.images.android.ramp.arrow.active4.keyName, ), images.load( - components.Assets.images.spaceship.ramp.arrow.active5.keyName, + components.Assets.images.android.ramp.arrow.active5.keyName, ), - images.load(components.Assets.images.spaceship.rail.main.keyName), - images.load(components.Assets.images.spaceship.rail.foreground.keyName), - images.load(components.Assets.images.alienBumper.a.active.keyName), - images.load(components.Assets.images.alienBumper.a.inactive.keyName), - images.load(components.Assets.images.alienBumper.b.active.keyName), - images.load(components.Assets.images.alienBumper.b.inactive.keyName), - images.load(components.Assets.images.chromeDino.mouth.keyName), - images.load(components.Assets.images.chromeDino.head.keyName), + images.load(components.Assets.images.android.rail.main.keyName), + images.load(components.Assets.images.android.rail.exit.keyName), + images.load(components.Assets.images.android.bumper.a.lit.keyName), + images.load(components.Assets.images.android.bumper.a.dimmed.keyName), + images.load(components.Assets.images.android.bumper.b.lit.keyName), + images.load(components.Assets.images.android.bumper.b.dimmed.keyName), + images.load(components.Assets.images.android.bumper.cow.lit.keyName), + images.load(components.Assets.images.android.bumper.cow.dimmed.keyName), images.load(components.Assets.images.sparky.computer.top.keyName), images.load(components.Assets.images.sparky.computer.base.keyName), + images.load(components.Assets.images.sparky.computer.glow.keyName), images.load(components.Assets.images.sparky.animatronic.keyName), - images.load(components.Assets.images.sparky.bumper.a.inactive.keyName), - images.load(components.Assets.images.sparky.bumper.a.active.keyName), - images.load(components.Assets.images.sparky.bumper.b.active.keyName), - images.load(components.Assets.images.sparky.bumper.b.inactive.keyName), - images.load(components.Assets.images.sparky.bumper.c.active.keyName), - images.load(components.Assets.images.sparky.bumper.c.inactive.keyName), + images.load(components.Assets.images.sparky.bumper.a.lit.keyName), + images.load(components.Assets.images.sparky.bumper.a.dimmed.keyName), + images.load(components.Assets.images.sparky.bumper.b.lit.keyName), + images.load(components.Assets.images.sparky.bumper.b.dimmed.keyName), + images.load(components.Assets.images.sparky.bumper.c.lit.keyName), + images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName), images.load(components.Assets.images.backboard.backboardScores.keyName), images.load(components.Assets.images.backboard.backboardGameOver.keyName), images.load(components.Assets.images.googleWord.letter1.active.keyName), @@ -107,6 +114,16 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.googleWord.letter6.active.keyName), images.load(components.Assets.images.googleWord.letter6.inactive.keyName), images.load(components.Assets.images.backboard.display.keyName), + images.load(components.Assets.images.multiplier.x2.lit.keyName), + images.load(components.Assets.images.multiplier.x2.dimmed.keyName), + images.load(components.Assets.images.multiplier.x3.lit.keyName), + images.load(components.Assets.images.multiplier.x3.dimmed.keyName), + images.load(components.Assets.images.multiplier.x4.lit.keyName), + images.load(components.Assets.images.multiplier.x4.dimmed.keyName), + images.load(components.Assets.images.multiplier.x5.lit.keyName), + images.load(components.Assets.images.multiplier.x5.dimmed.keyName), + images.load(components.Assets.images.multiplier.x6.lit.keyName), + images.load(components.Assets.images.multiplier.x6.dimmed.keyName), images.load(dashTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName), diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 55a6b23a..fcc58487 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -6,21 +6,22 @@ import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -import 'package:pinball_theme/pinball_theme.dart' hide Assets; +import 'package:pinball_theme/pinball_theme.dart'; class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents, - Controls<_GameBallsController> { + Controls<_GameBallsController>, + TapDetector { PinballGame({ - required this.theme, + required this.characterTheme, required this.audio, }) { images.prefix = ''; @@ -33,7 +34,7 @@ class PinballGame extends Forge2DGame @override Color backgroundColor() => Colors.transparent; - final PinballTheme theme; + final CharacterTheme characterTheme; final PinballAudio audio; @@ -41,162 +42,170 @@ class PinballGame extends Forge2DGame @override Future onLoad() async { - _addContactCallbacks(); - - unawaited(add(gameFlowController = GameFlowController(this))); - unawaited(add(CameraController(this))); - unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); - - // TODO(allisonryan0002): banish Wall and Board classes in later PR. - await add(BottomWall()); - unawaited(addFromBlueprint(Boundaries())); - unawaited(addFromBlueprint(LaunchRamp())); - - final launcher = Launcher(); - unawaited(addFromBlueprint(launcher)); - unawaited(add(Board())); - unawaited(add(AlienZone())); - await addFromBlueprint(SparkyFireZone()); - unawaited(addFromBlueprint(Slingshots())); - unawaited(addFromBlueprint(DinoWalls())); - unawaited(_addBonusWord()); - unawaited(addFromBlueprint(SpaceshipRamp())); - unawaited( - addFromBlueprint( - Spaceship( - position: Vector2(-26.5, -28.5), - ), + await add(gameFlowController = GameFlowController(this)); + await add(CameraController(this)); + + final machine = [ + BoardBackgroundSpriteComponent(), + Boundaries(), + Backboard.waiting(position: Vector2(0, -88)), + ]; + final decals = [ + GoogleWord( + position: Vector2(-4.1, 1.8), + ), + Multipliers(), + ]; + final characterAreas = [ + AndroidAcres(), + DinoDesert(), + FlutterForest(), + SparkyScorch(), + ]; + + await add( + ZCanvasComponent( + children: [ + ...machine, + ...decals, + ...characterAreas, + Drain(), + BottomGroup(), + Launcher(), + ], ), ); - unawaited(addFromBlueprint(SpaceshipRail())); - controller.attachTo(launcher.components.whereType().first); await super.onLoad(); } - void _addContactCallbacks() { - addContactCallback(BallScorePointsCallback(this)); - addContactCallback(BottomWallBallContactCallback()); + BoardSide? focusedBoardSide; + + @override + void onTapDown(TapDownInfo info) { + if (info.raw.kind == PointerDeviceKind.touch) { + final rocket = descendants().whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + // NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. + if (bounds.contains(info.eventPosition.game.toOffset())) { + descendants().whereType().single.pull(); + } else { + final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; + focusedBoardSide = leftSide ? BoardSide.left : BoardSide.right; + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveUp(); + } + } + + super.onTapDown(info); } - Future _addBonusWord() async { - await add( - GoogleWord( - position: Vector2( - BoardDimensions.bounds.center.dx - 4.1, - BoardDimensions.bounds.center.dy + 1.8, - ), - ), - ); + @override + void onTapUp(TapUpInfo info) { + final rocket = descendants().whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + if (bounds.contains(info.eventPosition.game.toOffset())) { + descendants().whereType().single.release(); + } else { + _moveFlippersDown(); + } + super.onTapUp(info); + } + + @override + void onTapCancel() { + descendants().whereType().single.release(); + + _moveFlippersDown(); + super.onTapCancel(); + } + + void _moveFlippersDown() { + if (focusedBoardSide != null) { + final flippers = descendants().whereType().where((flipper) { + return flipper.side == focusedBoardSide; + }); + flippers.first.moveDown(); + focusedBoardSide = null; + } } } class _GameBallsController extends ComponentController - with BlocComponent, HasGameRef { + with BlocComponent { _GameBallsController(PinballGame game) : super(game); - late final Plunger _plunger; - @override bool listenWhen(GameState? previousState, GameState newState) { final noBallsLeft = component.descendants().whereType().isEmpty; - final canBallRespawn = newState.balls > 0; + final notGameOver = !newState.isGameOver; - return noBallsLeft && canBallRespawn; + return noBallsLeft && notGameOver; } @override void onNewState(GameState state) { super.onNewState(state); - _spawnBall(); + spawnBall(); } @override Future onLoad() async { await super.onLoad(); - _spawnBall(); - } - - void _spawnBall() { - final ball = ControlledBall.launch( - theme: gameRef.theme, - )..initialPosition = Vector2( - _plunger.body.position.x, - _plunger.body.position.y - Ball.size.y, - ); - component.add(ball); + spawnBall(); } - /// Attaches the controller to the plunger. - // TODO(alestiago): Remove this method and use onLoad instead. - // ignore: use_setters_to_change_properties - void attachTo(Plunger plunger) { - _plunger = plunger; + void spawnBall() { + // TODO(alestiago): Refactor with behavioural pattern. + component.ready().whenComplete(() { + final plunger = parent!.descendants().whereType().single; + final ball = ControlledBall.launch( + characterTheme: component.characterTheme, + )..initialPosition = Vector2( + plunger.body.position.x, + plunger.body.position.y - Ball.size.y, + ); + component.firstChild()?.add(ball); + }); } } -class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { +class DebugPinballGame extends PinballGame with FPSCounter { DebugPinballGame({ - required PinballTheme theme, + required CharacterTheme characterTheme, required PinballAudio audio, }) : super( - theme: theme, + characterTheme: characterTheme, audio: audio, ) { - controller = _DebugGameBallsController(this); + controller = _GameBallsController(this); } @override Future onLoad() async { await super.onLoad(); - await _loadBackground(); await add(_DebugInformation()); } - // TODO(alestiago): Move to PinballGame once we have the real background - // component. - Future _loadBackground() async { - final sprite = await loadSprite( - Assets.images.components.background.path, - ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: Vector2(120, 160), - anchor: Anchor.center, - ) - ..position = Vector2(0, -7.8) - ..priority = RenderPriority.background; - - await add(spriteComponent); - } - @override void onTapUp(TapUpInfo info) { - add( - ControlledBall.debug()..initialPosition = info.eventPosition.game, - ); - } -} + super.onTapUp(info); -class _DebugGameBallsController extends _GameBallsController { - _DebugGameBallsController(PinballGame game) : super(game); - - @override - bool listenWhen(GameState? previousState, GameState newState) { - final noBallsLeft = component - .descendants() - .whereType() - .where((ball) => ball.controller is! DebugBallController) - .isEmpty; - final canBallRespawn = newState.balls > 0; - - return noBallsLeft && canBallRespawn; + if (info.raw.kind == PointerDeviceKind.mouse) { + final ball = ControlledBall.debug() + ..initialPosition = info.eventPosition.game; + firstChild()?.add(ball); + } } } +// TODO(wolfenrain): investigate this CI failure. +// coverage:ignore-start class _DebugInformation extends Component with HasGameRef { - _DebugInformation() : super(priority: RenderPriority.debugInfo); - @override PositionType get positionType => PositionType.widget; @@ -225,3 +234,4 @@ class _DebugInformation extends Component with HasGameRef { _debugTextPaint.render(canvas, debugText, position); } } +// coverage:ignore-end diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 38ae0144..be11a15c 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -5,8 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; -import 'package:pinball/theme/theme.dart'; import 'package:pinball_audio/pinball_audio.dart'; class PinballGamePage extends StatelessWidget { @@ -31,17 +31,19 @@ class PinballGamePage extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = context.read().state.theme; + final characterTheme = + context.read().state.characterTheme; final audio = context.read(); final pinballAudio = context.read(); final game = isDebugMode - ? DebugPinballGame(theme: theme, audio: audio) - : PinballGame(theme: theme, audio: audio); + ? DebugPinballGame(characterTheme: characterTheme, audio: audio) + : PinballGame(characterTheme: characterTheme, audio: audio); final loadables = [ ...game.preLoadAssets(), pinballAudio.load(), + ...BonusAnimation.loadAssets(), ]; return MultiBlocProvider( @@ -112,6 +114,10 @@ class PinballGameLoadedView extends StatelessWidget { @override Widget build(BuildContext context) { + final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; + final screenWidth = MediaQuery.of(context).size.width; + final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); + return Stack( children: [ Positioned.fill( @@ -130,10 +136,12 @@ class PinballGameLoadedView extends StatelessWidget { }, ), ), - const Positioned( - top: 8, - left: 8, - child: GameHud(), + // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc + // status + Positioned( + top: 16, + left: leftMargin, + child: const GameHud(), ), ], ); diff --git a/lib/game/view/widgets/bonus_animation.dart b/lib/game/view/widgets/bonus_animation.dart index 39cee913..da67e1aa 100644 --- a/lib/game/view/widgets/bonus_animation.dart +++ b/lib/game/view/widgets/bonus_animation.dart @@ -1,19 +1,23 @@ -// ignore_for_file: public_member_api_docs - import 'package:flame/flame.dart'; import 'package:flame/sprite.dart'; -import 'package:flame/widgets.dart'; import 'package:flutter/material.dart' hide Image; import 'package:pinball/gen/assets.gen.dart'; +import 'package:pinball_flame/pinball_flame.dart'; -class BonusAnimation extends StatelessWidget { +/// {@template bonus_animation} +/// [Widget] that displays bonus animations. +/// {@endtemplate} +class BonusAnimation extends StatefulWidget { + /// {@macro bonus_animation} const BonusAnimation._( - this.imagePath, { + String imagePath, { VoidCallback? onCompleted, Key? key, - }) : _onCompleted = onCompleted, + }) : _imagePath = imagePath, + _onCompleted = onCompleted, super(key: key); + /// [Widget] that displays the dash nest animation. BonusAnimation.dashNest({ Key? key, VoidCallback? onCompleted, @@ -23,6 +27,7 @@ class BonusAnimation extends StatelessWidget { key: key, ); + /// [Widget] that displays the sparky turbo charge animation. BonusAnimation.sparkyTurboCharge({ Key? key, VoidCallback? onCompleted, @@ -32,56 +37,94 @@ class BonusAnimation extends StatelessWidget { key: key, ); - BonusAnimation.dino({ + /// [Widget] that displays the dino chomp animation. + BonusAnimation.dinoChomp({ Key? key, VoidCallback? onCompleted, }) : this._( - Assets.images.bonusAnimation.dino.keyName, + Assets.images.bonusAnimation.dinoChomp.keyName, onCompleted: onCompleted, key: key, ); - BonusAnimation.android({ + /// [Widget] that displays the android spaceship animation. + BonusAnimation.androidSpaceship({ Key? key, VoidCallback? onCompleted, }) : this._( - Assets.images.bonusAnimation.android.keyName, + Assets.images.bonusAnimation.androidSpaceship.keyName, onCompleted: onCompleted, key: key, ); - BonusAnimation.google({ + /// [Widget] that displays the google word animation. + BonusAnimation.googleWord({ Key? key, VoidCallback? onCompleted, }) : this._( - Assets.images.bonusAnimation.google.keyName, + Assets.images.bonusAnimation.googleWord.keyName, onCompleted: onCompleted, key: key, ); - final String imagePath; + final String _imagePath; final VoidCallback? _onCompleted; - static Future loadAssets() { + /// Returns a list of assets to be loaded for animations. + static List loadAssets() { Flame.images.prefix = ''; - return Flame.images.loadAll([ - Assets.images.bonusAnimation.dashNest.keyName, - Assets.images.bonusAnimation.sparkyTurboCharge.keyName, - Assets.images.bonusAnimation.dino.keyName, - Assets.images.bonusAnimation.android.keyName, - Assets.images.bonusAnimation.google.keyName, - ]); + return [ + Flame.images.load(Assets.images.bonusAnimation.dashNest.keyName), + Flame.images.load(Assets.images.bonusAnimation.sparkyTurboCharge.keyName), + Flame.images.load(Assets.images.bonusAnimation.dinoChomp.keyName), + Flame.images.load(Assets.images.bonusAnimation.androidSpaceship.keyName), + Flame.images.load(Assets.images.bonusAnimation.googleWord.keyName), + ]; + } + + @override + State createState() => _BonusAnimationState(); +} + +class _BonusAnimationState extends State + with TickerProviderStateMixin { + late SpriteAnimationController controller; + late SpriteAnimation animation; + bool shouldRunBuildCallback = true; + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + // When the animation is overwritten by another animation, we need to stop + // the callback in the build method as it will break the new animation. + // Otherwise we need to set up a new callback when a new animation starts to + // show the score view at the end of the animation. + @override + void didUpdateWidget(BonusAnimation oldWidget) { + shouldRunBuildCallback = oldWidget._imagePath == widget._imagePath; + + Future.delayed( + Duration(seconds: animation.totalDuration().ceil()), + () { + widget._onCompleted?.call(); + }, + ); + + super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { final spriteSheet = SpriteSheet.fromColumnsAndRows( - image: Flame.images.fromCache(imagePath), + image: Flame.images.fromCache(widget._imagePath), columns: 8, rows: 9, ); - final animation = spriteSheet.createAnimation( + animation = spriteSheet.createAnimation( row: 0, stepTime: 1 / 24, to: spriteSheet.rows * spriteSheet.columns, @@ -91,15 +134,22 @@ class BonusAnimation extends StatelessWidget { Future.delayed( Duration(seconds: animation.totalDuration().ceil()), () { - _onCompleted?.call(); + if (shouldRunBuildCallback) { + widget._onCompleted?.call(); + } }, ); + controller = SpriteAnimationController( + animation: animation, + vsync: this, + )..forward(); + return SizedBox( width: double.infinity, height: double.infinity, child: SpriteAnimationWidget( - animation: animation, + controller: controller, ), ); } diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 00eedd2b..605bceb4 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -1,46 +1,122 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/gen/gen.dart'; +import 'package:pinball_ui/pinball_ui.dart'; /// {@template game_hud} -/// Overlay of a [PinballGame] that displays the current [GameState.score] and -/// [GameState.balls]. +/// Overlay on the [PinballGame]. +/// +/// Displays the current [GameState.score], [GameState.rounds] and animates when +/// the player gets a [GameBonus]. /// {@endtemplate} -class GameHud extends StatelessWidget { +class GameHud extends StatefulWidget { /// {@macro game_hud} const GameHud({Key? key}) : super(key: key); + @override + State createState() => _GameHudState(); +} + +class _GameHudState extends State { + bool showAnimation = false; + + /// Ratio from sprite frame (width 500, height 144) w / h = ratio + static const _ratio = 3.47; + static const _width = 265.0; + @override Widget build(BuildContext context) { - final state = context.watch().state; - - return Container( - color: Colors.redAccent, - width: 200, - height: 100, - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${state.score}', - style: Theme.of(context).textTheme.headline3, + final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + + return _ScoreViewDecoration( + child: SizedBox( + height: _width / _ratio, + width: _width, + child: BlocListener( + listenWhen: (previous, current) => + previous.bonusHistory.length != current.bonusHistory.length, + listener: (_, __) => setState(() => showAnimation = true), + child: AnimatedSwitcher( + duration: kThemeAnimationDuration, + child: showAnimation && !isGameOver + ? _AnimationView( + onComplete: () { + if (mounted) { + setState(() => showAnimation = false); + } + }, + ) + : const ScoreView(), ), - Wrap( - direction: Axis.vertical, - children: [ - for (var i = 0; i < state.balls; i++) - const Padding( - padding: EdgeInsets.only(top: 6, right: 6), - child: CircleAvatar( - radius: 8, - backgroundColor: Colors.black, - ), - ), - ], + ), + ), + ); + } +} + +class _ScoreViewDecoration extends StatelessWidget { + const _ScoreViewDecoration({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(12)); + const borderWidth = 5.0; + + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: radius, + border: Border.all( + color: PinballColors.white, + width: borderWidth, + ), + image: DecorationImage( + fit: BoxFit.cover, + image: AssetImage( + Assets.images.score.miniScoreBackground.path, ), - ], + ), ), + child: Padding( + padding: const EdgeInsets.all(borderWidth - 1), + child: ClipRRect( + borderRadius: radius, + child: child, + ), + ), + ); + } +} + +class _AnimationView extends StatelessWidget { + const _AnimationView({ + Key? key, + required this.onComplete, + }) : super(key: key); + + final VoidCallback onComplete; + + @override + Widget build(BuildContext context) { + final lastBonus = context.select( + (GameBloc bloc) => bloc.state.bonusHistory.last, ); + switch (lastBonus) { + case GameBonus.dashNest: + return BonusAnimation.dashNest(onCompleted: onComplete); + case GameBonus.sparkyTurboCharge: + return BonusAnimation.sparkyTurboCharge(onCompleted: onComplete); + case GameBonus.dinoChomp: + return BonusAnimation.dinoChomp(onCompleted: onComplete); + case GameBonus.googleWord: + return BonusAnimation.googleWord(onCompleted: onComplete); + case GameBonus.androidSpaceship: + return BonusAnimation.androidSpaceship(onCompleted: onComplete); + } } } diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index ce5dce4b..c855f776 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:pinball/game/pinball_game.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; /// {@template play_button_overlay} /// [Widget] that renders the button responsible to starting the game @@ -22,23 +22,9 @@ class PlayButtonOverlay extends StatelessWidget { return Center( child: ElevatedButton( - onPressed: () { + onPressed: () async { _game.gameFlowController.start(); - showDialog( - context: context, - barrierDismissible: false, - builder: (_) { - final height = MediaQuery.of(context).size.height * 0.5; - - return Center( - child: SizedBox( - height: height, - width: height * 1.4, - child: const CharacterSelectionDialog(), - ), - ); - }, - ); + await showCharacterSelectionDialog(context); }, child: Text(l10n.play), ), diff --git a/lib/game/view/widgets/round_count_display.dart b/lib/game/view/widgets/round_count_display.dart new file mode 100644 index 00000000..63e1fa43 --- /dev/null +++ b/lib/game/view/widgets/round_count_display.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template round_count_display} +/// Colored square indicating if a round is available. +/// {@endtemplate} +class RoundCountDisplay extends StatelessWidget { + /// {@macro round_count_display} + const RoundCountDisplay({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final rounds = context.select((GameBloc bloc) => bloc.state.rounds); + + return Row( + children: [ + Text( + l10n.rounds, + style: Theme.of(context).textTheme.subtitle1, + ), + const SizedBox(width: 8), + Row( + children: [ + RoundIndicator(isActive: rounds >= 1), + RoundIndicator(isActive: rounds >= 2), + RoundIndicator(isActive: rounds >= 3), + ], + ), + ], + ); + } +} + +/// {@template round_indicator} +/// [Widget] that displays the round indicator. +/// {@endtemplate} +@visibleForTesting +class RoundIndicator extends StatelessWidget { + /// {@macro round_indicator} + const RoundIndicator({ + Key? key, + required this.isActive, + }) : super(key: key); + + /// A value that describes whether the indicator is active. + final bool isActive; + + @override + Widget build(BuildContext context) { + final color = + isActive ? PinballColors.yellow : PinballColors.yellow.withAlpha(128); + const size = 8.0; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Container( + color: color, + height: size, + width: size, + ), + ); + } +} diff --git a/lib/game/view/widgets/score_view.dart b/lib/game/view/widgets/score_view.dart new file mode 100644 index 00000000..1fe57eb1 --- /dev/null +++ b/lib/game/view/widgets/score_view.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template score_view} +/// [Widget] that displays the score. +/// {@endtemplate} +class ScoreView extends StatelessWidget { + /// {@macro score_view} + const ScoreView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: AnimatedSwitcher( + duration: kThemeAnimationDuration, + child: isGameOver ? const _GameOver() : const _ScoreDisplay(), + ), + ); + } +} + +class _GameOver extends StatelessWidget { + const _GameOver({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Text( + l10n.gameOver, + style: Theme.of(context).textTheme.headline1, + ); + } +} + +class _ScoreDisplay extends StatelessWidget { + const _ScoreDisplay({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + l10n.score.toLowerCase(), + style: Theme.of(context).textTheme.subtitle1, + ), + const _ScoreText(), + const RoundCountDisplay(), + ], + ); + } +} + +class _ScoreText extends StatelessWidget { + const _ScoreText({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final score = context.select((GameBloc bloc) => bloc.state.score); + + return Text( + score.formatScore(), + style: Theme.of(context).textTheme.headline1, + ); + } +} diff --git a/lib/game/view/widgets/widgets.dart b/lib/game/view/widgets/widgets.dart index 674577af..5d1fccf8 100644 --- a/lib/game/view/widgets/widgets.dart +++ b/lib/game/view/widgets/widgets.dart @@ -1,3 +1,5 @@ export 'bonus_animation.dart'; export 'game_hud.dart'; export 'play_button_overlay.dart'; +export 'round_count_display.dart'; +export 'score_view.dart'; diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 3e52e399..9559fd45 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -14,26 +14,27 @@ class $AssetsImagesGen { const $AssetsImagesBonusAnimationGen(); $AssetsImagesComponentsGen get components => const $AssetsImagesComponentsGen(); + $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); } class $AssetsImagesBonusAnimationGen { const $AssetsImagesBonusAnimationGen(); - /// File path: assets/images/bonus_animation/android.png - AssetGenImage get android => - const AssetGenImage('assets/images/bonus_animation/android.png'); + /// File path: assets/images/bonus_animation/android_spaceship.png + AssetGenImage get androidSpaceship => const AssetGenImage( + 'assets/images/bonus_animation/android_spaceship.png'); /// File path: assets/images/bonus_animation/dash_nest.png AssetGenImage get dashNest => const AssetGenImage('assets/images/bonus_animation/dash_nest.png'); - /// File path: assets/images/bonus_animation/dino.png - AssetGenImage get dino => - const AssetGenImage('assets/images/bonus_animation/dino.png'); + /// File path: assets/images/bonus_animation/dino_chomp.png + AssetGenImage get dinoChomp => + const AssetGenImage('assets/images/bonus_animation/dino_chomp.png'); - /// File path: assets/images/bonus_animation/google.png - AssetGenImage get google => - const AssetGenImage('assets/images/bonus_animation/google.png'); + /// File path: assets/images/bonus_animation/google_word.png + AssetGenImage get googleWord => + const AssetGenImage('assets/images/bonus_animation/google_word.png'); /// File path: assets/images/bonus_animation/sparky_turbo_charge.png AssetGenImage get sparkyTurboCharge => const AssetGenImage( @@ -46,6 +47,22 @@ class $AssetsImagesComponentsGen { /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); + + /// File path: assets/images/components/key.png + AssetGenImage get key => + const AssetGenImage('assets/images/components/key.png'); + + /// File path: assets/images/components/space.png + AssetGenImage get space => + const AssetGenImage('assets/images/components/space.png'); +} + +class $AssetsImagesScoreGen { + const $AssetsImagesScoreGen(); + + /// File path: assets/images/score/mini_score_background.png + AssetGenImage get miniScoreBackground => + const AssetGenImage('assets/images/score/mini_score_background.png'); } class Assets { diff --git a/lib/how_to_play/how_to_play.dart b/lib/how_to_play/how_to_play.dart new file mode 100644 index 00000000..e691bba3 --- /dev/null +++ b/lib/how_to_play/how_to_play.dart @@ -0,0 +1 @@ +export 'widgets/widgets.dart'; diff --git a/lib/how_to_play/widgets/how_to_play_dialog.dart b/lib/how_to_play/widgets/how_to_play_dialog.dart new file mode 100644 index 00000000..766944b9 --- /dev/null +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -0,0 +1,305 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:pinball/gen/gen.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_ui/pinball_ui.dart'; +import 'package:platform_helper/platform_helper.dart'; + +enum Control { + left, + right, + down, + a, + d, + s, + space, +} + +extension on Control { + bool get isArrow => isDown || isRight || isLeft; + + bool get isDown => this == Control.down; + + bool get isRight => this == Control.right; + + bool get isLeft => this == Control.left; + + bool get isSpace => this == Control.space; + + String getCharacter(BuildContext context) { + switch (this) { + case Control.a: + return 'A'; + case Control.d: + return 'D'; + case Control.down: + return '>'; // Will be rotated + case Control.left: + return '<'; + case Control.right: + return '>'; + case Control.s: + return 'S'; + case Control.space: + return context.l10n.space; + } + } +} + +Future showHowToPlayDialog(BuildContext context) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (_) => HowToPlayDialog(), + ); +} + +class HowToPlayDialog extends StatefulWidget { + HowToPlayDialog({ + Key? key, + @visibleForTesting PlatformHelper? platformHelper, + }) : platformHelper = platformHelper ?? PlatformHelper(), + super(key: key); + + final PlatformHelper platformHelper; + + @override + State createState() => _HowToPlayDialogState(); +} + +class _HowToPlayDialogState extends State { + late Timer closeTimer; + @override + void initState() { + super.initState(); + closeTimer = Timer(const Duration(seconds: 3), () { + if (mounted) { + Navigator.of(context).pop(); + } + }); + } + + @override + void dispose() { + closeTimer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isMobile = widget.platformHelper.isMobile; + final l10n = context.l10n; + return PinballDialog( + title: l10n.howToPlay, + subtitle: l10n.tipsForFlips, + child: isMobile ? const _MobileBody() : const _DesktopBody(), + ); + } +} + +class _MobileBody extends StatelessWidget { + const _MobileBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final paddingWidth = MediaQuery.of(context).size.width * 0.15; + final paddingHeight = MediaQuery.of(context).size.height * 0.075; + return FittedBox( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: paddingWidth, + ), + child: Column( + children: [ + const _MobileLaunchControls(), + SizedBox(height: paddingHeight), + const _MobileFlipperControls(), + ], + ), + ), + ); + } +} + +class _MobileLaunchControls extends StatelessWidget { + const _MobileLaunchControls({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final headline3 = Theme.of(context) + .textTheme + .headline3! + .copyWith(color: PinballColors.white); + return Column( + children: [ + Text(l10n.tapAndHoldRocket, style: headline3), + Text.rich( + TextSpan( + children: [ + TextSpan(text: '${l10n.to} ', style: headline3), + TextSpan( + text: l10n.launch, + style: headline3.copyWith(color: PinballColors.blue), + ), + ], + ), + ), + ], + ); + } +} + +class _MobileFlipperControls extends StatelessWidget { + const _MobileFlipperControls({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final headline3 = Theme.of(context) + .textTheme + .headline3! + .copyWith(color: PinballColors.white); + return Column( + children: [ + Text(l10n.tapLeftRightScreen, style: headline3), + Text.rich( + TextSpan( + children: [ + TextSpan(text: '${l10n.to} ', style: headline3), + TextSpan( + text: l10n.flip, + style: headline3.copyWith(color: PinballColors.orange), + ), + ], + ), + ), + ], + ); + } +} + +class _DesktopBody extends StatelessWidget { + const _DesktopBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListView( + children: const [ + SizedBox(height: 16), + _DesktopLaunchControls(), + SizedBox(height: 16), + _DesktopFlipperControls(), + ], + ); + } +} + +class _DesktopLaunchControls extends StatelessWidget { + const _DesktopLaunchControls({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Column( + children: [ + Text( + l10n.launchControls, + style: Theme.of(context).textTheme.headline4, + ), + const SizedBox(height: 10), + Wrap( + children: const [ + _KeyButton(control: Control.down), + SizedBox(width: 10), + _KeyButton(control: Control.space), + SizedBox(width: 10), + _KeyButton(control: Control.s), + ], + ) + ], + ); + } +} + +class _DesktopFlipperControls extends StatelessWidget { + const _DesktopFlipperControls({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Column( + children: [ + Text( + l10n.flipperControls, + style: Theme.of(context).textTheme.subtitle2, + ), + const SizedBox(height: 10), + Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + _KeyButton(control: Control.left), + SizedBox(width: 20), + _KeyButton(control: Control.right), + ], + ), + const SizedBox(height: 8), + Wrap( + children: const [ + _KeyButton(control: Control.a), + SizedBox(width: 20), + _KeyButton(control: Control.d), + ], + ) + ], + ) + ], + ); + } +} + +class _KeyButton extends StatelessWidget { + const _KeyButton({Key? key, required this.control}) : super(key: key); + + final Control control; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final textStyle = + control.isArrow ? textTheme.headline1 : textTheme.headline3; + const height = 60.0; + final width = control.isSpace ? height * 2.83 : height; + return DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.fill, + image: AssetImage( + control.isSpace + ? Assets.images.components.space.keyName + : Assets.images.components.key.keyName, + ), + ), + ), + child: SizedBox( + width: width, + height: height, + child: Center( + child: RotatedBox( + quarterTurns: control.isDown ? 1 : 0, + child: Text( + control.getCharacter(context), + style: textStyle?.copyWith(color: PinballColors.white), + ), + ), + ), + ), + ); + } +} diff --git a/lib/start_game/widgets/widgets.dart b/lib/how_to_play/widgets/widgets.dart similarity index 100% rename from lib/start_game/widgets/widgets.dart rename to lib/how_to_play/widgets/widgets.dart diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c551535f..562d9b1f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -8,6 +8,10 @@ "@howToPlay": { "description": "Text displayed on the landing page how to play button" }, + "tipsForFlips": "Tips for flips", + "@tipsForFlips": { + "description": "Text displayed on the landing page how to play button" + }, "launchControls": "Launch Controls", "@launchControls": { "description": "Text displayed on the how to play dialog with the launch controls" @@ -16,21 +20,45 @@ "@flipperControls": { "description": "Text displayed on the how to play dialog with the flipper controls" }, + "tapAndHoldRocket": "Tap & Hold Rocket", + "@tapAndHoldRocket": { + "description": "Text displayed on the how to launch on mobile" + }, + "to": "to", + "@to": { + "description": "Text displayed for the word to" + }, + "launch": "LAUNCH", + "@launch": { + "description": "Text displayed for the word launch" + }, + "tapLeftRightScreen": "Tap left/right screen", + "@tapLeftRightScreen": { + "description": "Text displayed on the how to flip on mobile" + }, + "flip": "FLIP", + "@flip": { + "description": "Text displayed for the word FLIP" + }, "start": "Start", "@start": { "description": "Text displayed on the character selection page start button" }, "select": "Select", "@select": { - "description": "Text displayed on the character selection page select button" + "description": "Text displayed on the character selection dialog - select button" }, - "characterSelectionTitle": "Choose your character!", + "space": "Space", + "@space": { + "description": "Text displayed on space control button" + }, + "characterSelectionTitle": "Select a Character", "@characterSelectionTitle": { - "description": "Title text displayed on the character selection page" + "description": "Title text displayed on the character selection dialog" }, - "characterSelectionSubtitle": "There’s no wrong answer", + "characterSelectionSubtitle": "There’s no wrong choice!", "@characterSelectionSubtitle": { - "description": "Text displayed on the selecting character dialog at game beginning" + "description": "Subtitle text displayed on the character selection dialog" }, "gameOver": "Game Over", "@gameOver": { @@ -75,5 +103,25 @@ "enterInitials": "Enter your initials", "@enterInitials": { "description": "Text displayed on the ending dialog when game finishes to ask the user for his initials" + }, + "rounds": "Ball Ct:", + "@rounds": { + "description": "Text displayed on the scoreboard widget to indicate rounds left" + }, + "footerMadeWithText": "Made with ", + "@footerMadeWithText": { + "description": "Text shown on the footer which mentions technologies used to build the app." + }, + "footerFlutterLinkText": "Flutter", + "@footerFlutterLinkText": { + "description": "Text on the link shown on the footer which navigates to the Flutter page" + }, + "footerFirebaseLinkText": "Firebase", + "@footerFirebaseLinkText": { + "description": "Text on the link shown on the footer which navigates to the Firebase page" + }, + "footerGoogleIOText": "Google I/O", + "@footerGoogleIOText": { + "description": "Text shown on the footer which mentions Google I/O" } } diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb deleted file mode 100644 index 597a39d8..00000000 --- a/lib/l10n/arb/app_es.arb +++ /dev/null @@ -1,15 +0,0 @@ -{ - "@@locale": "es", - "play": "Jugar", - "@play": { - "description": "Text displayed on the landing page play button" - }, - "start": "Comienzo", - "@start": { - "description": "Text displayed on the character selection page start button" - }, - "characterSelectionTitle": "¡Elige a tu personaje!", - "@characterSelectionTitle": { - "description": "Title text displayed on the character selection page" - } -} \ No newline at end of file diff --git a/lib/leaderboard/bloc/leaderboard_bloc.dart b/lib/leaderboard/bloc/leaderboard_bloc.dart deleted file mode 100644 index 49a35474..00000000 --- a/lib/leaderboard/bloc/leaderboard_bloc.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; - -part 'leaderboard_event.dart'; -part 'leaderboard_state.dart'; - -/// {@template leaderboard_bloc} -/// Manages leaderboard events. -/// -/// Uses a [LeaderboardRepository] to request and update players participations. -/// {@endtemplate} -class LeaderboardBloc extends Bloc { - /// {@macro leaderboard_bloc} - LeaderboardBloc(this._leaderboardRepository) - : super(const LeaderboardState.initial()) { - on(_onTop10Fetched); - on(_onLeaderboardEntryAdded); - } - - final LeaderboardRepository _leaderboardRepository; - - Future _onTop10Fetched( - Top10Fetched event, - Emitter emit, - ) async { - emit(state.copyWith(status: LeaderboardStatus.loading)); - try { - final top10Leaderboard = - await _leaderboardRepository.fetchTop10Leaderboard(); - - final leaderboardEntries = []; - top10Leaderboard.asMap().forEach( - (index, value) => leaderboardEntries.add(value.toEntry(index + 1)), - ); - - emit( - state.copyWith( - status: LeaderboardStatus.success, - leaderboard: leaderboardEntries, - ), - ); - } catch (error) { - emit(state.copyWith(status: LeaderboardStatus.error)); - addError(error); - } - } - - Future _onLeaderboardEntryAdded( - LeaderboardEntryAdded event, - Emitter emit, - ) async { - emit(state.copyWith(status: LeaderboardStatus.loading)); - try { - final ranking = - await _leaderboardRepository.addLeaderboardEntry(event.entry); - emit( - state.copyWith( - status: LeaderboardStatus.success, - ranking: ranking, - ), - ); - } catch (error) { - emit(state.copyWith(status: LeaderboardStatus.error)); - addError(error); - } - } -} diff --git a/lib/leaderboard/bloc/leaderboard_event.dart b/lib/leaderboard/bloc/leaderboard_event.dart deleted file mode 100644 index b9e6955a..00000000 --- a/lib/leaderboard/bloc/leaderboard_event.dart +++ /dev/null @@ -1,36 +0,0 @@ -part of 'leaderboard_bloc.dart'; - -/// {@template leaderboard_event} -/// Represents the events available for [LeaderboardBloc]. -/// {endtemplate} -abstract class LeaderboardEvent extends Equatable { - /// {@macro leaderboard_event} - const LeaderboardEvent(); -} - -/// {@template top_10_fetched} -/// Request the top 10 [LeaderboardEntryData]s. -/// {endtemplate} -class Top10Fetched extends LeaderboardEvent { - /// {@macro top_10_fetched} - const Top10Fetched(); - - @override - List get props => []; -} - -/// {@template leaderboard_entry_added} -/// Writes a new [LeaderboardEntryData]. -/// -/// Should be added when a player finishes a game. -/// {endtemplate} -class LeaderboardEntryAdded extends LeaderboardEvent { - /// {@macro leaderboard_entry_added} - const LeaderboardEntryAdded({required this.entry}); - - /// [LeaderboardEntryData] to be written to the remote storage. - final LeaderboardEntryData entry; - - @override - List get props => [entry]; -} diff --git a/lib/leaderboard/bloc/leaderboard_state.dart b/lib/leaderboard/bloc/leaderboard_state.dart deleted file mode 100644 index 20d68f0d..00000000 --- a/lib/leaderboard/bloc/leaderboard_state.dart +++ /dev/null @@ -1,59 +0,0 @@ -// ignore_for_file: public_member_api_docs - -part of 'leaderboard_bloc.dart'; - -/// Defines the request status. -enum LeaderboardStatus { - /// Request is being loaded. - loading, - - /// Request was processed successfully and received a valid response. - success, - - /// Request was processed unsuccessfully and received an error. - error, -} - -/// {@template leaderboard_state} -/// Represents the state of the leaderboard. -/// {@endtemplate} -class LeaderboardState extends Equatable { - /// {@macro leaderboard_state} - const LeaderboardState({ - required this.status, - required this.ranking, - required this.leaderboard, - }); - - const LeaderboardState.initial() - : status = LeaderboardStatus.loading, - ranking = const LeaderboardRanking( - ranking: 0, - outOf: 0, - ), - leaderboard = const []; - - /// The current [LeaderboardStatus] of the state. - final LeaderboardStatus status; - - /// Rank of the current player. - final LeaderboardRanking ranking; - - /// List of top-ranked players. - final List leaderboard; - - @override - List get props => [status, ranking, leaderboard]; - - LeaderboardState copyWith({ - LeaderboardStatus? status, - LeaderboardRanking? ranking, - List? leaderboard, - }) { - return LeaderboardState( - status: status ?? this.status, - ranking: ranking ?? this.ranking, - leaderboard: leaderboard ?? this.leaderboard, - ); - } -} diff --git a/lib/leaderboard/leaderboard.dart b/lib/leaderboard/leaderboard.dart deleted file mode 100644 index 08765743..00000000 --- a/lib/leaderboard/leaderboard.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'bloc/leaderboard_bloc.dart'; -export 'models/leader_board_entry.dart'; -export 'view/leaderboard_page.dart'; diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart deleted file mode 100644 index 61e63d75..00000000 --- a/lib/leaderboard/view/leaderboard_page.dart +++ /dev/null @@ -1,306 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball/theme/theme.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -class LeaderboardPage extends StatelessWidget { - const LeaderboardPage({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - static Route route({required CharacterTheme theme}) { - return MaterialPageRoute( - builder: (_) => LeaderboardPage(theme: theme), - ); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => LeaderboardBloc( - context.read(), - )..add(const Top10Fetched()), - child: LeaderboardView(theme: theme), - ); - } -} - -class LeaderboardView extends StatelessWidget { - const LeaderboardView({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Scaffold( - body: Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 80), - Text( - l10n.leaderboard, - style: Theme.of(context).textTheme.headline3, - ), - const SizedBox(height: 80), - BlocBuilder( - builder: (context, state) { - switch (state.status) { - case LeaderboardStatus.loading: - return _LeaderboardLoading(theme: theme); - case LeaderboardStatus.success: - return _LeaderboardRanking( - ranking: state.leaderboard, - theme: theme, - ); - case LeaderboardStatus.error: - return _LeaderboardError(theme: theme); - } - }, - ), - const SizedBox(height: 20), - TextButton( - onPressed: () => Navigator.of(context).push( - CharacterSelectionDialog.route(), - ), - child: Text(l10n.retry), - ), - ], - ), - ), - ), - ); - } -} - -class _LeaderboardLoading extends StatelessWidget { - const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return const Center( - child: CircularProgressIndicator(), - ); - } -} - -class _LeaderboardError extends StatelessWidget { - const _LeaderboardError({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), - child: Text( - 'There was en error loading data!', - style: - Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red), - ), - ); - } -} - -class _LeaderboardRanking extends StatelessWidget { - const _LeaderboardRanking({ - Key? key, - required this.ranking, - required this.theme, - }) : super(key: key); - - final List ranking; - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _LeaderboardHeaders(theme: theme), - _LeaderboardList( - ranking: ranking, - theme: theme, - ), - ], - ), - ); - } -} - -class _LeaderboardHeaders extends StatelessWidget { - const _LeaderboardHeaders({Key? key, required this.theme}) : super(key: key); - - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _LeaderboardHeaderItem(title: l10n.rank, theme: theme), - _LeaderboardHeaderItem(title: l10n.character, theme: theme), - _LeaderboardHeaderItem(title: l10n.username, theme: theme), - _LeaderboardHeaderItem(title: l10n.score, theme: theme), - ], - ); - } -} - -class _LeaderboardHeaderItem extends StatelessWidget { - const _LeaderboardHeaderItem({ - Key? key, - required this.title, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - final String title; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: theme.ballColor, - ), - child: Text( - title, - style: Theme.of(context).textTheme.headline5, - ), - ), - ); - } -} - -class _LeaderboardList extends StatelessWidget { - const _LeaderboardList({ - Key? key, - required this.ranking, - required this.theme, - }) : super(key: key); - - final List ranking; - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return ListView.builder( - shrinkWrap: true, - itemBuilder: (_, index) => _LeaderBoardCompetitor( - entry: ranking[index], - theme: theme, - ), - itemCount: ranking.length, - ); - } -} - -class _LeaderBoardCompetitor extends StatelessWidget { - const _LeaderBoardCompetitor({ - Key? key, - required this.entry, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - - final LeaderboardEntry entry; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _LeaderboardCompetitorField( - text: entry.rank, - theme: theme, - ), - _LeaderboardCompetitorCharacter( - characterAsset: entry.character, - theme: theme, - ), - _LeaderboardCompetitorField( - text: entry.playerInitials, - theme: theme, - ), - _LeaderboardCompetitorField( - text: entry.score.toString(), - theme: theme, - ), - ], - ); - } -} - -class _LeaderboardCompetitorField extends StatelessWidget { - const _LeaderboardCompetitorField({ - Key? key, - required this.text, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - final String text; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: theme.ballColor, - width: 2, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(text), - ), - ), - ); - } -} - -class _LeaderboardCompetitorCharacter extends StatelessWidget { - const _LeaderboardCompetitorCharacter({ - Key? key, - required this.characterAsset, - required this.theme, - }) : super(key: key); - - final CharacterTheme theme; - final AssetGenImage characterAsset; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: theme.ballColor, - width: 2, - ), - ), - child: SizedBox( - height: 30, - child: characterAsset.image(), - ), - ), - ); - } -} diff --git a/lib/main_development.dart b/lib/main_development.dart index 8944073d..529c66e2 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -5,16 +5,27 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'dart:async'; + +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/bootstrap.dart'; import 'package:pinball_audio/pinball_audio.dart'; void main() { - bootstrap((firestore) async { + bootstrap((firestore, firebaseAuth) async { final leaderboardRepository = LeaderboardRepository(firestore); + final authenticationRepository = AuthenticationRepository(firebaseAuth); final pinballAudio = PinballAudio(); + unawaited( + Firebase.initializeApp().then( + (_) => authenticationRepository.authenticateAnonymously(), + ), + ); return App( + authenticationRepository: authenticationRepository, leaderboardRepository: leaderboardRepository, pinballAudio: pinballAudio, ); diff --git a/lib/main_production.dart b/lib/main_production.dart index 8944073d..529c66e2 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -5,16 +5,27 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'dart:async'; + +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/bootstrap.dart'; import 'package:pinball_audio/pinball_audio.dart'; void main() { - bootstrap((firestore) async { + bootstrap((firestore, firebaseAuth) async { final leaderboardRepository = LeaderboardRepository(firestore); + final authenticationRepository = AuthenticationRepository(firebaseAuth); final pinballAudio = PinballAudio(); + unawaited( + Firebase.initializeApp().then( + (_) => authenticationRepository.authenticateAnonymously(), + ), + ); return App( + authenticationRepository: authenticationRepository, leaderboardRepository: leaderboardRepository, pinballAudio: pinballAudio, ); diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 8944073d..529c66e2 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -5,16 +5,27 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'dart:async'; + +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/bootstrap.dart'; import 'package:pinball_audio/pinball_audio.dart'; void main() { - bootstrap((firestore) async { + bootstrap((firestore, firebaseAuth) async { final leaderboardRepository = LeaderboardRepository(firestore); + final authenticationRepository = AuthenticationRepository(firebaseAuth); final pinballAudio = PinballAudio(); + unawaited( + Firebase.initializeApp().then( + (_) => authenticationRepository.authenticateAnonymously(), + ), + ); return App( + authenticationRepository: authenticationRepository, leaderboardRepository: leaderboardRepository, pinballAudio: pinballAudio, ); diff --git a/lib/theme/cubit/theme_cubit.dart b/lib/select_character/cubit/character_theme_cubit.dart similarity index 59% rename from lib/theme/cubit/theme_cubit.dart rename to lib/select_character/cubit/character_theme_cubit.dart index 94eba4a6..84792a71 100644 --- a/lib/theme/cubit/theme_cubit.dart +++ b/lib/select_character/cubit/character_theme_cubit.dart @@ -5,12 +5,12 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:pinball_theme/pinball_theme.dart'; -part 'theme_state.dart'; +part 'character_theme_state.dart'; -class ThemeCubit extends Cubit { - ThemeCubit() : super(const ThemeState.initial()); +class CharacterThemeCubit extends Cubit { + CharacterThemeCubit() : super(const CharacterThemeState.initial()); void characterSelected(CharacterTheme characterTheme) { - emit(ThemeState(PinballTheme(characterTheme: characterTheme))); + emit(CharacterThemeState(characterTheme)); } } diff --git a/lib/select_character/cubit/character_theme_state.dart b/lib/select_character/cubit/character_theme_state.dart new file mode 100644 index 00000000..a1669f69 --- /dev/null +++ b/lib/select_character/cubit/character_theme_state.dart @@ -0,0 +1,23 @@ +// ignore_for_file: public_member_api_docs +// TODO(allisonryan0002): Document this section when the API is stable. + +part of 'character_theme_cubit.dart'; + +class CharacterThemeState extends Equatable { + const CharacterThemeState(this.characterTheme); + + const CharacterThemeState.initial() : characterTheme = const DashTheme(); + + final CharacterTheme characterTheme; + + bool get isSparkySelected => characterTheme == const SparkyTheme(); + + bool get isDashSelected => characterTheme == const DashTheme(); + + bool get isAndroidSelected => characterTheme == const AndroidTheme(); + + bool get isDinoSelected => characterTheme == const DinoTheme(); + + @override + List get props => [characterTheme]; +} diff --git a/lib/select_character/select_character.dart b/lib/select_character/select_character.dart new file mode 100644 index 00000000..40699840 --- /dev/null +++ b/lib/select_character/select_character.dart @@ -0,0 +1,2 @@ +export 'cubit/character_theme_cubit.dart'; +export 'view/view.dart'; diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart new file mode 100644 index 00000000..1df01ad7 --- /dev/null +++ b/lib/select_character/view/character_selection_page.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/select_character/cubit/character_theme_cubit.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_theme/pinball_theme.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// Inflates [CharacterSelectionDialog] using [showDialog]. +Future showCharacterSelectionDialog(BuildContext context) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const CharacterSelectionDialog(), + ); +} + +/// {@template character_selection_dialog} +/// Dialog used to select the playing character of the game. +/// {@endtemplate character_selection_dialog} +class CharacterSelectionDialog extends StatelessWidget { + /// {@macro character_selection_dialog} + const CharacterSelectionDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return PinballDialog( + title: l10n.characterSelectionTitle, + subtitle: l10n.characterSelectionSubtitle, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + children: [ + Expanded(child: _CharacterPreview()), + Expanded(child: _CharacterGrid()), + ], + ), + ), + const SizedBox(height: 8), + const _SelectCharacterButton(), + ], + ), + ), + ); + } +} + +class _SelectCharacterButton extends StatelessWidget { + const _SelectCharacterButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return PinballButton( + onTap: () async { + Navigator.of(context).pop(); + await showHowToPlayDialog(context); + }, + text: l10n.select, + ); + } +} + +class _CharacterGrid extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + _Character( + key: const Key('sparky_character_selection'), + character: const SparkyTheme(), + isSelected: state.isSparkySelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('android_character_selection'), + character: const AndroidTheme(), + isSelected: state.isAndroidSelected, + ), + ], + ), + const SizedBox(width: 6), + Column( + children: [ + _Character( + key: const Key('dash_character_selection'), + character: const DashTheme(), + isSelected: state.isDashSelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('dino_character_selection'), + character: const DinoTheme(), + isSelected: state.isDinoSelected, + ), + ], + ), + ], + ); + }, + ); + } +} + +class _CharacterPreview extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + state.characterTheme.name, + style: Theme.of(context).textTheme.headline2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Expanded(child: state.characterTheme.icon.image()), + ], + ); + }, + ); + } +} + +class _Character extends StatelessWidget { + const _Character({ + Key? key, + required this.character, + required this.isSelected, + }) : super(key: key); + + final CharacterTheme character; + final bool isSelected; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Opacity( + opacity: isSelected ? 1 : 0.3, + child: InkWell( + onTap: () => + context.read().characterSelected(character), + child: character.icon.image(fit: BoxFit.contain), + ), + ), + ); + } +} diff --git a/lib/theme/view/view.dart b/lib/select_character/view/view.dart similarity index 100% rename from lib/theme/view/view.dart rename to lib/select_character/view/view.dart diff --git a/lib/start_game/start_game.dart b/lib/start_game/start_game.dart index 1556b533..7171c66d 100644 --- a/lib/start_game/start_game.dart +++ b/lib/start_game/start_game.dart @@ -1,2 +1 @@ export 'bloc/start_game_bloc.dart'; -export 'widgets/widgets.dart'; diff --git a/lib/start_game/widgets/how_to_play_dialog.dart b/lib/start_game/widgets/how_to_play_dialog.dart deleted file mode 100644 index aed7a3e3..00000000 --- a/lib/start_game/widgets/how_to_play_dialog.dart +++ /dev/null @@ -1,161 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:pinball/l10n/l10n.dart'; - -class HowToPlayDialog extends StatelessWidget { - const HowToPlayDialog({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - const spacing = SizedBox(height: 16); - - return Dialog( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(l10n.howToPlay), - spacing, - const _LaunchControls(), - spacing, - const _FlipperControls(), - ], - ), - ), - ); - } -} - -class _LaunchControls extends StatelessWidget { - const _LaunchControls({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - const spacing = SizedBox(width: 10); - - return Column( - children: [ - Text(l10n.launchControls), - const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down), - spacing, - KeyIndicator.fromKeyName(keyName: 'SPACE'), - spacing, - KeyIndicator.fromKeyName(keyName: 'S'), - ], - ) - ], - ); - } -} - -class _FlipperControls extends StatelessWidget { - const _FlipperControls({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - const rowSpacing = SizedBox(width: 20); - - return Column( - children: [ - Text(l10n.flipperControls), - const SizedBox(height: 10), - Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left), - rowSpacing, - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_right), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - KeyIndicator.fromKeyName(keyName: 'A'), - rowSpacing, - KeyIndicator.fromKeyName(keyName: 'D'), - ], - ) - ], - ) - ], - ); - } -} - -// TODO(allisonryan0002): remove visibility when adding final UI. -@visibleForTesting -class KeyIndicator extends StatelessWidget { - const KeyIndicator._({ - Key? key, - required String keyName, - required IconData keyIcon, - required bool fromIcon, - }) : _keyName = keyName, - _keyIcon = keyIcon, - _fromIcon = fromIcon, - super(key: key); - - const KeyIndicator.fromKeyName({Key? key, required String keyName}) - : this._( - key: key, - keyName: keyName, - keyIcon: Icons.keyboard_arrow_down, - fromIcon: false, - ); - - const KeyIndicator.fromIcon({Key? key, required IconData keyIcon}) - : this._( - key: key, - keyName: '', - keyIcon: keyIcon, - fromIcon: true, - ); - - final String _keyName; - - final IconData _keyIcon; - - final bool _fromIcon; - - @override - Widget build(BuildContext context) { - const iconPadding = EdgeInsets.all(15); - const textPadding = EdgeInsets.symmetric(vertical: 20, horizontal: 22); - final boarderColor = Colors.blue.withOpacity(0.5); - final color = Colors.blue.withOpacity(0.7); - - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - border: Border.all( - color: boarderColor, - width: 3, - ), - ), - child: _fromIcon - ? Padding( - padding: iconPadding, - child: Icon(_keyIcon, color: color), - ) - : Padding( - padding: textPadding, - child: Text(_keyName, style: TextStyle(color: color)), - ), - ); - } -} diff --git a/lib/theme/app_text_style.dart b/lib/theme/app_text_style.dart deleted file mode 100644 index 068f1eb9..00000000 --- a/lib/theme/app_text_style.dart +++ /dev/null @@ -1,35 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:flutter/widgets.dart'; -import 'package:pinball/theme/theme.dart'; -import 'package:pinball_components/pinball_components.dart'; - -const _fontPackage = 'pinball_components'; -const _primaryFontFamily = PinballFonts.pixeloidSans; - -abstract class AppTextStyle { - static const headline1 = TextStyle( - fontSize: 28, - package: _fontPackage, - fontFamily: _primaryFontFamily, - ); - - static const headline2 = TextStyle( - fontSize: 24, - package: _fontPackage, - fontFamily: _primaryFontFamily, - ); - - static const headline3 = TextStyle( - color: AppColors.white, - fontSize: 20, - package: _fontPackage, - fontFamily: _primaryFontFamily, - ); - - static const subtitle1 = TextStyle( - fontSize: 10, - fontFamily: _primaryFontFamily, - package: _fontPackage, - ); -} diff --git a/lib/theme/cubit/theme_state.dart b/lib/theme/cubit/theme_state.dart deleted file mode 100644 index 078f5c84..00000000 --- a/lib/theme/cubit/theme_state.dart +++ /dev/null @@ -1,16 +0,0 @@ -// ignore_for_file: public_member_api_docs -// TODO(allisonryan0002): Document this section when the API is stable. - -part of 'theme_cubit.dart'; - -class ThemeState extends Equatable { - const ThemeState(this.theme); - - const ThemeState.initial() - : theme = const PinballTheme(characterTheme: DashTheme()); - - final PinballTheme theme; - - @override - List get props => [theme]; -} diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart deleted file mode 100644 index 5e4fefe9..00000000 --- a/lib/theme/theme.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'app_colors.dart'; -export 'app_text_style.dart'; -export 'cubit/theme_cubit.dart'; -export 'view/view.dart'; diff --git a/lib/theme/view/character_selection_page.dart b/lib/theme/view/character_selection_page.dart deleted file mode 100644 index ef37270b..00000000 --- a/lib/theme/view/character_selection_page.dart +++ /dev/null @@ -1,132 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/start_game/start_game.dart'; -import 'package:pinball/theme/theme.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -class CharacterSelectionDialog extends StatelessWidget { - const CharacterSelectionDialog({Key? key}) : super(key: key); - - static Route route() { - return MaterialPageRoute( - builder: (_) => const CharacterSelectionDialog(), - ); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ThemeCubit(), - child: const CharacterSelectionView(), - ); - } -} - -class CharacterSelectionView extends StatelessWidget { - const CharacterSelectionView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return Scaffold( - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 80), - Text( - l10n.characterSelectionTitle, - style: Theme.of(context).textTheme.headline3, - ), - const SizedBox(height: 80), - const _CharacterSelectionGridView(), - const SizedBox(height: 20), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - showDialog( - context: context, - builder: (_) => const HowToPlayDialog(), - ); - }, - child: Text(l10n.start), - ), - ], - ), - ), - ); - } -} - -class _CharacterSelectionGridView extends StatelessWidget { - const _CharacterSelectionGridView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), - child: GridView.count( - shrinkWrap: true, - crossAxisCount: 2, - mainAxisSpacing: 20, - crossAxisSpacing: 20, - children: const [ - CharacterImageButton( - DashTheme(), - key: Key('characterSelectionPage_dashButton'), - ), - CharacterImageButton( - SparkyTheme(), - key: Key('characterSelectionPage_sparkyButton'), - ), - CharacterImageButton( - AndroidTheme(), - key: Key('characterSelectionPage_androidButton'), - ), - CharacterImageButton( - DinoTheme(), - key: Key('characterSelectionPage_dinoButton'), - ), - ], - ), - ); - } -} - -// TODO(allisonryan0002): remove visibility when adding final UI. -@visibleForTesting -class CharacterImageButton extends StatelessWidget { - const CharacterImageButton( - this.characterTheme, { - Key? key, - }) : super(key: key); - - final CharacterTheme characterTheme; - - @override - Widget build(BuildContext context) { - final currentCharacterTheme = context.select( - (cubit) => cubit.state.theme.characterTheme, - ); - - return GestureDetector( - onTap: () => context.read().characterSelected(characterTheme), - child: DecoratedBox( - decoration: BoxDecoration( - color: (currentCharacterTheme == characterTheme) - ? Colors.blue.withOpacity(0.5) - : null, - borderRadius: BorderRadius.circular(6), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: characterTheme.icon.image(), - ), - ), - ); - } -} diff --git a/packages/authentication_repository/.gitignore b/packages/authentication_repository/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/authentication_repository/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/authentication_repository/README.md b/packages/authentication_repository/README.md new file mode 100644 index 00000000..8f56b868 --- /dev/null +++ b/packages/authentication_repository/README.md @@ -0,0 +1,11 @@ +# authentication_repository + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Repository to manage user authentication. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/authentication_repository/analysis_options.yaml b/packages/authentication_repository/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/authentication_repository/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/authentication_repository/lib/authentication_repository.dart b/packages/authentication_repository/lib/authentication_repository.dart new file mode 100644 index 00000000..77b1b6b9 --- /dev/null +++ b/packages/authentication_repository/lib/authentication_repository.dart @@ -0,0 +1,3 @@ +library authentication_repository; + +export 'src/authentication_repository.dart'; diff --git a/packages/authentication_repository/lib/src/authentication_repository.dart b/packages/authentication_repository/lib/src/authentication_repository.dart new file mode 100644 index 00000000..9f252518 --- /dev/null +++ b/packages/authentication_repository/lib/src/authentication_repository.dart @@ -0,0 +1,36 @@ +import 'package:firebase_auth/firebase_auth.dart'; + +/// {@template authentication_exception} +/// Exception for authentication repository failures. +/// {@endtemplate} +class AuthenticationException implements Exception { + /// {@macro authentication_exception} + const AuthenticationException(this.error, this.stackTrace); + + /// The error that was caught. + final Object error; + + /// The Stacktrace associated with the [error]. + final StackTrace stackTrace; +} + +/// {@template authentication_repository} +/// Repository to manage user authentication. +/// {@endtemplate} +class AuthenticationRepository { + /// {@macro authentication_repository} + AuthenticationRepository(this._firebaseAuth); + + final FirebaseAuth _firebaseAuth; + + /// Sign in the existing user anonymously using [FirebaseAuth]. If the + /// authentication process can't be completed, it will throw an + /// [AuthenticationException]. + Future authenticateAnonymously() async { + try { + await _firebaseAuth.signInAnonymously(); + } on Exception catch (error, stackTrace) { + throw AuthenticationException(error, stackTrace); + } + } +} diff --git a/packages/authentication_repository/pubspec.yaml b/packages/authentication_repository/pubspec.yaml new file mode 100644 index 00000000..bac20507 --- /dev/null +++ b/packages/authentication_repository/pubspec.yaml @@ -0,0 +1,18 @@ +name: authentication_repository +description: Repository to manage user authentication. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + firebase_auth: ^3.3.16 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^0.2.0 + very_good_analysis: ^2.4.0 \ No newline at end of file diff --git a/packages/authentication_repository/test/src/authentication_repository_test.dart b/packages/authentication_repository/test/src/authentication_repository_test.dart new file mode 100644 index 00000000..a179bb68 --- /dev/null +++ b/packages/authentication_repository/test/src/authentication_repository_test.dart @@ -0,0 +1,40 @@ +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +class MockUserCredential extends Mock implements UserCredential {} + +void main() { + late FirebaseAuth firebaseAuth; + late UserCredential userCredential; + late AuthenticationRepository authenticationRepository; + + group('AuthenticationRepository', () { + setUp(() { + firebaseAuth = MockFirebaseAuth(); + userCredential = MockUserCredential(); + authenticationRepository = AuthenticationRepository(firebaseAuth); + }); + + group('authenticateAnonymously', () { + test('completes if no exception is thrown', () async { + when(() => firebaseAuth.signInAnonymously()) + .thenAnswer((_) async => userCredential); + await authenticationRepository.authenticateAnonymously(); + verify(() => firebaseAuth.signInAnonymously()).called(1); + }); + + test('throws AuthenticationException when firebase auth fails', () async { + when(() => firebaseAuth.signInAnonymously()) + .thenThrow(Exception('oops')); + expect( + () => authenticationRepository.authenticateAnonymously(), + throwsA(isA()), + ); + }); + }); + }); +} diff --git a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart index 9d8b2434..c522584c 100644 --- a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart +++ b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart @@ -1,91 +1,6 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; -/// {@template leaderboard_exception} -/// Base exception for leaderboard repository failures. -/// {@endtemplate} -abstract class LeaderboardException implements Exception { - /// {@macro leaderboard_exception} - const LeaderboardException(this.error, this.stackTrace); - - /// The error that was caught. - final Object error; - - /// The Stacktrace associated with the [error]. - final StackTrace stackTrace; -} - -/// {@template leaderboard_deserialization_exception} -/// Exception thrown when leaderboard data cannot be deserialized in the -/// expected way. -/// {@endtemplate} -class LeaderboardDeserializationException extends LeaderboardException { - /// {@macro leaderboard_deserialization_exception} - const LeaderboardDeserializationException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - -/// {@template fetch_top_10_leaderboard_exception} -/// Exception thrown when failure occurs while fetching top 10 leaderboard. -/// {@endtemplate} -class FetchTop10LeaderboardException extends LeaderboardException { - /// {@macro fetch_top_10_leaderboard_exception} - const FetchTop10LeaderboardException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - -/// {@template add_leaderboard_entry_exception} -/// Exception thrown when failure occurs while adding entry to leaderboard. -/// {@endtemplate} -class AddLeaderboardEntryException extends LeaderboardException { - /// {@macro add_leaderboard_entry_exception} - const AddLeaderboardEntryException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - -/// {@template fetch_player_ranking_exception} -/// Exception thrown when failure occurs while fetching player ranking. -/// {@endtemplate} -class FetchPlayerRankingException extends LeaderboardException { - /// {@macro fetch_player_ranking_exception} - const FetchPlayerRankingException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - -/// {@template fetch_prohibited_initials_exception} -/// Exception thrown when failure occurs while fetching prohibited initials. -/// {@endtemplate} -class FetchProhibitedInitialsException extends LeaderboardException { - /// {@macro fetch_prohibited_initials_exception} - const FetchProhibitedInitialsException( - Object error, - StackTrace stackTrace, - ) : super( - error, - stackTrace, - ); -} - /// {@template leaderboard_repository} /// Repository to access leaderboard data in Firebase Cloud Firestore. /// {@endtemplate} @@ -97,73 +12,40 @@ class LeaderboardRepository { final FirebaseFirestore _firebaseFirestore; + static const _leaderboardLimit = 10; + static const _leaderboardCollectionName = 'leaderboard'; + static const _scoreFieldName = 'score'; + /// Acquires top 10 [LeaderboardEntryData]s. Future> fetchTop10Leaderboard() async { - final leaderboardEntries = []; - late List documents; - try { final querySnapshot = await _firebaseFirestore - .collection('leaderboard') - .orderBy('score', descending: true) - .limit(10) + .collection(_leaderboardCollectionName) + .orderBy(_scoreFieldName, descending: true) + .limit(_leaderboardLimit) .get(); - documents = querySnapshot.docs; + final documents = querySnapshot.docs; + return documents.toLeaderboard(); + } on LeaderboardDeserializationException { + rethrow; } on Exception catch (error, stackTrace) { throw FetchTop10LeaderboardException(error, stackTrace); } - - for (final document in documents) { - final data = document.data() as Map?; - if (data != null) { - try { - leaderboardEntries.add(LeaderboardEntryData.fromJson(data)); - } catch (error, stackTrace) { - throw LeaderboardDeserializationException(error, stackTrace); - } - } - } - - return leaderboardEntries; } - /// Adds player's score entry to the leaderboard and gets their - /// [LeaderboardRanking]. - Future addLeaderboardEntry( + /// Adds player's score entry to the leaderboard if it is within the top-10 + Future addLeaderboardEntry( LeaderboardEntryData entry, ) async { - late DocumentReference entryReference; - try { - entryReference = await _firebaseFirestore - .collection('leaderboard') - .add(entry.toJson()); - } on Exception catch (error, stackTrace) { - throw AddLeaderboardEntryException(error, stackTrace); - } - - try { - final querySnapshot = await _firebaseFirestore - .collection('leaderboard') - .orderBy('score', descending: true) - .get(); - - // TODO(allisonryan0002): see if we can find a more performant solution. - final documents = querySnapshot.docs; - final ranking = documents.indexWhere( - (document) => document.id == entryReference.id, - ) + - 1; - - if (ranking > 0) { - return LeaderboardRanking(ranking: ranking, outOf: documents.length); - } else { - throw FetchPlayerRankingException( - 'Player score could not be found and ranking cannot be provided.', - StackTrace.current, - ); + final leaderboard = await _fetchLeaderboardSortedByScore(); + if (leaderboard.length < 10) { + await _saveScore(entry); + } else { + final tenthPositionScore = leaderboard[9].score; + if (entry.score > tenthPositionScore) { + await _saveScore(entry); + await _deleteScoresUnder(tenthPositionScore); } - } on Exception catch (error, stackTrace) { - throw FetchPlayerRankingException(error, stackTrace); } } @@ -174,7 +56,6 @@ class LeaderboardRepository { if (!initialsRegex.hasMatch(initials)) { return false; } - try { final document = await _firebaseFirestore .collection('prohibitedInitials') @@ -187,4 +68,61 @@ class LeaderboardRepository { throw FetchProhibitedInitialsException(error, stackTrace); } } + + Future> _fetchLeaderboardSortedByScore() async { + try { + final querySnapshot = await _firebaseFirestore + .collection(_leaderboardCollectionName) + .orderBy(_scoreFieldName, descending: true) + .get(); + final documents = querySnapshot.docs; + return documents.toLeaderboard(); + } on Exception catch (error, stackTrace) { + throw FetchLeaderboardException(error, stackTrace); + } + } + + Future _saveScore(LeaderboardEntryData entry) { + try { + return _firebaseFirestore + .collection(_leaderboardCollectionName) + .add(entry.toJson()); + } on Exception catch (error, stackTrace) { + throw AddLeaderboardEntryException(error, stackTrace); + } + } + + Future _deleteScoresUnder(int score) async { + try { + final querySnapshot = await _firebaseFirestore + .collection(_leaderboardCollectionName) + .where(_scoreFieldName, isLessThanOrEqualTo: score) + .get(); + final documents = querySnapshot.docs; + for (final document in documents) { + await document.reference.delete(); + } + } on LeaderboardDeserializationException { + rethrow; + } on Exception catch (error, stackTrace) { + throw DeleteLeaderboardException(error, stackTrace); + } + } +} + +extension on List { + List toLeaderboard() { + final leaderboardEntries = []; + for (final document in this) { + final data = document.data() as Map?; + if (data != null) { + try { + leaderboardEntries.add(LeaderboardEntryData.fromJson(data)); + } catch (error, stackTrace) { + throw LeaderboardDeserializationException(error, stackTrace); + } + } + } + return leaderboardEntries; + } } diff --git a/packages/leaderboard_repository/lib/src/models/exceptions.dart b/packages/leaderboard_repository/lib/src/models/exceptions.dart new file mode 100644 index 00000000..f709a27e --- /dev/null +++ b/packages/leaderboard_repository/lib/src/models/exceptions.dart @@ -0,0 +1,69 @@ +/// {@template leaderboard_exception} +/// Base exception for leaderboard repository failures. +/// {@endtemplate} +abstract class LeaderboardException implements Exception { + /// {@macro leaderboard_exception} + const LeaderboardException(this.error, this.stackTrace); + + /// The error that was caught. + final Object error; + + /// The Stacktrace associated with the [error]. + final StackTrace stackTrace; +} + +/// {@template leaderboard_deserialization_exception} +/// Exception thrown when leaderboard data cannot be deserialized in the +/// expected way. +/// {@endtemplate} +class LeaderboardDeserializationException extends LeaderboardException { + /// {@macro leaderboard_deserialization_exception} + const LeaderboardDeserializationException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template fetch_top_10_leaderboard_exception} +/// Exception thrown when failure occurs while fetching top 10 leaderboard. +/// {@endtemplate} +class FetchTop10LeaderboardException extends LeaderboardException { + /// {@macro fetch_top_10_leaderboard_exception} + const FetchTop10LeaderboardException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template fetch_leaderboard_exception} +/// Exception thrown when failure occurs while fetching the leaderboard. +/// {@endtemplate} +class FetchLeaderboardException extends LeaderboardException { + /// {@macro fetch_top_10_leaderboard_exception} + const FetchLeaderboardException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template delete_leaderboard_exception} +/// Exception thrown when failure occurs while deleting the leaderboard under +/// the tenth position. +/// {@endtemplate} +class DeleteLeaderboardException extends LeaderboardException { + /// {@macro fetch_top_10_leaderboard_exception} + const DeleteLeaderboardException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template add_leaderboard_entry_exception} +/// Exception thrown when failure occurs while adding entry to leaderboard. +/// {@endtemplate} +class AddLeaderboardEntryException extends LeaderboardException { + /// {@macro add_leaderboard_entry_exception} + const AddLeaderboardEntryException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} + +/// {@template fetch_prohibited_initials_exception} +/// Exception thrown when failure occurs while fetching prohibited initials. +/// {@endtemplate} +class FetchProhibitedInitialsException extends LeaderboardException { + /// {@macro fetch_prohibited_initials_exception} + const FetchProhibitedInitialsException(Object error, StackTrace stackTrace) + : super(error, stackTrace); +} diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart deleted file mode 100644 index 4a322e00..00000000 --- a/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; - -/// {@template leaderboard_ranking} -/// Contains [ranking] for a single [LeaderboardEntryData] and the number of -/// players the [ranking] is [outOf]. -/// {@endtemplate} -class LeaderboardRanking extends Equatable { - /// {@macro leaderboard_ranking} - const LeaderboardRanking({required this.ranking, required this.outOf}); - - /// Place ranking by score for a [LeaderboardEntryData]. - final int ranking; - - /// Number of [LeaderboardEntryData]s at the time of score entry. - final int outOf; - - @override - List get props => [ranking, outOf]; -} diff --git a/packages/leaderboard_repository/lib/src/models/models.dart b/packages/leaderboard_repository/lib/src/models/models.dart index e10a743b..a612f3ac 100644 --- a/packages/leaderboard_repository/lib/src/models/models.dart +++ b/packages/leaderboard_repository/lib/src/models/models.dart @@ -1,2 +1,2 @@ +export 'exceptions.dart'; export 'leaderboard_entry_data.dart'; -export 'leaderboard_ranking.dart'; diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart index 9d31983f..af3c5fa3 100644 --- a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -153,7 +153,6 @@ void main() { character: CharacterType.dash, ); const entryDocumentId = 'id$entryScore'; - final ranking = LeaderboardRanking(ranking: 3, outOf: 4); setUp(() { leaderboardRepository = LeaderboardRepository(firestore); @@ -165,13 +164,12 @@ void main() { final queryDocumentSnapshot = MockQueryDocumentSnapshot(); when(queryDocumentSnapshot.data).thenReturn({ 'character': 'dash', - 'username': 'user$score', + 'playerInitials': 'AAA', 'score': score }); when(() => queryDocumentSnapshot.id).thenReturn('id$score'); return queryDocumentSnapshot; }).toList(); - when(() => firestore.collection('leaderboard')) .thenAnswer((_) => collectionReference); when(() => collectionReference.add(any())) @@ -184,19 +182,29 @@ void main() { }); test( - 'adds leaderboard entry and returns player ranking when ' - 'firestore operations succeed', () async { - final rankingResult = - await leaderboardRepository.addLeaderboardEntry(leaderboardEntry); - - expect(rankingResult, equals(ranking)); + 'throws FetchLeaderboardException ' + 'when querying the leaderboard fails', () { + when(() => firestore.collection('leaderboard')).thenThrow(Exception()); + expect( + () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), + throwsA(isA()), + ); }); test( - 'throws AddLeaderboardEntryException when Exception occurs ' - 'when trying to add entry to firestore', () async { - when(() => firestore.collection('leaderboard')).thenThrow(Exception()); + 'saves the new score if the existing leaderboard ' + 'has less than 10 scores', () async { + await leaderboardRepository.addLeaderboardEntry(leaderboardEntry); + verify( + () => collectionReference.add(leaderboardEntry.toJson()), + ).called(1); + }); + test( + 'throws AddLeaderboardEntryException ' + 'when adding a new entry fails', () async { + when(() => collectionReference.add(leaderboardEntry.toJson())) + .thenThrow(Exception('oops')); expect( () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), throwsA(isA()), @@ -204,26 +212,160 @@ void main() { }); test( - 'throws FetchPlayerRankingException when Exception occurs ' - 'when trying to retrieve information from firestore', () async { - when(() => collectionReference.orderBy('score', descending: true)) - .thenThrow(Exception()); + 'does nothing if there are more than 10 scores in the leaderboard ' + 'and the new score is smaller than the top 10', () async { + final leaderboardScores = [ + 10000, + 9500, + 9000, + 8500, + 8000, + 7500, + 7000, + 6500, + 6000, + 5500, + 5000 + ]; + final queryDocumentSnapshots = leaderboardScores.map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + return queryDocumentSnapshot; + }).toList(); + when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); - expect( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - throwsA(isA()), + await leaderboardRepository.addLeaderboardEntry(leaderboardEntry); + verifyNever( + () => collectionReference.add(leaderboardEntry.toJson()), ); }); test( - 'throws FetchPlayerRankingException when score cannot be found ' - 'in firestore leaderboard data', () async { - when(() => documentReference.id).thenReturn('nonexistentDocumentId'); - + 'throws DeleteLeaderboardException ' + 'when deleting scores outside the top 10 fails', () async { + final deleteQuery = MockQuery(); + final deleteQuerySnapshot = MockQuerySnapshot(); + final newScore = LeaderboardEntryData( + playerInitials: 'ABC', + score: 15000, + character: CharacterType.android, + ); + final leaderboardScores = [ + 10000, + 9500, + 9000, + 8500, + 8000, + 7500, + 7000, + 6500, + 6000, + 5500, + 5000, + ]; + final deleteDocumentSnapshots = [5500, 5000].map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + when(() => queryDocumentSnapshot.reference) + .thenReturn(documentReference); + return queryDocumentSnapshot; + }).toList(); + when(deleteQuery.get).thenAnswer((_) async => deleteQuerySnapshot); + when(() => deleteQuerySnapshot.docs) + .thenReturn(deleteDocumentSnapshots); + final queryDocumentSnapshots = leaderboardScores.map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + when(() => queryDocumentSnapshot.reference) + .thenReturn(documentReference); + return queryDocumentSnapshot; + }).toList(); + when( + () => collectionReference.where('score', isLessThanOrEqualTo: 5500), + ).thenAnswer((_) => deleteQuery); + when(() => documentReference.delete()).thenThrow(Exception('oops')); + when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); expect( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - throwsA(isA()), + () => leaderboardRepository.addLeaderboardEntry(newScore), + throwsA(isA()), + ); + }); + + test( + 'saves the new score when there are more than 10 scores in the ' + 'leaderboard and the new score is higher than the lowest top 10, and ' + 'deletes the scores that are not in the top 10 anymore', () async { + final deleteQuery = MockQuery(); + final deleteQuerySnapshot = MockQuerySnapshot(); + final newScore = LeaderboardEntryData( + playerInitials: 'ABC', + score: 15000, + character: CharacterType.android, ); + final leaderboardScores = [ + 10000, + 9500, + 9000, + 8500, + 8000, + 7500, + 7000, + 6500, + 6000, + 5500, + 5000, + ]; + final deleteDocumentSnapshots = [5500, 5000].map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + when(() => queryDocumentSnapshot.reference) + .thenReturn(documentReference); + return queryDocumentSnapshot; + }).toList(); + when(deleteQuery.get).thenAnswer((_) async => deleteQuerySnapshot); + when(() => deleteQuerySnapshot.docs) + .thenReturn(deleteDocumentSnapshots); + final queryDocumentSnapshots = leaderboardScores.map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'AAA', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + when(() => queryDocumentSnapshot.reference) + .thenReturn(documentReference); + return queryDocumentSnapshot; + }).toList(); + when( + () => collectionReference.where('score', isLessThanOrEqualTo: 5500), + ).thenAnswer((_) => deleteQuery); + when(() => documentReference.delete()) + .thenAnswer((_) async => Future.value()); + when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); + await leaderboardRepository.addLeaderboardEntry(newScore); + verify(() => collectionReference.add(newScore.toJson())).called(1); + verify(() => documentReference.delete()).called(2); }); }); diff --git a/packages/leaderboard_repository/test/src/models/leaderboard_ranking_test.dart b/packages/leaderboard_repository/test/src/models/leaderboard_ranking_test.dart deleted file mode 100644 index 577251e4..00000000 --- a/packages/leaderboard_repository/test/src/models/leaderboard_ranking_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:test/test.dart'; - -void main() { - group('LeaderboardRanking', () { - test('can be instantiated', () { - const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1); - - expect(leaderboardRanking, isNotNull); - }); - - test('supports value equality.', () { - const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1); - const leaderboardRanking2 = LeaderboardRanking(ranking: 1, outOf: 1); - - expect(leaderboardRanking, equals(leaderboardRanking2)); - }); - }); -} diff --git a/packages/pinball_components/assets/images/alien_bumper/a/inactive.png b/packages/pinball_components/assets/images/android/bumper/a/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/alien_bumper/a/inactive.png rename to packages/pinball_components/assets/images/android/bumper/a/dimmed.png diff --git a/packages/pinball_components/assets/images/alien_bumper/a/active.png b/packages/pinball_components/assets/images/android/bumper/a/lit.png similarity index 100% rename from packages/pinball_components/assets/images/alien_bumper/a/active.png rename to packages/pinball_components/assets/images/android/bumper/a/lit.png diff --git a/packages/pinball_components/assets/images/alien_bumper/b/inactive.png b/packages/pinball_components/assets/images/android/bumper/b/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/alien_bumper/b/inactive.png rename to packages/pinball_components/assets/images/android/bumper/b/dimmed.png diff --git a/packages/pinball_components/assets/images/alien_bumper/b/active.png b/packages/pinball_components/assets/images/android/bumper/b/lit.png similarity index 100% rename from packages/pinball_components/assets/images/alien_bumper/b/active.png rename to packages/pinball_components/assets/images/android/bumper/b/lit.png diff --git a/packages/pinball_components/assets/images/android/bumper/cow/dimmed.png b/packages/pinball_components/assets/images/android/bumper/cow/dimmed.png new file mode 100644 index 00000000..6a8bb146 Binary files /dev/null and b/packages/pinball_components/assets/images/android/bumper/cow/dimmed.png differ diff --git a/packages/pinball_components/assets/images/android/bumper/cow/lit.png b/packages/pinball_components/assets/images/android/bumper/cow/lit.png new file mode 100644 index 00000000..4909708b Binary files /dev/null and b/packages/pinball_components/assets/images/android/bumper/cow/lit.png differ diff --git a/packages/pinball_components/assets/images/android/rail/exit.png b/packages/pinball_components/assets/images/android/rail/exit.png new file mode 100644 index 00000000..80a819d0 Binary files /dev/null and b/packages/pinball_components/assets/images/android/rail/exit.png differ diff --git a/packages/pinball_components/assets/images/android/rail/main.png b/packages/pinball_components/assets/images/android/rail/main.png new file mode 100644 index 00000000..9291c784 Binary files /dev/null and b/packages/pinball_components/assets/images/android/rail/main.png differ diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active1.png b/packages/pinball_components/assets/images/android/ramp/arrow/active1.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active1.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active1.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active2.png b/packages/pinball_components/assets/images/android/ramp/arrow/active2.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active2.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active2.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active3.png b/packages/pinball_components/assets/images/android/ramp/arrow/active3.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active3.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active3.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active4.png b/packages/pinball_components/assets/images/android/ramp/arrow/active4.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active4.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active4.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/active5.png b/packages/pinball_components/assets/images/android/ramp/arrow/active5.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/active5.png rename to packages/pinball_components/assets/images/android/ramp/arrow/active5.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/arrow/inactive.png b/packages/pinball_components/assets/images/android/ramp/arrow/inactive.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/arrow/inactive.png rename to packages/pinball_components/assets/images/android/ramp/arrow/inactive.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/board-opening.png b/packages/pinball_components/assets/images/android/ramp/board-opening.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/board-opening.png rename to packages/pinball_components/assets/images/android/ramp/board-opening.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/main.png b/packages/pinball_components/assets/images/android/ramp/main.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/main.png rename to packages/pinball_components/assets/images/android/ramp/main.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/railing-background.png b/packages/pinball_components/assets/images/android/ramp/railing-background.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/railing-background.png rename to packages/pinball_components/assets/images/android/ramp/railing-background.png diff --git a/packages/pinball_components/assets/images/spaceship/ramp/railing-foreground.png b/packages/pinball_components/assets/images/android/ramp/railing-foreground.png similarity index 100% rename from packages/pinball_components/assets/images/spaceship/ramp/railing-foreground.png rename to packages/pinball_components/assets/images/android/ramp/railing-foreground.png diff --git a/packages/pinball_components/assets/images/android/spaceship/animatronic.png b/packages/pinball_components/assets/images/android/spaceship/animatronic.png new file mode 100644 index 00000000..d4b165f3 Binary files /dev/null and b/packages/pinball_components/assets/images/android/spaceship/animatronic.png differ diff --git a/packages/pinball_components/assets/images/android/spaceship/light-beam.png b/packages/pinball_components/assets/images/android/spaceship/light-beam.png new file mode 100644 index 00000000..eb33725d Binary files /dev/null and b/packages/pinball_components/assets/images/android/spaceship/light-beam.png differ diff --git a/packages/pinball_components/assets/images/android/spaceship/saucer.png b/packages/pinball_components/assets/images/android/spaceship/saucer.png new file mode 100644 index 00000000..6c77525a Binary files /dev/null and b/packages/pinball_components/assets/images/android/spaceship/saucer.png differ diff --git a/packages/pinball_components/assets/images/board-background.png b/packages/pinball_components/assets/images/board-background.png new file mode 100644 index 00000000..979a0873 Binary files /dev/null and b/packages/pinball_components/assets/images/board-background.png differ diff --git a/packages/pinball_components/assets/images/boundary/bottom.png b/packages/pinball_components/assets/images/boundary/bottom.png index 90bfa493..806f7051 100644 Binary files a/packages/pinball_components/assets/images/boundary/bottom.png and b/packages/pinball_components/assets/images/boundary/bottom.png differ diff --git a/packages/pinball_components/assets/images/boundary/outer.png b/packages/pinball_components/assets/images/boundary/outer.png index 3c06cb6c..1f3bab69 100644 Binary files a/packages/pinball_components/assets/images/boundary/outer.png and b/packages/pinball_components/assets/images/boundary/outer.png differ diff --git a/packages/pinball_components/assets/images/chrome_dino/head.png b/packages/pinball_components/assets/images/chrome_dino/head.png deleted file mode 100644 index 15be6fcd..00000000 Binary files a/packages/pinball_components/assets/images/chrome_dino/head.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/chrome_dino/mouth.png b/packages/pinball_components/assets/images/chrome_dino/mouth.png deleted file mode 100644 index 3d9caeae..00000000 Binary files a/packages/pinball_components/assets/images/chrome_dino/mouth.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dino/animatronic/head.png b/packages/pinball_components/assets/images/dino/animatronic/head.png new file mode 100644 index 00000000..87332679 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/animatronic/head.png differ diff --git a/packages/pinball_components/assets/images/dino/animatronic/mouth.png b/packages/pinball_components/assets/images/dino/animatronic/mouth.png new file mode 100644 index 00000000..4955bdf3 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/animatronic/mouth.png differ diff --git a/packages/pinball_components/assets/images/dino/bottom-wall.png b/packages/pinball_components/assets/images/dino/bottom-wall.png new file mode 100644 index 00000000..6a20f1a7 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/bottom-wall.png differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-bottom.png b/packages/pinball_components/assets/images/dino/dino-land-bottom.png deleted file mode 100644 index 9aa42e12..00000000 Binary files a/packages/pinball_components/assets/images/dino/dino-land-bottom.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dino/dino-land-top.png b/packages/pinball_components/assets/images/dino/dino-land-top.png deleted file mode 100644 index 18b92541..00000000 Binary files a/packages/pinball_components/assets/images/dino/dino-land-top.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/dino/top-wall.png b/packages/pinball_components/assets/images/dino/top-wall.png new file mode 100644 index 00000000..72b912e3 Binary files /dev/null and b/packages/pinball_components/assets/images/dino/top-wall.png differ diff --git a/packages/pinball_components/assets/images/kicker/left.png b/packages/pinball_components/assets/images/kicker/left.png deleted file mode 100644 index 42bd5030..00000000 Binary files a/packages/pinball_components/assets/images/kicker/left.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/kicker/left/dimmed.png b/packages/pinball_components/assets/images/kicker/left/dimmed.png new file mode 100644 index 00000000..70196876 Binary files /dev/null and b/packages/pinball_components/assets/images/kicker/left/dimmed.png differ diff --git a/packages/pinball_components/assets/images/kicker/left/lit.png b/packages/pinball_components/assets/images/kicker/left/lit.png new file mode 100644 index 00000000..d2f57661 Binary files /dev/null and b/packages/pinball_components/assets/images/kicker/left/lit.png differ diff --git a/packages/pinball_components/assets/images/kicker/right.png b/packages/pinball_components/assets/images/kicker/right.png deleted file mode 100644 index 0a746f3c..00000000 Binary files a/packages/pinball_components/assets/images/kicker/right.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/kicker/right/dimmed.png b/packages/pinball_components/assets/images/kicker/right/dimmed.png new file mode 100644 index 00000000..3e4a3b4f Binary files /dev/null and b/packages/pinball_components/assets/images/kicker/right/dimmed.png differ diff --git a/packages/pinball_components/assets/images/kicker/right/lit.png b/packages/pinball_components/assets/images/kicker/right/lit.png new file mode 100644 index 00000000..cfe992b4 Binary files /dev/null and b/packages/pinball_components/assets/images/kicker/right/lit.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x2/dimmed.png b/packages/pinball_components/assets/images/multiplier/x2/dimmed.png new file mode 100644 index 00000000..7cc9fc4f Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x2/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x2/lit.png b/packages/pinball_components/assets/images/multiplier/x2/lit.png new file mode 100644 index 00000000..be2b3f08 Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x2/lit.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x3/dimmed.png b/packages/pinball_components/assets/images/multiplier/x3/dimmed.png new file mode 100644 index 00000000..460b1a0e Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x3/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x3/lit.png b/packages/pinball_components/assets/images/multiplier/x3/lit.png new file mode 100644 index 00000000..7fdedbbe Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x3/lit.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x4/dimmed.png b/packages/pinball_components/assets/images/multiplier/x4/dimmed.png new file mode 100644 index 00000000..e8a6256e Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x4/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x4/lit.png b/packages/pinball_components/assets/images/multiplier/x4/lit.png new file mode 100644 index 00000000..5beceabb Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x4/lit.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x5/dimmed.png b/packages/pinball_components/assets/images/multiplier/x5/dimmed.png new file mode 100644 index 00000000..96e018e4 Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x5/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x5/lit.png b/packages/pinball_components/assets/images/multiplier/x5/lit.png new file mode 100644 index 00000000..23fd3aab Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x5/lit.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x6/dimmed.png b/packages/pinball_components/assets/images/multiplier/x6/dimmed.png new file mode 100644 index 00000000..d518e1eb Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x6/dimmed.png differ diff --git a/packages/pinball_components/assets/images/multiplier/x6/lit.png b/packages/pinball_components/assets/images/multiplier/x6/lit.png new file mode 100644 index 00000000..54244bab Binary files /dev/null and b/packages/pinball_components/assets/images/multiplier/x6/lit.png differ diff --git a/packages/pinball_components/assets/images/plunger/rocket.png b/packages/pinball_components/assets/images/plunger/rocket.png index ee5eef5b..bef65ea1 100644 Binary files a/packages/pinball_components/assets/images/plunger/rocket.png and b/packages/pinball_components/assets/images/plunger/rocket.png differ diff --git a/packages/pinball_components/assets/images/spaceship/bridge.png b/packages/pinball_components/assets/images/spaceship/bridge.png deleted file mode 100644 index 6ebb143e..00000000 Binary files a/packages/pinball_components/assets/images/spaceship/bridge.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/spaceship/rail/foreground.png b/packages/pinball_components/assets/images/spaceship/rail/foreground.png deleted file mode 100644 index 4d11e865..00000000 Binary files a/packages/pinball_components/assets/images/spaceship/rail/foreground.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/spaceship/rail/main.png b/packages/pinball_components/assets/images/spaceship/rail/main.png deleted file mode 100644 index 4b299c2c..00000000 Binary files a/packages/pinball_components/assets/images/spaceship/rail/main.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/spaceship/saucer.png b/packages/pinball_components/assets/images/spaceship/saucer.png deleted file mode 100644 index 4cd65522..00000000 Binary files a/packages/pinball_components/assets/images/spaceship/saucer.png and /dev/null differ diff --git a/packages/pinball_components/assets/images/sparky/bumper/a/inactive.png b/packages/pinball_components/assets/images/sparky/bumper/a/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/a/inactive.png rename to packages/pinball_components/assets/images/sparky/bumper/a/dimmed.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/a/active.png b/packages/pinball_components/assets/images/sparky/bumper/a/lit.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/a/active.png rename to packages/pinball_components/assets/images/sparky/bumper/a/lit.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/b/inactive.png b/packages/pinball_components/assets/images/sparky/bumper/b/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/b/inactive.png rename to packages/pinball_components/assets/images/sparky/bumper/b/dimmed.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/b/active.png b/packages/pinball_components/assets/images/sparky/bumper/b/lit.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/b/active.png rename to packages/pinball_components/assets/images/sparky/bumper/b/lit.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/c/inactive.png b/packages/pinball_components/assets/images/sparky/bumper/c/dimmed.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/c/inactive.png rename to packages/pinball_components/assets/images/sparky/bumper/c/dimmed.png diff --git a/packages/pinball_components/assets/images/sparky/bumper/c/active.png b/packages/pinball_components/assets/images/sparky/bumper/c/lit.png similarity index 100% rename from packages/pinball_components/assets/images/sparky/bumper/c/active.png rename to packages/pinball_components/assets/images/sparky/bumper/c/lit.png diff --git a/packages/pinball_components/assets/images/sparky/computer/glow.png b/packages/pinball_components/assets/images/sparky/computer/glow.png new file mode 100644 index 00000000..07ffdb0c Binary files /dev/null and b/packages/pinball_components/assets/images/sparky/computer/glow.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index c91736dc..eff80155 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -10,14 +10,15 @@ import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); - $AssetsImagesAlienBumperGen get alienBumper => - const $AssetsImagesAlienBumperGen(); + $AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); + + /// File path: assets/images/board-background.png + AssetGenImage get boardBackground => + const AssetGenImage('assets/images/board-background.png'); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); - $AssetsImagesChromeDinoGen get chromeDino => - const $AssetsImagesChromeDinoGen(); $AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); @@ -26,18 +27,23 @@ class $AssetsImagesGen { $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); + $AssetsImagesMultiplierGen get multiplier => + const $AssetsImagesMultiplierGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); - $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); } -class $AssetsImagesAlienBumperGen { - const $AssetsImagesAlienBumperGen(); +class $AssetsImagesAndroidGen { + const $AssetsImagesAndroidGen(); - $AssetsImagesAlienBumperAGen get a => const $AssetsImagesAlienBumperAGen(); - $AssetsImagesAlienBumperBGen get b => const $AssetsImagesAlienBumperBGen(); + $AssetsImagesAndroidBumperGen get bumper => + const $AssetsImagesAndroidBumperGen(); + $AssetsImagesAndroidRailGen get rail => const $AssetsImagesAndroidRailGen(); + $AssetsImagesAndroidRampGen get ramp => const $AssetsImagesAndroidRampGen(); + $AssetsImagesAndroidSpaceshipGen get spaceship => + const $AssetsImagesAndroidSpaceshipGen(); } class $AssetsImagesBackboardGen { @@ -95,18 +101,6 @@ class $AssetsImagesBoundaryGen { const AssetGenImage('assets/images/boundary/outer.png'); } -class $AssetsImagesChromeDinoGen { - const $AssetsImagesChromeDinoGen(); - - /// File path: assets/images/chrome_dino/head.png - AssetGenImage get head => - const AssetGenImage('assets/images/chrome_dino/head.png'); - - /// File path: assets/images/chrome_dino/mouth.png - AssetGenImage get mouth => - const AssetGenImage('assets/images/chrome_dino/mouth.png'); -} - class $AssetsImagesDashGen { const $AssetsImagesDashGen(); @@ -120,13 +114,16 @@ class $AssetsImagesDashGen { class $AssetsImagesDinoGen { const $AssetsImagesDinoGen(); - /// File path: assets/images/dino/dino-land-bottom.png - AssetGenImage get dinoLandBottom => - const AssetGenImage('assets/images/dino/dino-land-bottom.png'); + $AssetsImagesDinoAnimatronicGen get animatronic => + const $AssetsImagesDinoAnimatronicGen(); + + /// File path: assets/images/dino/bottom-wall.png + AssetGenImage get bottomWall => + const AssetGenImage('assets/images/dino/bottom-wall.png'); - /// File path: assets/images/dino/dino-land-top.png - AssetGenImage get dinoLandTop => - const AssetGenImage('assets/images/dino/dino-land-top.png'); + /// File path: assets/images/dino/top-wall.png + AssetGenImage get topWall => + const AssetGenImage('assets/images/dino/top-wall.png'); } class $AssetsImagesFlipperGen { @@ -161,13 +158,8 @@ class $AssetsImagesGoogleWordGen { class $AssetsImagesKickerGen { const $AssetsImagesKickerGen(); - /// File path: assets/images/kicker/left.png - AssetGenImage get left => - const AssetGenImage('assets/images/kicker/left.png'); - - /// File path: assets/images/kicker/right.png - AssetGenImage get right => - const AssetGenImage('assets/images/kicker/right.png'); + $AssetsImagesKickerLeftGen get left => const $AssetsImagesKickerLeftGen(); + $AssetsImagesKickerRightGen get right => const $AssetsImagesKickerRightGen(); } class $AssetsImagesLaunchRampGen { @@ -186,6 +178,16 @@ class $AssetsImagesLaunchRampGen { const AssetGenImage('assets/images/launch_ramp/ramp.png'); } +class $AssetsImagesMultiplierGen { + const $AssetsImagesMultiplierGen(); + + $AssetsImagesMultiplierX2Gen get x2 => const $AssetsImagesMultiplierX2Gen(); + $AssetsImagesMultiplierX3Gen get x3 => const $AssetsImagesMultiplierX3Gen(); + $AssetsImagesMultiplierX4Gen get x4 => const $AssetsImagesMultiplierX4Gen(); + $AssetsImagesMultiplierX5Gen get x5 => const $AssetsImagesMultiplierX5Gen(); + $AssetsImagesMultiplierX6Gen get x6 => const $AssetsImagesMultiplierX6Gen(); +} + class $AssetsImagesPlungerGen { const $AssetsImagesPlungerGen(); @@ -230,56 +232,79 @@ class $AssetsImagesSlingshotGen { const AssetGenImage('assets/images/slingshot/upper.png'); } -class $AssetsImagesSpaceshipGen { - const $AssetsImagesSpaceshipGen(); - - /// File path: assets/images/spaceship/bridge.png - AssetGenImage get bridge => - const AssetGenImage('assets/images/spaceship/bridge.png'); - - $AssetsImagesSpaceshipRailGen get rail => - const $AssetsImagesSpaceshipRailGen(); - $AssetsImagesSpaceshipRampGen get ramp => - const $AssetsImagesSpaceshipRampGen(); - - /// File path: assets/images/spaceship/saucer.png - AssetGenImage get saucer => - const AssetGenImage('assets/images/spaceship/saucer.png'); -} - class $AssetsImagesSparkyGen { const $AssetsImagesSparkyGen(); + /// File path: assets/images/sparky/animatronic.png AssetGenImage get animatronic => const AssetGenImage('assets/images/sparky/animatronic.png'); + $AssetsImagesSparkyBumperGen get bumper => const $AssetsImagesSparkyBumperGen(); $AssetsImagesSparkyComputerGen get computer => const $AssetsImagesSparkyComputerGen(); } -class $AssetsImagesAlienBumperAGen { - const $AssetsImagesAlienBumperAGen(); +class $AssetsImagesAndroidBumperGen { + const $AssetsImagesAndroidBumperGen(); - /// File path: assets/images/alien_bumper/a/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/alien_bumper/a/active.png'); + $AssetsImagesAndroidBumperAGen get a => + const $AssetsImagesAndroidBumperAGen(); + $AssetsImagesAndroidBumperBGen get b => + const $AssetsImagesAndroidBumperBGen(); + $AssetsImagesAndroidBumperCowGen get cow => + const $AssetsImagesAndroidBumperCowGen(); +} - /// File path: assets/images/alien_bumper/a/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/alien_bumper/a/inactive.png'); +class $AssetsImagesAndroidRailGen { + const $AssetsImagesAndroidRailGen(); + + /// File path: assets/images/android/rail/exit.png + AssetGenImage get exit => + const AssetGenImage('assets/images/android/rail/exit.png'); + + /// File path: assets/images/android/rail/main.png + AssetGenImage get main => + const AssetGenImage('assets/images/android/rail/main.png'); } -class $AssetsImagesAlienBumperBGen { - const $AssetsImagesAlienBumperBGen(); +class $AssetsImagesAndroidRampGen { + const $AssetsImagesAndroidRampGen(); - /// File path: assets/images/alien_bumper/b/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/alien_bumper/b/active.png'); + $AssetsImagesAndroidRampArrowGen get arrow => + const $AssetsImagesAndroidRampArrowGen(); - /// File path: assets/images/alien_bumper/b/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/alien_bumper/b/inactive.png'); + /// File path: assets/images/android/ramp/board-opening.png + AssetGenImage get boardOpening => + const AssetGenImage('assets/images/android/ramp/board-opening.png'); + + /// File path: assets/images/android/ramp/main.png + AssetGenImage get main => + const AssetGenImage('assets/images/android/ramp/main.png'); + + /// File path: assets/images/android/ramp/railing-background.png + AssetGenImage get railingBackground => + const AssetGenImage('assets/images/android/ramp/railing-background.png'); + + /// File path: assets/images/android/ramp/railing-foreground.png + AssetGenImage get railingForeground => + const AssetGenImage('assets/images/android/ramp/railing-foreground.png'); +} + +class $AssetsImagesAndroidSpaceshipGen { + const $AssetsImagesAndroidSpaceshipGen(); + + /// File path: assets/images/android/spaceship/animatronic.png + AssetGenImage get animatronic => + const AssetGenImage('assets/images/android/spaceship/animatronic.png'); + + /// File path: assets/images/android/spaceship/light-beam.png + AssetGenImage get lightBeam => + const AssetGenImage('assets/images/android/spaceship/light-beam.png'); + + /// File path: assets/images/android/spaceship/saucer.png + AssetGenImage get saucer => + const AssetGenImage('assets/images/android/spaceship/saucer.png'); } class $AssetsImagesDashBumperGen { @@ -363,39 +388,100 @@ class $AssetsImagesGoogleWordLetter6Gen { const AssetGenImage('assets/images/google_word/letter6/inactive.png'); } -class $AssetsImagesSpaceshipRailGen { - const $AssetsImagesSpaceshipRailGen(); +class $AssetsImagesDinoAnimatronicGen { + const $AssetsImagesDinoAnimatronicGen(); - /// File path: assets/images/spaceship/rail/foreground.png - AssetGenImage get foreground => - const AssetGenImage('assets/images/spaceship/rail/foreground.png'); + /// File path: assets/images/dino/animatronic/head.png + AssetGenImage get head => + const AssetGenImage('assets/images/dino/animatronic/head.png'); - /// File path: assets/images/spaceship/rail/main.png - AssetGenImage get main => - const AssetGenImage('assets/images/spaceship/rail/main.png'); + /// File path: assets/images/dino/animatronic/mouth.png + AssetGenImage get mouth => + const AssetGenImage('assets/images/dino/animatronic/mouth.png'); } -class $AssetsImagesSpaceshipRampGen { - const $AssetsImagesSpaceshipRampGen(); +class $AssetsImagesKickerLeftGen { + const $AssetsImagesKickerLeftGen(); - $AssetsImagesSpaceshipRampArrowGen get arrow => - const $AssetsImagesSpaceshipRampArrowGen(); + /// File path: assets/images/kicker/left/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/kicker/left/dimmed.png'); - /// File path: assets/images/spaceship/ramp/board-opening.png - AssetGenImage get boardOpening => - const AssetGenImage('assets/images/spaceship/ramp/board-opening.png'); + /// File path: assets/images/kicker/left/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/kicker/left/lit.png'); +} - /// File path: assets/images/spaceship/ramp/main.png - AssetGenImage get main => - const AssetGenImage('assets/images/spaceship/ramp/main.png'); +class $AssetsImagesKickerRightGen { + const $AssetsImagesKickerRightGen(); + + /// File path: assets/images/kicker/right/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/kicker/right/dimmed.png'); + + /// File path: assets/images/kicker/right/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/kicker/right/lit.png'); +} + +class $AssetsImagesMultiplierX2Gen { + const $AssetsImagesMultiplierX2Gen(); + + /// File path: assets/images/multiplier/x2/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x2/dimmed.png'); + + /// File path: assets/images/multiplier/x2/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x2/lit.png'); +} + +class $AssetsImagesMultiplierX3Gen { + const $AssetsImagesMultiplierX3Gen(); + + /// File path: assets/images/multiplier/x3/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x3/dimmed.png'); + + /// File path: assets/images/multiplier/x3/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x3/lit.png'); +} + +class $AssetsImagesMultiplierX4Gen { + const $AssetsImagesMultiplierX4Gen(); + + /// File path: assets/images/multiplier/x4/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x4/dimmed.png'); + + /// File path: assets/images/multiplier/x4/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x4/lit.png'); +} + +class $AssetsImagesMultiplierX5Gen { + const $AssetsImagesMultiplierX5Gen(); - /// File path: assets/images/spaceship/ramp/railing-background.png - AssetGenImage get railingBackground => const AssetGenImage( - 'assets/images/spaceship/ramp/railing-background.png'); + /// File path: assets/images/multiplier/x5/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x5/dimmed.png'); - /// File path: assets/images/spaceship/ramp/railing-foreground.png - AssetGenImage get railingForeground => const AssetGenImage( - 'assets/images/spaceship/ramp/railing-foreground.png'); + /// File path: assets/images/multiplier/x5/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x5/lit.png'); +} + +class $AssetsImagesMultiplierX6Gen { + const $AssetsImagesMultiplierX6Gen(); + + /// File path: assets/images/multiplier/x6/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/multiplier/x6/dimmed.png'); + + /// File path: assets/images/multiplier/x6/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/multiplier/x6/lit.png'); } class $AssetsImagesSparkyBumperGen { @@ -413,11 +499,79 @@ class $AssetsImagesSparkyComputerGen { AssetGenImage get base => const AssetGenImage('assets/images/sparky/computer/base.png'); + /// File path: assets/images/sparky/computer/glow.png + AssetGenImage get glow => + const AssetGenImage('assets/images/sparky/computer/glow.png'); + /// File path: assets/images/sparky/computer/top.png AssetGenImage get top => const AssetGenImage('assets/images/sparky/computer/top.png'); } +class $AssetsImagesAndroidBumperAGen { + const $AssetsImagesAndroidBumperAGen(); + + /// File path: assets/images/android/bumper/a/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/android/bumper/a/dimmed.png'); + + /// File path: assets/images/android/bumper/a/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/android/bumper/a/lit.png'); +} + +class $AssetsImagesAndroidBumperBGen { + const $AssetsImagesAndroidBumperBGen(); + + /// File path: assets/images/android/bumper/b/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/android/bumper/b/dimmed.png'); + + /// File path: assets/images/android/bumper/b/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/android/bumper/b/lit.png'); +} + +class $AssetsImagesAndroidBumperCowGen { + const $AssetsImagesAndroidBumperCowGen(); + + /// File path: assets/images/android/bumper/cow/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/android/bumper/cow/dimmed.png'); + + /// File path: assets/images/android/bumper/cow/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/android/bumper/cow/lit.png'); +} + +class $AssetsImagesAndroidRampArrowGen { + const $AssetsImagesAndroidRampArrowGen(); + + /// File path: assets/images/android/ramp/arrow/active1.png + AssetGenImage get active1 => + const AssetGenImage('assets/images/android/ramp/arrow/active1.png'); + + /// File path: assets/images/android/ramp/arrow/active2.png + AssetGenImage get active2 => + const AssetGenImage('assets/images/android/ramp/arrow/active2.png'); + + /// File path: assets/images/android/ramp/arrow/active3.png + AssetGenImage get active3 => + const AssetGenImage('assets/images/android/ramp/arrow/active3.png'); + + /// File path: assets/images/android/ramp/arrow/active4.png + AssetGenImage get active4 => + const AssetGenImage('assets/images/android/ramp/arrow/active4.png'); + + /// File path: assets/images/android/ramp/arrow/active5.png + AssetGenImage get active5 => + const AssetGenImage('assets/images/android/ramp/arrow/active5.png'); + + /// File path: assets/images/android/ramp/arrow/inactive.png + AssetGenImage get inactive => + const AssetGenImage('assets/images/android/ramp/arrow/inactive.png'); +} + class $AssetsImagesDashBumperAGen { const $AssetsImagesDashBumperAGen(); @@ -454,68 +608,40 @@ class $AssetsImagesDashBumperMainGen { const AssetGenImage('assets/images/dash/bumper/main/inactive.png'); } -class $AssetsImagesSpaceshipRampArrowGen { - const $AssetsImagesSpaceshipRampArrowGen(); - - /// File path: assets/images/spaceship/ramp/arrow/active1.png - AssetGenImage get active1 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active1.png'); - - /// File path: assets/images/spaceship/ramp/arrow/active2.png - AssetGenImage get active2 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active2.png'); - - /// File path: assets/images/spaceship/ramp/arrow/active3.png - AssetGenImage get active3 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active3.png'); - - /// File path: assets/images/spaceship/ramp/arrow/active4.png - AssetGenImage get active4 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active4.png'); - - /// File path: assets/images/spaceship/ramp/arrow/active5.png - AssetGenImage get active5 => - const AssetGenImage('assets/images/spaceship/ramp/arrow/active5.png'); - - /// File path: assets/images/spaceship/ramp/arrow/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/spaceship/ramp/arrow/inactive.png'); -} - class $AssetsImagesSparkyBumperAGen { const $AssetsImagesSparkyBumperAGen(); - /// File path: assets/images/sparky/bumper/a/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/sparky/bumper/a/active.png'); + /// File path: assets/images/sparky/bumper/a/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/sparky/bumper/a/dimmed.png'); - /// File path: assets/images/sparky/bumper/a/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/sparky/bumper/a/inactive.png'); + /// File path: assets/images/sparky/bumper/a/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/sparky/bumper/a/lit.png'); } class $AssetsImagesSparkyBumperBGen { const $AssetsImagesSparkyBumperBGen(); - /// File path: assets/images/sparky/bumper/b/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/sparky/bumper/b/active.png'); + /// File path: assets/images/sparky/bumper/b/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/sparky/bumper/b/dimmed.png'); - /// File path: assets/images/sparky/bumper/b/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/sparky/bumper/b/inactive.png'); + /// File path: assets/images/sparky/bumper/b/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/sparky/bumper/b/lit.png'); } class $AssetsImagesSparkyBumperCGen { const $AssetsImagesSparkyBumperCGen(); - /// File path: assets/images/sparky/bumper/c/active.png - AssetGenImage get active => - const AssetGenImage('assets/images/sparky/bumper/c/active.png'); + /// File path: assets/images/sparky/bumper/c/dimmed.png + AssetGenImage get dimmed => + const AssetGenImage('assets/images/sparky/bumper/c/dimmed.png'); - /// File path: assets/images/sparky/bumper/c/inactive.png - AssetGenImage get inactive => - const AssetGenImage('assets/images/sparky/bumper/c/inactive.png'); + /// File path: assets/images/sparky/bumper/c/lit.png + AssetGenImage get lit => + const AssetGenImage('assets/images/sparky/bumper/c/lit.png'); } class Assets { diff --git a/packages/pinball_components/lib/gen/gen.dart b/packages/pinball_components/lib/gen/gen.dart index 0171b231..ada8b777 100644 --- a/packages/pinball_components/lib/gen/gen.dart +++ b/packages/pinball_components/lib/gen/gen.dart @@ -1,2 +1,3 @@ export 'assets.gen.dart'; +export 'fonts.gen.dart'; export 'pinball_fonts.dart'; diff --git a/packages/pinball_components/lib/src/components/alien_bumper.dart b/packages/pinball_components/lib/src/components/alien_bumper.dart deleted file mode 100644 index 1f96d214..00000000 --- a/packages/pinball_components/lib/src/components/alien_bumper.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'dart:async'; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template alien_bumper} -/// Bumper for area under the [Spaceship]. -/// {@endtemplate} -class AlienBumper extends BodyComponent with InitialPosition { - /// {@macro alien_bumper} - AlienBumper._({ - required double majorRadius, - required double minorRadius, - required String onAssetPath, - required String offAssetPath, - }) : _majorRadius = majorRadius, - _minorRadius = minorRadius, - super( - priority: RenderPriority.alienBumper, - children: [ - _AlienBumperSpriteGroupComponent( - onAssetPath: onAssetPath, - offAssetPath: offAssetPath, - ), - ], - ) { - renderBody = false; - } - - /// {@macro alien_bumper} - AlienBumper.a() - : this._( - majorRadius: 3.52, - minorRadius: 2.97, - onAssetPath: Assets.images.alienBumper.a.active.keyName, - offAssetPath: Assets.images.alienBumper.a.inactive.keyName, - ); - - /// {@macro alien_bumper} - AlienBumper.b() - : this._( - majorRadius: 3.19, - minorRadius: 2.79, - onAssetPath: Assets.images.alienBumper.b.active.keyName, - offAssetPath: Assets.images.alienBumper.b.inactive.keyName, - ); - - final double _majorRadius; - final double _minorRadius; - - @override - Body createBody() { - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: _majorRadius, - minorRadius: _minorRadius, - )..rotate(1.29); - final fixtureDef = FixtureDef( - shape, - restitution: 4, - ); - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - - /// Animates the [AlienBumper]. - Future animate() async { - final spriteGroupComponent = firstChild<_AlienBumperSpriteGroupComponent>() - ?..current = AlienBumperSpriteState.inactive; - await Future.delayed(const Duration(milliseconds: 50)); - spriteGroupComponent?.current = AlienBumperSpriteState.active; - } -} - -/// Indicates the [AlienBumper]'s current sprite state. -@visibleForTesting -enum AlienBumperSpriteState { - /// A lit up bumper. - active, - - /// A dimmed bumper. - inactive, -} - -class _AlienBumperSpriteGroupComponent - extends SpriteGroupComponent with HasGameRef { - _AlienBumperSpriteGroupComponent({ - required String onAssetPath, - required String offAssetPath, - }) : _onAssetPath = onAssetPath, - _offAssetPath = offAssetPath, - super( - anchor: Anchor.center, - position: Vector2(0, -0.1), - ); - - final String _onAssetPath; - final String _offAssetPath; - - @override - Future onLoad() async { - await super.onLoad(); - - final sprites = { - AlienBumperSpriteState.active: - Sprite(gameRef.images.fromCache(_onAssetPath)), - AlienBumperSpriteState.inactive: - Sprite(gameRef.images.fromCache(_offAssetPath)), - }; - this.sprites = sprites; - - current = AlienBumperSpriteState.active; - size = sprites[current]!.originalSize / 10; - } -} diff --git a/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart b/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart new file mode 100644 index 00000000..b1fb642d --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_bumper/android_bumper.dart @@ -0,0 +1,162 @@ +import 'dart:async'; + +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/android_bumper/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/android_bumper_cubit.dart'; + +/// {@template android_bumper} +/// Bumper for area under the [AndroidSpaceship]. +/// {@endtemplate} +class AndroidBumper extends BodyComponent with InitialPosition, ZIndex { + /// {@macro android_bumper} + AndroidBumper._({ + required double majorRadius, + required double minorRadius, + required String litAssetPath, + required String dimmedAssetPath, + required Vector2 spritePosition, + Iterable? children, + required this.bloc, + }) : _majorRadius = majorRadius, + _minorRadius = minorRadius, + super( + renderBody: false, + children: [ + AndroidBumperBallContactBehavior(), + AndroidBumperBlinkingBehavior(), + _AndroidBumperSpriteGroupComponent( + dimmedAssetPath: dimmedAssetPath, + litAssetPath: litAssetPath, + position: spritePosition, + state: bloc.state, + ), + ...?children, + ], + ) { + zIndex = ZIndexes.androidBumper; + } + + /// {@macro android_bumper} + AndroidBumper.a({ + Iterable? children, + }) : this._( + majorRadius: 3.52, + minorRadius: 2.97, + litAssetPath: Assets.images.android.bumper.a.lit.keyName, + dimmedAssetPath: Assets.images.android.bumper.a.dimmed.keyName, + spritePosition: Vector2(0, -0.1), + bloc: AndroidBumperCubit(), + children: children, + ); + + /// {@macro android_bumper} + AndroidBumper.b({ + Iterable? children, + }) : this._( + majorRadius: 3.19, + minorRadius: 2.79, + litAssetPath: Assets.images.android.bumper.b.lit.keyName, + dimmedAssetPath: Assets.images.android.bumper.b.dimmed.keyName, + spritePosition: Vector2(0, -0.1), + bloc: AndroidBumperCubit(), + children: children, + ); + + /// {@macro android_bumper} + AndroidBumper.cow({ + Iterable? children, + }) : this._( + majorRadius: 3.4, + minorRadius: 2.9, + litAssetPath: Assets.images.android.bumper.cow.lit.keyName, + dimmedAssetPath: Assets.images.android.bumper.cow.dimmed.keyName, + spritePosition: Vector2(0, -0.68), + bloc: AndroidBumperCubit(), + children: children, + ); + + /// Creates an [AndroidBumper] without any children. + /// + /// This can be used for testing [AndroidBumper]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + AndroidBumper.test({ + required this.bloc, + }) : _majorRadius = 3.52, + _minorRadius = 2.97; + + final double _majorRadius; + + final double _minorRadius; + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final AndroidBumperCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: _majorRadius, + minorRadius: _minorRadius, + )..rotate(1.29); + final fixtureDef = FixtureDef( + shape, + restitution: 4, + ); + final bodyDef = BodyDef( + position: initialPosition, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _AndroidBumperSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _AndroidBumperSpriteGroupComponent({ + required String litAssetPath, + required String dimmedAssetPath, + required Vector2 position, + required AndroidBumperState state, + }) : _litAssetPath = litAssetPath, + _dimmedAssetPath = dimmedAssetPath, + super( + anchor: Anchor.center, + position: position, + current: state, + ); + + final String _litAssetPath; + final String _dimmedAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state); + + final sprites = { + AndroidBumperState.lit: Sprite( + gameRef.images.fromCache(_litAssetPath), + ), + AndroidBumperState.dimmed: + Sprite(gameRef.images.fromCache(_dimmedAssetPath)), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior.dart new file mode 100644 index 00000000..d28aa39c --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior.dart @@ -0,0 +1,14 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class AndroidBumperBallContactBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} diff --git a/packages/pinball_components/lib/src/components/android_bumper/behaviors/android_bumper_blinking_behavior.dart b/packages/pinball_components/lib/src/components/android_bumper/behaviors/android_bumper_blinking_behavior.dart new file mode 100644 index 00000000..4f7dc135 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_bumper/behaviors/android_bumper_blinking_behavior.dart @@ -0,0 +1,39 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template android_bumper_blinking_behavior} +/// Makes an [AndroidBumper] blink back to [AndroidBumperState.lit] when +/// [AndroidBumperState.dimmed]. +/// {@endtemplate} +class AndroidBumperBlinkingBehavior extends TimerComponent + with ParentIsA { + /// {@macro android_bumper_blinking_behavior} + AndroidBumperBlinkingBehavior() : super(period: 0.05); + + void _onNewState(AndroidBumperState state) { + switch (state) { + case AndroidBumperState.lit: + break; + case AndroidBumperState.dimmed: + timer + ..reset() + ..start(); + break; + } + } + + @override + Future onLoad() async { + await super.onLoad(); + timer.stop(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + timer.stop(); + parent.bloc.onBlinked(); + } +} diff --git a/packages/pinball_components/lib/src/components/android_bumper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/android_bumper/behaviors/behaviors.dart new file mode 100644 index 00000000..f7ce4900 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_bumper/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'android_bumper_ball_contact_behavior.dart'; +export 'android_bumper_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_cubit.dart b/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_cubit.dart new file mode 100644 index 00000000..3e75f890 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'android_bumper_state.dart'; + +class AndroidBumperCubit extends Cubit { + AndroidBumperCubit() : super(AndroidBumperState.lit); + + void onBallContacted() { + emit(AndroidBumperState.dimmed); + } + + void onBlinked() { + emit(AndroidBumperState.lit); + } +} diff --git a/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_state.dart b/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_state.dart new file mode 100644 index 00000000..f101c3e9 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_bumper/cubit/android_bumper_state.dart @@ -0,0 +1,8 @@ +// ignore_for_file: public_member_api_docs + +part of 'android_bumper_cubit.dart'; + +enum AndroidBumperState { + lit, + dimmed, +} diff --git a/packages/pinball_components/lib/src/components/android_spaceship.dart b/packages/pinball_components/lib/src/components/android_spaceship.dart new file mode 100644 index 00000000..81a564e1 --- /dev/null +++ b/packages/pinball_components/lib/src/components/android_spaceship.dart @@ -0,0 +1,212 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; + +class AndroidSpaceship extends Component { + AndroidSpaceship({required Vector2 position}) + : super( + children: [ + _SpaceshipSaucer()..initialPosition = position, + _SpaceshipSaucerSpriteAnimationComponent()..position = position, + _LightBeamSpriteComponent()..position = position + Vector2(2.5, 5), + _AndroidHead()..initialPosition = position + Vector2(0.5, 0.25), + _SpaceshipHole( + outsideLayer: Layer.spaceshipExitRail, + outsidePriority: ZIndexes.ballOnSpaceshipRail, + )..initialPosition = position - Vector2(5.3, -5.4), + _SpaceshipHole( + outsideLayer: Layer.board, + outsidePriority: ZIndexes.ballOnBoard, + )..initialPosition = position - Vector2(-7.5, -1.1), + ], + ); +} + +class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { + _SpaceshipSaucer() : super(renderBody: false) { + layer = Layer.spaceship; + } + + @override + Body createBody() { + final shape = _SpaceshipSaucerShape(); + final bodyDef = BodyDef( + position: initialPosition, + userData: this, + angle: -1.7, + ); + + return world.createBody(bodyDef)..createFixtureFromShape(shape); + } +} + +class _SpaceshipSaucerShape extends ChainShape { + _SpaceshipSaucerShape() { + const minorRadius = 9.75; + const majorRadius = 11.9; + + createChain( + [ + for (var angle = 0.2618; angle <= 6.0214; angle += math.pi / 180) + Vector2( + minorRadius * math.cos(angle), + majorRadius * math.sin(angle), + ), + ], + ); + } +} + +class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef, ZIndex { + _SpaceshipSaucerSpriteAnimationComponent() + : super( + anchor: Anchor.center, + ) { + zIndex = ZIndexes.spaceshipSaucer; + } + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.android.spaceship.saucer.keyName, + ); + + const amountPerRow = 5; + const amountPerColumn = 3; + 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, + ), + ); + } +} + +// TODO(allisonryan0002): add pulsing behavior. +class _LightBeamSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _LightBeamSpriteComponent() + : super( + anchor: Anchor.center, + ) { + zIndex = ZIndexes.spaceshipLightBeam; + } + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.android.spaceship.lightBeam.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} + +class _AndroidHead extends BodyComponent with InitialPosition, Layered, ZIndex { + _AndroidHead() + : super( + children: [_AndroidHeadSpriteAnimationComponent()], + renderBody: false, + ) { + layer = Layer.spaceship; + zIndex = ZIndexes.androidHead; + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: 3.1, + minorRadius: 2, + )..rotate(1.4); + // TODO(allisonryan0002): use bumping behavior. + final fixtureDef = FixtureDef( + shape, + restitution: 0.1, + ); + final bodyDef = BodyDef(position: initialPosition); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent + with HasGameRef { + _AndroidHeadSpriteAnimationComponent() + : super( + anchor: Anchor.center, + position: Vector2(-0.24, -2.6), + ); + + @override + Future onLoad() async { + await super.onLoad(); + + final spriteSheet = gameRef.images.fromCache( + Assets.images.android.spaceship.animatronic.keyName, + ); + + const amountPerRow = 18; + const amountPerColumn = 4; + 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, + ), + ); + } +} + +class _SpaceshipHole extends LayerSensor { + _SpaceshipHole({required Layer outsideLayer, required int outsidePriority}) + : super( + insideLayer: Layer.spaceship, + outsideLayer: outsideLayer, + orientation: LayerEntranceOrientation.down, + insideZIndex: ZIndexes.ballOnSpaceship, + outsideZIndex: outsidePriority, + ) { + layer = Layer.spaceship; + } + + @override + Shape get shape { + return ArcShape( + center: Vector2(0, -3.2), + arcRadius: 5, + angle: 1, + rotation: -2, + ); + } +} diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index b1e2703b..81a57e7c 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -6,16 +6,18 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/widgets.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template ball} /// A solid, [BodyType.dynamic] sphere that rolls and bounces around. /// {@endtemplate} class Ball extends BodyComponent - with Layered, InitialPosition { + with Layered, InitialPosition, ZIndex { /// {@macro ball} Ball({ required this.baseColor, }) : super( + renderBody: false, children: [ _BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), ], @@ -26,7 +28,6 @@ class Ball extends BodyComponent // We need to see what happens if Ball appears from other place like nest // bumper, it will need to explicit change layer to Layer.board then. layer = Layer.board; - renderBody = false; } /// The size of the [Ball]. @@ -115,7 +116,7 @@ class Ball extends BodyComponent math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2), ); - body.gravityOverride = Vector2(positionalXForce, positionalYForce); + body.gravityOverride = Vector2(-positionalXForce, positionalYForce); } } @@ -133,13 +134,14 @@ class _BallSpriteComponent extends SpriteComponent with HasGameRef { } class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent - with HasGameRef { + with HasGameRef, ZIndex { _TurboChargeSpriteAnimationComponent() : super( anchor: const Anchor(0.53, 0.72), - priority: RenderPriority.turboChargeFlame, removeOnFinish: true, - ); + ) { + zIndex = ZIndexes.turboChargeFlame; + } late final Vector2 _textureSize; diff --git a/packages/pinball_components/lib/src/components/baseboard.dart b/packages/pinball_components/lib/src/components/baseboard.dart index 07f39070..47ba4666 100644 --- a/packages/pinball_components/lib/src/components/baseboard.dart +++ b/packages/pinball_components/lib/src/components/baseboard.dart @@ -13,10 +13,9 @@ class Baseboard extends BodyComponent with InitialPosition { required BoardSide side, }) : _side = side, super( + renderBody: false, children: [_BaseboardSpriteComponent(side: side)], - ) { - renderBody = false; - } + ); /// Whether the [Baseboard] is on the left or right side of the board. final BoardSide _side; diff --git a/packages/pinball_components/lib/src/components/board_background_sprite_component.dart b/packages/pinball_components/lib/src/components/board_background_sprite_component.dart new file mode 100644 index 00000000..ba5b430e --- /dev/null +++ b/packages/pinball_components/lib/src/components/board_background_sprite_component.dart @@ -0,0 +1,29 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class BoardBackgroundSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + BoardBackgroundSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(0, -1), + ) { + zIndex = ZIndexes.boardBackground; + } + + @override + Future onLoad() async { + await super.onLoad(); + + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.boardBackground.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/boundaries.dart b/packages/pinball_components/lib/src/components/boundaries.dart index 86e1844e..84d3aaeb 100644 --- a/packages/pinball_components/lib/src/components/boundaries.dart +++ b/packages/pinball_components/lib/src/components/boundaries.dart @@ -4,13 +4,13 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template boundaries} -/// A [Blueprint] which creates the [_BottomBoundary] and [_OuterBoundary]. -///{@endtemplate boundaries} -class Boundaries extends Blueprint { +/// Pinball machine walls. +/// {@endtemplate} +class Boundaries extends Component { /// {@macro boundaries} Boundaries() : super( - components: [ + children: [ _BottomBoundary(), _OuterBoundary(), _OuterBottomBoundarySpriteComponent(), @@ -22,14 +22,14 @@ class Boundaries extends Blueprint { /// Curved boundary at the bottom of the board where the [Ball] exits the field /// of play. /// {@endtemplate bottom_boundary} -class _BottomBoundary extends BodyComponent with InitialPosition { +class _BottomBoundary extends BodyComponent with InitialPosition, ZIndex { /// {@macro bottom_boundary} _BottomBoundary() : super( - priority: RenderPriority.bottomBoundary, + renderBody: false, children: [_BottomBoundarySpriteComponent()], ) { - renderBody = false; + zIndex = ZIndexes.bottomBoundary; } List _createFixtureDefs() { @@ -68,7 +68,7 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { _BottomBoundarySpriteComponent() : super( anchor: Anchor.center, - position: Vector2(-5.4, 55.6), + position: Vector2(-5, 55.6), ); @override @@ -85,19 +85,19 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { } /// {@template outer_boundary} -/// Boundary enclosing the top and left side of the board. The right side of the -/// board is closed by the barrier the [LaunchRamp] creates. +/// Boundary enclosing the top and left side of the board. +/// +/// The right side of the board is closed by the barrier the [LaunchRamp] +/// creates. /// {@endtemplate outer_boundary} -class _OuterBoundary extends BodyComponent with InitialPosition { +class _OuterBoundary extends BodyComponent with InitialPosition, ZIndex { /// {@macro outer_boundary} _OuterBoundary() : super( - priority: RenderPriority.outerBoundary, - children: [ - _OuterBoundarySpriteComponent(), - ], + renderBody: false, + children: [_OuterBoundarySpriteComponent()], ) { - renderBody = false; + zIndex = ZIndexes.outerBoundary; } List _createFixtureDefs() { @@ -106,28 +106,59 @@ class _OuterBoundary extends BodyComponent with InitialPosition { Vector2(3.6, -70.2), Vector2(-14.1, -70.2), ); - final topWallFixtureDef = FixtureDef(topWall); final topLeftCurve = BezierCurveShape( controlPoints: [ - Vector2(-32.3, -57.2), + topWall.vertex1, Vector2(-31.5, -69.9), - Vector2(-14.1, -70.2), + Vector2(-32.3, -57.2), ], ); - final topLeftCurveFixtureDef = FixtureDef(topLeftCurve); - final leftWall = EdgeShape() + final topLeftWall = EdgeShape() ..set( - Vector2(-32.3, -57.2), + topLeftCurve.vertices.last, + Vector2(-33.5, -44), + ); + + final upperLeftWallCurve = BezierCurveShape( + controlPoints: [ + topLeftWall.vertex1, + Vector2(-33.9, -40.7), + Vector2(-32.5, -39), + ], + ); + + final middleLeftWallCurve = BezierCurveShape( + controlPoints: [ + upperLeftWallCurve.vertices.last, + Vector2(-23.2, -31.4), + Vector2(-33.9, -21.8), + ], + ); + + final lowerLeftWallCurve = BezierCurveShape( + controlPoints: [ + middleLeftWallCurve.vertices.last, + Vector2(-32.4, -17.6), + Vector2(-37.3, -11), + ], + ); + + final bottomLeftWall = EdgeShape() + ..set( + lowerLeftWallCurve.vertices.last, Vector2(-43.9, 41.8), ); - final leftWallFixtureDef = FixtureDef(leftWall); return [ - topWallFixtureDef, - topLeftCurveFixtureDef, - leftWallFixtureDef, + FixtureDef(topWall), + FixtureDef(topLeftCurve), + FixtureDef(topLeftWall), + FixtureDef(upperLeftWallCurve), + FixtureDef(middleLeftWallCurve), + FixtureDef(lowerLeftWallCurve), + FixtureDef(bottomLeftWall), ]; } @@ -162,13 +193,14 @@ class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef { } class _OuterBottomBoundarySpriteComponent extends SpriteComponent - with HasGameRef { + with HasGameRef, ZIndex { _OuterBottomBoundarySpriteComponent() : super( - priority: RenderPriority.outerBottomBoundary, anchor: Anchor.center, position: Vector2(0, 71), - ); + ) { + zIndex = ZIndexes.outerBottomBoundary; + } @override Future onLoad() async { diff --git a/packages/pinball_components/lib/src/components/bumping_behavior.dart b/packages/pinball_components/lib/src/components/bumping_behavior.dart new file mode 100644 index 00000000..af0d07c3 --- /dev/null +++ b/packages/pinball_components/lib/src/components/bumping_behavior.dart @@ -0,0 +1,33 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template bumping_behavior} +/// Makes any [BodyComponent] that contacts with [parent] bounce off. +/// {@endtemplate} +class BumpingBehavior extends ContactBehavior { + /// {@macro bumping_behavior} + BumpingBehavior({required double strength}) : _strength = strength; + + /// Determines how strong the bump is. + final double _strength; + + /// This is used to recoginze the current state of a contact manifold in world + /// coordinates. + @visibleForTesting + final WorldManifold worldManifold = WorldManifold(); + + @override + void postSolve(Object other, Contact contact, ContactImpulse impulse) { + super.postSolve(other, contact, impulse); + if (other is! BodyComponent) return; + + contact.getWorldManifold(worldManifold); + other.body.applyLinearImpulse( + worldManifold.normal + ..multiply( + Vector2.all(other.body.mass * _strength), + ), + ); + } +} diff --git a/packages/pinball_components/lib/src/components/chrome_dino.dart b/packages/pinball_components/lib/src/components/chrome_dino.dart index 7846f140..8619874e 100644 --- a/packages/pinball_components/lib/src/components/chrome_dino.dart +++ b/packages/pinball_components/lib/src/components/chrome_dino.dart @@ -1,31 +1,35 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart' hide Timer; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template chrome_dino} -/// Dinosaur that gobbles up a [Ball], swivel his head around, and shoots it -/// back out. +/// Dino that swivels back and forth, opening its mouth to eat a [Ball]. +/// +/// Upon eating a [Ball], the dino rotates and spits the [Ball] out in a +/// different direction. /// {@endtemplate} -class ChromeDino extends BodyComponent with InitialPosition { +class ChromeDino extends BodyComponent with InitialPosition, ZIndex { /// {@macro chrome_dino} ChromeDino() : super( - // TODO(alestiago): Remove once sprites are defined. - paint: Paint()..color = Colors.blue, - priority: RenderPriority.dino, - ); + renderBody: false, + ) { + zIndex = ZIndexes.dino; + } /// The size of the dinosaur mouth. - static final size = Vector2(5, 2.5); + static final size = Vector2(5.5, 5); /// Anchors the [ChromeDino] to the [RevoluteJoint] that controls its arc /// motion. Future<_ChromeDinoJoint> _anchorToJoint() async { - final anchor = _ChromeDinoAnchor(); + // TODO(allisonryan0002): try moving to anchor after new body is defined. + final anchor = _ChromeDinoAnchor() + ..initialPosition = initialPosition + Vector2(9, -4); + await add(anchor); final jointDef = _ChromeDinoAnchorRevoluteJointDef( @@ -42,9 +46,11 @@ class ChromeDino extends BodyComponent with InitialPosition { Future onLoad() async { await super.onLoad(); final joint = await _anchorToJoint(); + const framesInAnimation = 98; + const animationFPS = 1 / 24; await add( TimerComponent( - period: 1, + period: (framesInAnimation / 2) * animationFPS, onTick: joint._swivel, repeat: true, ), @@ -54,44 +60,17 @@ class ChromeDino extends BodyComponent with InitialPosition { List _createFixtureDefs() { final fixtureDefs = []; - // TODO(alestiago): Subject to change when sprites are added. - final box = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2); - final fixtureDef = FixtureDef( - box, - density: 999, - friction: 0.3, - restitution: 0.1, - isSensor: true, - ); - + // TODO(allisonryan0002): Update this shape to better match sprite. + final box = PolygonShape() + ..setAsBox( + size.x / 2, + size.y / 2, + initialPosition + Vector2(-4, 2), + -_ChromeDinoJoint._halfSweepingAngle, + ); + final fixtureDef = FixtureDef(box, density: 1); fixtureDefs.add(fixtureDef); - // FIXME(alestiago): Investigate why adding these fixtures is considered as - // an invalid contact type. - // final upperEdge = EdgeShape() - // ..set( - // Vector2(-size.x / 2, -size.y / 2), - // Vector2(size.x / 2, -size.y / 2), - // ); - // final upperEdgeDef = FixtureDef(upperEdge)..density = 0.5; - // fixtureDefs.add(upperEdgeDef); - - // final lowerEdge = EdgeShape() - // ..set( - // Vector2(-size.x / 2, size.y / 2), - // Vector2(size.x / 2, size.y / 2), - // ); - // final lowerEdgeDef = FixtureDef(lowerEdge)..density = 0.5; - // fixtureDefs.add(lowerEdgeDef); - - // final rightEdge = EdgeShape() - // ..set( - // Vector2(size.x / 2, -size.y / 2), - // Vector2(size.x / 2, size.y / 2), - // ); - // final rightEdgeDef = FixtureDef(rightEdge)..density = 0.5; - // fixtureDefs.add(rightEdgeDef); - return fixtureDefs; } @@ -110,13 +89,18 @@ class ChromeDino extends BodyComponent with InitialPosition { } } -/// {@template flipper_anchor} -/// [JointAnchor] positioned at the end of a [ChromeDino]. -/// {@endtemplate} class _ChromeDinoAnchor extends JointAnchor { - /// {@macro flipper_anchor} - _ChromeDinoAnchor() { - initialPosition = Vector2(ChromeDino.size.x / 2, 0); + _ChromeDinoAnchor(); + + // TODO(allisonryan0002): if these aren't moved when fixing the rendering, see + // if the joint can be created in onMount to resolve render syncing. + @override + Future onLoad() async { + await super.onLoad(); + await addAll([ + _ChromeDinoMouthSprite(), + _ChromeDinoHeadSprite(), + ]); } } @@ -135,22 +119,86 @@ class _ChromeDinoAnchorRevoluteJointDef extends RevoluteJointDef { chromeDino.body.position + anchor.body.position, ); enableLimit = true; - // TODO(alestiago): Apply design angle value. - const angle = math.pi / 3.5; - lowerAngle = -angle / 2; - upperAngle = angle / 2; + lowerAngle = -_ChromeDinoJoint._halfSweepingAngle; + upperAngle = _ChromeDinoJoint._halfSweepingAngle; enableMotor = true; - // TODO(alestiago): Tune this values. - maxMotorTorque = motorSpeed = chromeDino.body.mass * 30; + maxMotorTorque = chromeDino.body.mass * 255; + motorSpeed = 2; } } class _ChromeDinoJoint extends RevoluteJoint { _ChromeDinoJoint(_ChromeDinoAnchorRevoluteJointDef def) : super(def); + static const _halfSweepingAngle = 0.1143; + /// Sweeps the [ChromeDino] up and down repeatedly. void _swivel() { setMotorSpeed(-motorSpeed); } } + +class _ChromeDinoMouthSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoMouthSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: _ChromeDinoJoint._halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.mouth.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; + } +} + +class _ChromeDinoHeadSprite extends SpriteAnimationComponent with HasGameRef { + _ChromeDinoHeadSprite() + : super( + anchor: Anchor(Anchor.center.x + 0.47, Anchor.center.y - 0.29), + angle: _ChromeDinoJoint._halfSweepingAngle, + ); + + @override + Future onLoad() async { + await super.onLoad(); + final image = gameRef.images.fromCache( + Assets.images.dino.animatronic.head.keyName, + ); + + const amountPerRow = 11; + const amountPerColumn = 9; + final textureSize = Vector2( + image.width / amountPerRow, + image.height / amountPerColumn, + ); + size = textureSize / 10; + + final data = SpriteAnimationData.sequenced( + amount: (amountPerColumn * amountPerRow) - 1, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ); + animation = SpriteAnimation.fromFrameData(image, data)..currentIndex = 45; + } +} diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 57e93abb..a0beda53 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,34 +1,36 @@ -export 'alien_bumper.dart'; +export 'android_bumper/android_bumper.dart'; +export 'android_spaceship.dart'; export 'backboard/backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; +export 'board_background_sprite_component.dart'; export 'board_dimensions.dart'; export 'board_side.dart'; export 'boundaries.dart'; export 'camera_zoom.dart'; export 'chrome_dino.dart'; export 'dash_animatronic.dart'; -export 'dash_nest_bumper.dart'; +export 'dash_nest_bumper/dash_nest_bumper.dart'; export 'dino_walls.dart'; export 'fire_effect.dart'; export 'flipper.dart'; -export 'google_letter.dart'; +export 'google_letter/google_letter.dart'; export 'initial_position.dart'; export 'joint_anchor.dart'; -export 'kicker.dart'; +export 'kicker/kicker.dart'; export 'launch_ramp.dart'; export 'layer.dart'; export 'layer_sensor.dart'; +export 'multiplier/multiplier.dart'; export 'plunger.dart'; -export 'render_priority.dart'; export 'rocket.dart'; export 'score_text.dart'; export 'shapes/shapes.dart'; export 'signpost.dart'; export 'slingshot.dart'; -export 'spaceship.dart'; export 'spaceship_rail.dart'; export 'spaceship_ramp.dart'; export 'sparky_animatronic.dart'; -export 'sparky_bumper.dart'; +export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_computer.dart'; +export 'z_indexes.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_animatronic.dart b/packages/pinball_components/lib/src/components/dash_animatronic.dart index 47e1e08f..faa604e9 100644 --- a/packages/pinball_components/lib/src/components/dash_animatronic.dart +++ b/packages/pinball_components/lib/src/components/dash_animatronic.dart @@ -10,7 +10,6 @@ class DashAnimatronic extends SpriteAnimationComponent with HasGameRef { : super( anchor: Anchor.center, playing: false, - priority: RenderPriority.dashAnimatronic, ); @override diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/behaviors.dart new file mode 100644 index 00000000..839cbd67 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'dash_nest_bumper_contact_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_contact_behavior.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_contact_behavior.dart new file mode 100644 index 00000000..829229e4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_contact_behavior.dart @@ -0,0 +1,15 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class DashNestBumperBallContactBehavior + extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit.dart new file mode 100644 index 00000000..8fc6b157 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit.dart @@ -0,0 +1,19 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'dash_nest_bumper_state.dart'; + +class DashNestBumperCubit extends Cubit { + DashNestBumperCubit() : super(DashNestBumperState.inactive); + + /// Event added when the bumper contacts with a ball. + void onBallContacted() { + emit(DashNestBumperState.active); + } + + /// Event added when the bumper should return to its initial configuration. + void onReset() { + emit(DashNestBumperState.inactive); + } +} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_state.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_state.dart new file mode 100644 index 00000000..c169069f --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_state.dart @@ -0,0 +1,10 @@ +part of 'dash_nest_bumper_cubit.dart'; + +/// Indicates the [DashNestBumperCubit]'s current state. +enum DashNestBumperState { + /// A lit up bumper. + active, + + /// A dimmed bumper. + inactive, +} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart similarity index 63% rename from packages/pinball_components/lib/src/components/dash_nest_bumper.dart rename to packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart index 46f96b37..82ec0036 100644 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper.dart +++ b/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart @@ -4,6 +4,10 @@ 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/dash_nest_bumper/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/dash_nest_bumper_cubit.dart'; /// {@template dash_nest_bumper} /// Bumper with a nest appearance. @@ -16,54 +20,87 @@ class DashNestBumper extends BodyComponent with InitialPosition { required String activeAssetPath, required String inactiveAssetPath, required Vector2 spritePosition, + Iterable? children, + required this.bloc, }) : _majorRadius = majorRadius, _minorRadius = minorRadius, super( - priority: RenderPriority.dashBumper, + renderBody: false, children: [ _DashNestBumperSpriteGroupComponent( activeAssetPath: activeAssetPath, inactiveAssetPath: inactiveAssetPath, position: spritePosition, + current: bloc.state, ), + DashNestBumperBallContactBehavior(), + ...?children, ], - ) { - renderBody = false; - } + ); /// {@macro dash_nest_bumper} - DashNestBumper.main() - : this._( + DashNestBumper.main({ + Iterable? children, + }) : this._( majorRadius: 5.1, minorRadius: 3.75, activeAssetPath: Assets.images.dash.bumper.main.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName, spritePosition: Vector2(0, -0.3), + children: children, + bloc: DashNestBumperCubit(), ); /// {@macro dash_nest_bumper} - DashNestBumper.a() - : this._( + DashNestBumper.a({ + Iterable? children, + }) : this._( majorRadius: 3, minorRadius: 2.5, activeAssetPath: Assets.images.dash.bumper.a.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, spritePosition: Vector2(0.35, -1.2), + children: children, + bloc: DashNestBumperCubit(), ); /// {@macro dash_nest_bumper} - DashNestBumper.b() - : this._( + DashNestBumper.b({ + Iterable? children, + }) : this._( majorRadius: 3, minorRadius: 2.5, activeAssetPath: Assets.images.dash.bumper.b.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, spritePosition: Vector2(0.35, -1.2), + children: children, + bloc: DashNestBumperCubit(), ); + /// Creates an [DashNestBumper] without any children. + /// + /// This can be used for testing [DashNestBumper]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + DashNestBumper.test({required this.bloc}) + : _majorRadius = 3, + _minorRadius = 2.5; + final double _majorRadius; final double _minorRadius; + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final DashNestBumperCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + @override Body createBody() { final shape = EllipseShape( @@ -79,41 +116,22 @@ class DashNestBumper extends BodyComponent with InitialPosition { return world.createBody(bodyDef)..createFixture(fixtureDef); } - - /// Activates the [DashNestBumper]. - void activate() { - firstChild<_DashNestBumperSpriteGroupComponent>()?.current = - DashNestBumperSpriteState.active; - } - - /// Deactivates the [DashNestBumper]. - void deactivate() { - firstChild<_DashNestBumperSpriteGroupComponent>()?.current = - DashNestBumperSpriteState.inactive; - } -} - -/// Indicates the [DashNestBumper]'s current sprite state. -@visibleForTesting -enum DashNestBumperSpriteState { - /// A lit up bumper. - active, - - /// A dimmed bumper. - inactive, } class _DashNestBumperSpriteGroupComponent - extends SpriteGroupComponent with HasGameRef { + extends SpriteGroupComponent + with HasGameRef, ParentIsA { _DashNestBumperSpriteGroupComponent({ required String activeAssetPath, required String inactiveAssetPath, required Vector2 position, + required DashNestBumperState current, }) : _activeAssetPath = activeAssetPath, _inactiveAssetPath = inactiveAssetPath, super( anchor: Anchor.center, position: position, + current: current, ); final String _activeAssetPath; @@ -122,15 +140,15 @@ class _DashNestBumperSpriteGroupComponent @override Future onLoad() async { await super.onLoad(); + parent.bloc.stream.listen((state) => current = state); + final sprites = { - DashNestBumperSpriteState.active: + DashNestBumperState.active: Sprite(gameRef.images.fromCache(_activeAssetPath)), - DashNestBumperSpriteState.inactive: + DashNestBumperState.inactive: Sprite(gameRef.images.fromCache(_inactiveAssetPath)), }; this.sprites = sprites; - - current = DashNestBumperSpriteState.inactive; size = sprites[current]!.originalSize / 10; } } diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index 9ce5a523..552293d2 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -6,14 +6,14 @@ import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_flame/pinball_flame.dart'; -/// {@template dinowalls} -/// A [Blueprint] which creates walls for the [ChromeDino]. +/// {@template dino_walls} +/// Walls near the [ChromeDino]. /// {@endtemplate} -class DinoWalls extends Blueprint { - /// {@macro dinowalls} +class DinoWalls extends Component { + /// {@macro dino_walls} DinoWalls() : super( - components: [ + children: [ _DinoTopWall(), _DinoBottomWall(), ], @@ -23,63 +23,60 @@ class DinoWalls extends Blueprint { /// {@template dino_top_wall} /// Wall segment located above [ChromeDino]. /// {@endtemplate} -class _DinoTopWall extends BodyComponent with InitialPosition { +class _DinoTopWall extends BodyComponent with InitialPosition, ZIndex { ///{@macro dino_top_wall} _DinoTopWall() : super( - priority: RenderPriority.dinoTopWall, children: [_DinoTopWallSpriteComponent()], + renderBody: false, ) { - renderBody = false; + zIndex = ZIndexes.dinoTopWall; } List _createFixtureDefs() { - final fixturesDef = []; - final topStraightShape = EdgeShape() ..set( - Vector2(28.65, -35.1), - Vector2(29.5, -35.1), + Vector2(28.65, -34.3), + Vector2(29.5, -34.3), ); - final topStraightFixtureDef = FixtureDef(topStraightShape); - fixturesDef.add(topStraightFixtureDef); final topCurveShape = BezierCurveShape( controlPoints: [ topStraightShape.vertex1, - Vector2(17.4, -26.38), - Vector2(25.5, -20.7), + Vector2(18.8, -26.2), + Vector2(26.6, -20.2), ], ); - fixturesDef.add(FixtureDef(topCurveShape)); final middleCurveShape = BezierCurveShape( controlPoints: [ topCurveShape.vertices.last, - Vector2(27.8, -20.1), - Vector2(26.8, -19.5), + Vector2(27.8, -19.3), + Vector2(26.8, -18.7), ], ); - fixturesDef.add(FixtureDef(middleCurveShape)); final bottomCurveShape = BezierCurveShape( controlPoints: [ middleCurveShape.vertices.last, - Vector2(21.5, -15.8), - Vector2(25.8, -14.8), + Vector2(23, -14.2), + Vector2(27, -14.2), ], ); - fixturesDef.add(FixtureDef(bottomCurveShape)); final bottomStraightShape = EdgeShape() ..set( bottomCurveShape.vertices.last, - Vector2(31, -14.5), + Vector2(31, -13.7), ); - final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); - fixturesDef.add(bottomStraightFixtureDef); - return fixturesDef; + return [ + FixtureDef(topStraightShape), + FixtureDef(topCurveShape), + FixtureDef(middleCurveShape), + FixtureDef(bottomCurveShape), + FixtureDef(bottomStraightShape), + ]; } @override @@ -106,92 +103,63 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.dino.dinoLandTop.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.topWall.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(22, -41.8); + position = Vector2(22.8, -38.1); } } /// {@template dino_bottom_wall} /// Wall segment located below [ChromeDino]. /// {@endtemplate} -class _DinoBottomWall extends BodyComponent with InitialPosition { +class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex { ///{@macro dino_top_wall} _DinoBottomWall() : super( - priority: RenderPriority.dinoBottomWall, children: [_DinoBottomWallSpriteComponent()], + renderBody: false, ) { - renderBody = false; + zIndex = ZIndexes.dinoBottomWall; } List _createFixtureDefs() { - final fixturesDef = []; - const restitution = 1.0; - - final topStraightControlPoints = [ - Vector2(32.4, -8.8), - Vector2(25, -7.7), - ]; final topStraightShape = EdgeShape() ..set( - topStraightControlPoints.first, - topStraightControlPoints.last, + Vector2(32.4, -8.8), + Vector2(25, -7.7), ); - final topStraightFixtureDef = FixtureDef( - topStraightShape, - restitution: restitution, - ); - fixturesDef.add(topStraightFixtureDef); - final topLeftCurveControlPoints = [ - topStraightControlPoints.last, - Vector2(21.8, -7), - Vector2(29.5, 13.8), - ]; final topLeftCurveShape = BezierCurveShape( - controlPoints: topLeftCurveControlPoints, - ); - final topLeftCurveFixtureDef = FixtureDef( - topLeftCurveShape, - restitution: restitution, + controlPoints: [ + topStraightShape.vertex2, + Vector2(21.8, -7), + Vector2(29.8, 13.8), + ], ); - fixturesDef.add(topLeftCurveFixtureDef); - final bottomLeftStraightControlPoints = [ - topLeftCurveControlPoints.last, - Vector2(31.8, 44.1), - ]; final bottomLeftStraightShape = EdgeShape() ..set( - bottomLeftStraightControlPoints.first, - bottomLeftStraightControlPoints.last, + topLeftCurveShape.vertices.last, + Vector2(31.9, 44.1), ); - final bottomLeftStraightFixtureDef = FixtureDef( - bottomLeftStraightShape, - restitution: restitution, - ); - fixturesDef.add(bottomLeftStraightFixtureDef); - final bottomStraightControlPoints = [ - bottomLeftStraightControlPoints.last, - Vector2(37.8, 44.1), - ]; final bottomStraightShape = EdgeShape() ..set( - bottomStraightControlPoints.first, - bottomStraightControlPoints.last, + bottomLeftStraightShape.vertex2, + Vector2(37.8, 44.1), ); - final bottomStraightFixtureDef = FixtureDef( - bottomStraightShape, - restitution: restitution, - ); - fixturesDef.add(bottomStraightFixtureDef); - return fixturesDef; + return [ + FixtureDef(topStraightShape), + FixtureDef(topLeftCurveShape), + FixtureDef(bottomLeftStraightShape), + FixtureDef(bottomStraightShape), + ]; } @override @@ -212,11 +180,13 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.dino.dinoLandBottom.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.bottomWall.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(23.6, -9.5); + position = Vector2(23.8, -9.5); } } diff --git a/packages/pinball_components/lib/src/components/fire_effect.dart b/packages/pinball_components/lib/src/components/fire_effect.dart index 14639527..e793b3e6 100644 --- a/packages/pinball_components/lib/src/components/fire_effect.dart +++ b/packages/pinball_components/lib/src/components/fire_effect.dart @@ -26,10 +26,8 @@ class FireEffect extends ParticleSystemComponent { required this.burstPower, required this.direction, Vector2? position, - int? priority, }) : super( position: position, - priority: priority, ); /// A [double] value that will define how "strong" the burst of particles diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index bd826668..bb982e96 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -14,10 +14,9 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { Flipper({ required this.side, }) : super( + renderBody: false, children: [_FlipperSpriteComponent(side: side)], - ) { - renderBody = false; - } + ); /// The size of the [Flipper]. static final size = Vector2(13.5, 4.3); @@ -56,7 +55,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { ); final joint = _FlipperJoint(jointDef); world.createJoint(joint); - unawaited(mounted.whenComplete(joint.unlock)); } List _createFixtureDefs() { @@ -133,6 +131,15 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { return body; } + + @override + void onMount() { + super.onMount(); + + gameRef.ready().whenComplete( + () => body.joints.whereType<_FlipperJoint>().first.unlock(), + ); + } } class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { @@ -216,11 +223,8 @@ class _FlipperJoint extends RevoluteJoint { /// The joint is locked when initialized in order to force the [Flipper] /// at its resting position. void lock() { - const angle = _halfSweepingAngle; - setLimits( - angle * side.direction, - angle * side.direction, - ); + final angle = _halfSweepingAngle * side.direction; + setLimits(angle, angle); } /// Unlocks the [Flipper] from its resting position. diff --git a/packages/pinball_components/lib/src/components/google_letter.dart b/packages/pinball_components/lib/src/components/google_letter.dart deleted file mode 100644 index 1e6f19fc..00000000 --- a/packages/pinball_components/lib/src/components/google_letter.dart +++ /dev/null @@ -1,132 +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'; - -/// {@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) - : _sprite = _GoogleLetterSpriteGroupComponent( - _GoogleLetterSpriteGroupComponent.spritePaths[index], - ); - - final _GoogleLetterSpriteGroupComponent _sprite; - - /// Displays active sprite for this [GoogleLetter]. - void activate() => _sprite.activate(); - - /// Displays inactive sprite for this [GoogleLetter]. - void deactivate() => _sprite.deactivate(); - - @override - Future onLoad() async { - await super.onLoad(); - await add(_sprite); - } - - @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); - } -} - -/// Indicates the [GoogleLetter]'s current sprite state. -@visibleForTesting -enum GoogleLetterSpriteState { - /// A lit up letter. - active, - - /// A dimmed letter. - inactive, -} - -class _GoogleLetterSpriteGroupComponent - extends SpriteGroupComponent with HasGameRef { - _GoogleLetterSpriteGroupComponent( - Map statePaths, - ) : _statePaths = statePaths, - super(anchor: Anchor.center); - - static final spritePaths = >[ - { - GoogleLetterSpriteState.active: - Assets.images.googleWord.letter1.active.keyName, - GoogleLetterSpriteState.inactive: - Assets.images.googleWord.letter1.inactive.keyName, - }, - { - GoogleLetterSpriteState.active: - Assets.images.googleWord.letter2.active.keyName, - GoogleLetterSpriteState.inactive: - Assets.images.googleWord.letter2.inactive.keyName, - }, - { - GoogleLetterSpriteState.active: - Assets.images.googleWord.letter3.active.keyName, - GoogleLetterSpriteState.inactive: - Assets.images.googleWord.letter3.inactive.keyName, - }, - { - GoogleLetterSpriteState.active: - Assets.images.googleWord.letter4.active.keyName, - GoogleLetterSpriteState.inactive: - Assets.images.googleWord.letter4.inactive.keyName, - }, - { - GoogleLetterSpriteState.active: - Assets.images.googleWord.letter5.active.keyName, - GoogleLetterSpriteState.inactive: - Assets.images.googleWord.letter5.inactive.keyName, - }, - { - GoogleLetterSpriteState.active: - Assets.images.googleWord.letter6.active.keyName, - GoogleLetterSpriteState.inactive: - Assets.images.googleWord.letter6.inactive.keyName, - }, - ]; - - final Map _statePaths; - - void activate() { - current = GoogleLetterSpriteState.active; - } - - void deactivate() { - current = GoogleLetterSpriteState.inactive; - } - - @override - Future onLoad() async { - await super.onLoad(); - - paint = Paint()..isAntiAlias = false; - - final sprites = { - GoogleLetterSpriteState.active: Sprite( - gameRef.images.fromCache(_statePaths[GoogleLetterSpriteState.active]!), - ), - GoogleLetterSpriteState.inactive: Sprite( - gameRef.images - .fromCache(_statePaths[GoogleLetterSpriteState.inactive]!), - ), - }; - this.sprites = sprites; - - current = GoogleLetterSpriteState.inactive; - size = sprites[current]!.originalSize / 10; - } -} 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 new file mode 100644 index 00000000..df54c1f4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'google_letter_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_letter/behaviors/google_letter_ball_contact_behavior.dart new file mode 100644 index 00000000..c3f0423e --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/behaviors/google_letter_ball_contact_behavior.dart @@ -0,0 +1,14 @@ +// ignore_for_file: public_member_api_docs + +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 { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} 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 new file mode 100644 index 00000000..a352e98d --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'google_letter_state.dart'; + +class GoogleLetterCubit extends Cubit { + GoogleLetterCubit() : super(GoogleLetterState.inactive); + + void onBallContacted() { + emit(GoogleLetterState.active); + } + + void onReset() { + emit(GoogleLetterState.inactive); + } +} 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 new file mode 100644 index 00000000..e1339320 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/cubit/google_letter_state.dart @@ -0,0 +1,10 @@ +part of 'google_letter_cubit.dart'; + +/// Indicates the [GoogleLetterCubit]'s current state. +enum GoogleLetterState { + /// A lit up letter. + active, + + /// A dimmed letter. + inactive, +} 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 new file mode 100644 index 00000000..f92ee875 --- /dev/null +++ b/packages/pinball_components/lib/src/components/google_letter/google_letter.dart @@ -0,0 +1,154 @@ +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'; + +/// {@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( + activeAssetPath: _GoogleLetterSpriteGroupComponent + .spritePaths[index][GoogleLetterState.active]!, + inactiveAssetPath: _GoogleLetterSpriteGroupComponent + .spritePaths[index][GoogleLetterState.inactive]!, + current: bloc.state, + ), + GoogleLetterBallContactBehavior(), + ...?children, + ], + renderBody: false, + ); + + /// Creates a [GoogleLetter] without any children. + /// + /// This can be used for testing [GoogleLetter]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + GoogleLetter.test({ + required this.bloc, + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + 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 activeAssetPath, + required String inactiveAssetPath, + required GoogleLetterState current, + }) : _activeAssetPath = activeAssetPath, + _inactiveAssetPath = inactiveAssetPath, + super( + anchor: Anchor.center, + current: current, + ); + + final String _activeAssetPath; + final String _inactiveAssetPath; + + static final spritePaths = >[ + { + GoogleLetterState.active: Assets.images.googleWord.letter1.active.keyName, + GoogleLetterState.inactive: + Assets.images.googleWord.letter1.inactive.keyName, + }, + { + GoogleLetterState.active: Assets.images.googleWord.letter2.active.keyName, + GoogleLetterState.inactive: + Assets.images.googleWord.letter2.inactive.keyName, + }, + { + GoogleLetterState.active: Assets.images.googleWord.letter3.active.keyName, + GoogleLetterState.inactive: + Assets.images.googleWord.letter3.inactive.keyName, + }, + { + GoogleLetterState.active: Assets.images.googleWord.letter4.active.keyName, + GoogleLetterState.inactive: + Assets.images.googleWord.letter4.inactive.keyName, + }, + { + GoogleLetterState.active: Assets.images.googleWord.letter5.active.keyName, + GoogleLetterState.inactive: + Assets.images.googleWord.letter5.inactive.keyName, + }, + { + GoogleLetterState.active: Assets.images.googleWord.letter6.active.keyName, + GoogleLetterState.inactive: + Assets.images.googleWord.letter6.inactive.keyName, + }, + ]; + + void activate() { + current = GoogleLetterState.active; + } + + void deactivate() { + current = GoogleLetterState.inactive; + } + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state); + + final sprites = { + GoogleLetterState.active: Sprite( + gameRef.images.fromCache(_activeAssetPath), + ), + GoogleLetterState.inactive: Sprite( + gameRef.images.fromCache(_inactiveAssetPath), + ), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/kicker/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/kicker/behaviors/behaviors.dart new file mode 100644 index 00000000..e1098a34 --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'kicker_ball_contact_behavior.dart'; +export 'kicker_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_ball_contact_behavior.dart new file mode 100644 index 00000000..d5d2eb6c --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_ball_contact_behavior.dart @@ -0,0 +1,14 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class KickerBallContactBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} diff --git a/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_blinking_behavior.dart b/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_blinking_behavior.dart new file mode 100644 index 00000000..569d461f --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/behaviors/kicker_blinking_behavior.dart @@ -0,0 +1,37 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template kicker_blinking_behavior} +/// Makes a [Kicker] blink back to [KickerState.lit] when [KickerState.dimmed]. +/// {@endtemplate} +class KickerBlinkingBehavior extends TimerComponent with ParentIsA { + /// {@macro kicker_blinking_behavior} + KickerBlinkingBehavior() : super(period: 0.05); + + void _onNewState(KickerState state) { + switch (state) { + case KickerState.lit: + break; + case KickerState.dimmed: + timer + ..reset() + ..start(); + break; + } + } + + @override + Future onLoad() async { + await super.onLoad(); + timer.stop(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + timer.stop(); + parent.bloc.onBlinked(); + } +} diff --git a/packages/pinball_components/lib/src/components/kicker/cubit/kicker_cubit.dart b/packages/pinball_components/lib/src/components/kicker/cubit/kicker_cubit.dart new file mode 100644 index 00000000..488f4683 --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/cubit/kicker_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'kicker_state.dart'; + +class KickerCubit extends Cubit { + KickerCubit() : super(KickerState.lit); + + void onBallContacted() { + emit(KickerState.dimmed); + } + + void onBlinked() { + emit(KickerState.lit); + } +} diff --git a/packages/pinball_components/lib/src/components/kicker/cubit/kicker_state.dart b/packages/pinball_components/lib/src/components/kicker/cubit/kicker_state.dart new file mode 100644 index 00000000..08d52709 --- /dev/null +++ b/packages/pinball_components/lib/src/components/kicker/cubit/kicker_state.dart @@ -0,0 +1,8 @@ +// ignore_for_file: public_member_api_docs + +part of 'kicker_cubit.dart'; + +enum KickerState { + lit, + dimmed, +} diff --git a/packages/pinball_components/lib/src/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker/kicker.dart similarity index 53% rename from packages/pinball_components/lib/src/components/kicker.dart rename to packages/pinball_components/lib/src/components/kicker/kicker.dart index f6963d7c..3301e2ba 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker/kicker.dart @@ -2,9 +2,15 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; import 'package:geometry/geometry.dart' as geometry show centroid; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/src/components/bumping_behavior.dart'; +import 'package:pinball_components/src/components/kicker/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/kicker_cubit.dart'; /// {@template kicker} /// Triangular [BodyType.static] body that propels the [Ball] towards the @@ -16,40 +22,70 @@ class Kicker extends BodyComponent with InitialPosition { /// {@macro kicker} Kicker({ required BoardSide side, + Iterable? children, + }) : this._( + side: side, + bloc: KickerCubit(), + children: children, + ); + + Kicker._({ + required BoardSide side, + required this.bloc, + Iterable? children, }) : _side = side, super( - children: [_KickerSpriteComponent(side: side)], - ) { - renderBody = false; - } + children: [ + BumpingBehavior(strength: 15)..applyTo(['bouncy_edge']), + KickerBallContactBehavior()..applyTo(['bouncy_edge']), + KickerBlinkingBehavior(), + _KickerSpriteGroupComponent( + side: side, + state: bloc.state, + ), + ...?children, + ], + renderBody: false, + ); - /// The size of the [Kicker] body. - static final Vector2 size = Vector2(4.4, 15); + /// Creates a [Kicker] without any children. + /// + /// This can be used for testing [Kicker]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + Kicker.test({ + required this.bloc, + required BoardSide side, + }) : _side = side; + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final KickerCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } /// Whether the [Kicker] is on the left or right side of the board. - /// - /// A [Kicker] with [BoardSide.left] propels the [Ball] to the right, - /// whereas a [Kicker] with [BoardSide.right] propels the [Ball] to the - /// left. final BoardSide _side; List _createFixtureDefs() { - final fixturesDefs = []; final direction = _side.direction; const quarterPi = math.pi / 4; + final size = Vector2(4.4, 15); final upperCircle = CircleShape()..radius = 1.6; upperCircle.position.setValues(0, upperCircle.radius / 2); - final upperCircleFixtureDef = FixtureDef(upperCircle); - fixturesDefs.add(upperCircleFixtureDef); final lowerCircle = CircleShape()..radius = 1.6; lowerCircle.position.setValues( size.x * -direction, size.y + 0.8, ); - final lowerCircleFixtureDef = FixtureDef(lowerCircle); - fixturesDefs.add(lowerCircleFixtureDef); final wallFacingEdge = EdgeShape() ..set( @@ -60,8 +96,6 @@ class Kicker extends BodyComponent with InitialPosition { ), Vector2(2.5 * direction, size.y - 2), ); - final wallFacingLineFixtureDef = FixtureDef(wallFacingEdge); - fixturesDefs.add(wallFacingLineFixtureDef); final bottomEdge = EdgeShape() ..set( @@ -72,8 +106,6 @@ class Kicker extends BodyComponent with InitialPosition { lowerCircle.radius * math.sin(quarterPi), ), ); - final bottomLineFixtureDef = FixtureDef(bottomEdge); - fixturesDefs.add(bottomLineFixtureDef); final bouncyEdge = EdgeShape() ..set( @@ -89,12 +121,13 @@ class Kicker extends BodyComponent with InitialPosition { ), ); - final bouncyFixtureDef = FixtureDef( - bouncyEdge, - // TODO(alestiago): Play with restitution value once game is bundled. - restitution: 10, - ); - fixturesDefs.add(bouncyFixtureDef); + final fixturesDefs = [ + FixtureDef(upperCircle), + FixtureDef(lowerCircle), + FixtureDef(wallFacingEdge), + FixtureDef(bottomEdge), + FixtureDef(bouncyEdge, userData: 'bouncy_edge'), + ]; // TODO(alestiago): Evaluate if there is value on centering the fixtures. final centroid = geometry.centroid( @@ -127,25 +160,46 @@ class Kicker extends BodyComponent with InitialPosition { } } -class _KickerSpriteComponent extends SpriteComponent with HasGameRef { - _KickerSpriteComponent({required BoardSide side}) : _side = side; +class _KickerSpriteGroupComponent extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _KickerSpriteGroupComponent({ + required BoardSide side, + required KickerState state, + }) : _side = side, + super( + anchor: Anchor.center, + position: Vector2(0.7 * -side.direction, -2.2), + current: state, + ); final BoardSide _side; @override Future onLoad() async { await super.onLoad(); - - // TODO(alestiago): Used cached asset. - final sprite = await gameRef.loadSprite( - (_side.isLeft) - ? Assets.images.kicker.left.keyName - : Assets.images.kicker.right.keyName, - ); - this.sprite = sprite; - size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(0.7 * -_side.direction, -2.2); + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + parent.bloc.stream.listen((state) => current = state); + + final sprites = { + KickerState.lit: Sprite( + gameRef.images.fromCache( + (_side.isLeft) + ? Assets.images.kicker.left.lit.keyName + : Assets.images.kicker.right.lit.keyName, + ), + ), + KickerState.dimmed: Sprite( + gameRef.images.fromCache( + (_side.isLeft) + ? Assets.images.kicker.left.dimmed.keyName + : Assets.images.kicker.right.dimmed.keyName, + ), + ), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; } } diff --git a/packages/pinball_components/lib/src/components/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index baa54744..4713c3a2 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -8,14 +8,13 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template launch_ramp} -/// A [Blueprint] which creates the [_LaunchRampBase] and -/// [_LaunchRampForegroundRailing]. +/// Ramp where the ball is launched from. /// {@endtemplate} -class LaunchRamp extends Blueprint { +class LaunchRamp extends Component { /// {@macro launch_ramp} LaunchRamp() : super( - components: [ + children: [ _LaunchRampBase(), _LaunchRampForegroundRailing(), _LaunchRampExit()..initialPosition = Vector2(0.6, -34), @@ -24,21 +23,17 @@ class LaunchRamp extends Blueprint { ); } -/// {@template launch_ramp_base} -/// Ramp the [Ball] is launched from at the beginning of each ball life. -/// {@endtemplate} -class _LaunchRampBase extends BodyComponent with Layered { - /// {@macro launch_ramp_base} +class _LaunchRampBase extends BodyComponent with Layered, ZIndex { _LaunchRampBase() : super( - priority: RenderPriority.launchRamp, + renderBody: false, children: [ _LaunchRampBackgroundRailingSpriteComponent(), _LaunchRampBaseSpriteComponent(), ], ) { + zIndex = ZIndexes.launchRamp; layer = Layer.launcher; - renderBody = false; } // TODO(ruimiguel): final asset differs slightly from the current shape. We @@ -107,13 +102,6 @@ class _LaunchRampBase extends BodyComponent with Layered { return body; } - - @override - Future onLoad() async { - await super.onLoad(); - gameRef - .addContactCallback(LayerSensorBallContactCallback<_LaunchRampExit>()); - } } class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { @@ -147,18 +135,13 @@ class _LaunchRampBackgroundRailingSpriteComponent extends SpriteComponent } } -/// {@template launch_ramp_foreground_railing} -/// Foreground railing for the [_LaunchRampBase] to render in front of the -/// [Ball]. -/// {@endtemplate} -class _LaunchRampForegroundRailing extends BodyComponent { - /// {@macro launch_ramp_foreground_railing} +class _LaunchRampForegroundRailing extends BodyComponent with ZIndex { _LaunchRampForegroundRailing() : super( - priority: RenderPriority.launchRampForegroundRailing, children: [_LaunchRampForegroundRailingSpriteComponent()], + renderBody: false, ) { - renderBody = false; + zIndex = ZIndexes.launchRampForegroundRailing; } List _createFixtureDefs() { @@ -218,9 +201,8 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent } class _LaunchRampCloseWall extends BodyComponent with InitialPosition, Layered { - _LaunchRampCloseWall() { + _LaunchRampCloseWall() : super(renderBody: false) { layer = Layer.board; - renderBody = false; } @override @@ -248,11 +230,10 @@ class _LaunchRampExit extends LayerSensor { insideLayer: Layer.launcher, outsideLayer: Layer.board, orientation: LayerEntranceOrientation.down, - insidePriority: RenderPriority.ballOnLaunchRamp, - outsidePriority: RenderPriority.ballOnBoard, + insideZIndex: ZIndexes.ballOnLaunchRamp, + outsideZIndex: ZIndexes.ballOnBoard, ) { layer = Layer.launcher; - renderBody = false; } static final Vector2 _size = Vector2(1.6, 0.1); diff --git a/packages/pinball_components/lib/src/components/layer.dart b/packages/pinball_components/lib/src/components/layer.dart index 9b20ecf2..a39ad837 100644 --- a/packages/pinball_components/lib/src/components/layer.dart +++ b/packages/pinball_components/lib/src/components/layer.dart @@ -8,9 +8,6 @@ import 'package:flutter/material.dart'; /// [BodyComponent]s with compatible [Layer]s can collide with each other, /// ignoring others. This compatibility depends on bit masking operation /// between layers. For more information read: https://en.wikipedia.org/wiki/Mask_(computing). -/// -/// A parent [Layered] have priority against its children's layer. Them won't be -/// changed but will be ignored. /// {@endtemplate} mixin Layered on BodyComponent { Layer _layer = Layer.all; diff --git a/packages/pinball_components/lib/src/components/layer_sensor.dart b/packages/pinball_components/lib/src/components/layer_sensor.dart index 85cc8506..6b5f3832 100644 --- a/packages/pinball_components/lib/src/components/layer_sensor.dart +++ b/packages/pinball_components/lib/src/components/layer_sensor.dart @@ -17,42 +17,30 @@ enum LayerEntranceOrientation { /// {@template layer_sensor} /// [BodyComponent] located at the entrance and exit of a [Layer]. /// -/// [LayerSensorBallContactCallback] detects when a [Ball] passes -/// through this sensor. -/// /// By default the base [layer] is set to [Layer.board] and the -/// [outsidePriority] is set to the lowest possible [Layer]. +/// [_outsideZIndex] is set to [ZIndexes.ballOnBoard]. /// {@endtemplate} -abstract class LayerSensor extends BodyComponent with InitialPosition, Layered { +abstract class LayerSensor extends BodyComponent + with InitialPosition, Layered, ContactCallbacks { /// {@macro layer_sensor} LayerSensor({ required Layer insideLayer, Layer? outsideLayer, - required int insidePriority, - int? outsidePriority, + required int insideZIndex, + int? outsideZIndex, required this.orientation, }) : _insideLayer = insideLayer, _outsideLayer = outsideLayer ?? Layer.board, - _insidePriority = insidePriority, - _outsidePriority = outsidePriority ?? RenderPriority.ballOnBoard { + _insideZIndex = insideZIndex, + _outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard, + super(renderBody: false) { layer = Layer.opening; } + final Layer _insideLayer; final Layer _outsideLayer; - final int _insidePriority; - final int _outsidePriority; - - /// Mask bits value for collisions on [Layer]. - Layer get insideLayer => _insideLayer; - - /// Mask bits value for collisions outside of [Layer]. - Layer get outsideLayer => _outsideLayer; - - /// Render priority for the [Ball] on [Layer]. - int get insidePriority => _insidePriority; - - /// Render priority for the [Ball] outside of [Layer]. - int get outsidePriority => _outsidePriority; + final int _insideZIndex; + final int _outsideZIndex; /// The [Shape] of the [LayerSensor]. Shape get shape; @@ -75,36 +63,28 @@ abstract class LayerSensor extends BodyComponent with InitialPosition, Layered { return world.createBody(bodyDef)..createFixture(fixtureDef); } -} -/// {@template layer_sensor_ball_contact_callback} -/// Detects when a [Ball] enters or exits a [Layer] through a [LayerSensor]. -/// -/// Modifies [Ball]'s [Layer] and render priority depending on whether the -/// [Ball] is on or outside of a [Layer]. -/// {@endtemplate} -class LayerSensorBallContactCallback - extends ContactCallback { @override - void begin(Ball ball, LayerEntrance layerEntrance, Contact _) { - if (ball.layer != layerEntrance.insideLayer) { + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + + if (other.layer != _insideLayer) { final isBallEnteringOpening = - (layerEntrance.orientation == LayerEntranceOrientation.down && - ball.body.linearVelocity.y < 0) || - (layerEntrance.orientation == LayerEntranceOrientation.up && - ball.body.linearVelocity.y > 0); + (orientation == LayerEntranceOrientation.down && + other.body.linearVelocity.y < 0) || + (orientation == LayerEntranceOrientation.up && + other.body.linearVelocity.y > 0); if (isBallEnteringOpening) { - ball - ..layer = layerEntrance.insideLayer - ..priority = layerEntrance.insidePriority - ..reorderChildren(); + other + ..layer = _insideLayer + ..zIndex = _insideZIndex; } } else { - ball - ..layer = layerEntrance.outsideLayer - ..priority = layerEntrance.outsidePriority - ..reorderChildren(); + other + ..layer = _outsideLayer + ..zIndex = _outsideZIndex; } } } diff --git a/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_cubit.dart b/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_cubit.dart new file mode 100644 index 00000000..1d265b2e --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_cubit.dart @@ -0,0 +1,25 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball_components/pinball_components.dart'; + +part 'multiplier_state.dart'; + +class MultiplierCubit extends Cubit { + MultiplierCubit(MultiplierValue multiplierValue) + : super(MultiplierState.initial(multiplierValue)); + + /// Event added when the game's current multiplier changes. + void next(int multiplier) { + if (state.value.equals(multiplier)) { + if (state.spriteState == MultiplierSpriteState.dimmed) { + emit(state.copyWith(spriteState: MultiplierSpriteState.lit)); + } + } else { + if (state.spriteState == MultiplierSpriteState.lit) { + emit(state.copyWith(spriteState: MultiplierSpriteState.dimmed)); + } + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_state.dart b/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_state.dart new file mode 100644 index 00000000..e3adde70 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiplier/cubit/multiplier_state.dart @@ -0,0 +1,56 @@ +// ignore_for_file: public_member_api_docs + +part of 'multiplier_cubit.dart'; + +enum MultiplierSpriteState { + lit, + dimmed, +} + +class MultiplierState extends Equatable { + const MultiplierState({ + required this.value, + required this.spriteState, + }); + + const MultiplierState.initial(MultiplierValue multiplierValue) + : this( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ); + + /// Current value for the [Multiplier] + final MultiplierValue value; + + /// The [MultiplierSpriteGroupComponent] current sprite state + final MultiplierSpriteState spriteState; + + MultiplierState copyWith({ + MultiplierSpriteState? spriteState, + }) { + return MultiplierState( + value: value, + spriteState: spriteState ?? this.spriteState, + ); + } + + @override + List get props => [value, spriteState]; +} + +extension MultiplierValueX on MultiplierValue { + bool equals(int value) { + switch (this) { + case MultiplierValue.x2: + return value == 2; + case MultiplierValue.x3: + return value == 3; + case MultiplierValue.x4: + return value == 4; + case MultiplierValue.x5: + return value == 5; + case MultiplierValue.x6: + return value == 6; + } + } +} diff --git a/packages/pinball_components/lib/src/components/multiplier/multiplier.dart b/packages/pinball_components/lib/src/components/multiplier/multiplier.dart new file mode 100644 index 00000000..54d02857 --- /dev/null +++ b/packages/pinball_components/lib/src/components/multiplier/multiplier.dart @@ -0,0 +1,204 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_components/src/components/multiplier/cubit/multiplier_cubit.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/multiplier_cubit.dart'; + +/// {@template multiplier} +/// Backlit multiplier decal displayed on the board. +/// {@endtemplate} +class Multiplier extends Component { + /// {@macro multiplier} + Multiplier._({ + required MultiplierValue value, + required Vector2 position, + required double angle, + required this.bloc, + }) : _value = value, + _position = position, + _angle = angle, + super(); + + /// {@macro multiplier} + Multiplier.x2({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x2, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x2), + ); + + /// {@macro multiplier} + Multiplier.x3({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x3, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x3), + ); + + /// {@macro multiplier} + Multiplier.x4({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x4, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x4), + ); + + /// {@macro multiplier} + Multiplier.x5({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x5, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x5), + ); + + /// {@macro multiplier} + Multiplier.x6({ + required Vector2 position, + required double angle, + }) : this._( + value: MultiplierValue.x6, + position: position, + angle: angle, + bloc: MultiplierCubit(MultiplierValue.x6), + ); + + /// Creates a [Multiplier] without any children. + /// + /// This can be used for testing [Multiplier]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + Multiplier.test({ + required MultiplierValue value, + required this.bloc, + }) : _value = value, + _position = Vector2.zero(), + _angle = 0; + +// TODO(ruimiguel): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + final MultiplierCubit bloc; + + final MultiplierValue _value; + final Vector2 _position; + final double _angle; + late final MultiplierSpriteGroupComponent _sprite; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Future onLoad() async { + await super.onLoad(); + _sprite = MultiplierSpriteGroupComponent( + position: _position, + litAssetPath: _value.litAssetPath, + dimmedAssetPath: _value.dimmedAssetPath, + angle: _angle, + current: bloc.state, + ); + await add(_sprite); + } +} + +/// Available multiplier values. +enum MultiplierValue { + x2, + x3, + x4, + x5, + x6, +} + +extension on MultiplierValue { + String get litAssetPath { + switch (this) { + case MultiplierValue.x2: + return Assets.images.multiplier.x2.lit.keyName; + case MultiplierValue.x3: + return Assets.images.multiplier.x3.lit.keyName; + case MultiplierValue.x4: + return Assets.images.multiplier.x4.lit.keyName; + case MultiplierValue.x5: + return Assets.images.multiplier.x5.lit.keyName; + case MultiplierValue.x6: + return Assets.images.multiplier.x6.lit.keyName; + } + } + + String get dimmedAssetPath { + switch (this) { + case MultiplierValue.x2: + return Assets.images.multiplier.x2.dimmed.keyName; + case MultiplierValue.x3: + return Assets.images.multiplier.x3.dimmed.keyName; + case MultiplierValue.x4: + return Assets.images.multiplier.x4.dimmed.keyName; + case MultiplierValue.x5: + return Assets.images.multiplier.x5.dimmed.keyName; + case MultiplierValue.x6: + return Assets.images.multiplier.x6.dimmed.keyName; + } + } +} + +/// {@template multiplier_sprite_group_component} +/// A [SpriteGroupComponent] for a [Multiplier] with lit and dimmed states. +/// {@endtemplate} +@visibleForTesting +class MultiplierSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + /// {@macro multiplier_sprite_group_component} + MultiplierSpriteGroupComponent({ + required Vector2 position, + required String litAssetPath, + required String dimmedAssetPath, + required double angle, + required MultiplierState current, + }) : _litAssetPath = litAssetPath, + _dimmedAssetPath = dimmedAssetPath, + super( + anchor: Anchor.center, + position: position, + angle: angle, + current: current.spriteState, + ); + + final String _litAssetPath; + final String _dimmedAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + parent.bloc.stream.listen((state) => current = state.spriteState); + + final sprites = { + MultiplierSpriteState.lit: + Sprite(gameRef.images.fromCache(_litAssetPath)), + MultiplierSpriteState.dimmed: + Sprite(gameRef.images.fromCache(_dimmedAssetPath)), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/plunger.dart b/packages/pinball_components/lib/src/components/plunger.dart index 295c799d..143e1049 100644 --- a/packages/pinball_components/lib/src/components/plunger.dart +++ b/packages/pinball_components/lib/src/components/plunger.dart @@ -1,6 +1,7 @@ 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'; /// {@template plunger} /// [Plunger] serves as a spring, that shoots the ball on the right side of the @@ -8,15 +9,13 @@ import 'package:pinball_components/pinball_components.dart'; /// /// [Plunger] ignores gravity so the player controls its downward [pull]. /// {@endtemplate} -class Plunger extends BodyComponent with InitialPosition, Layered { +class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex { /// {@macro plunger} Plunger({ required this.compressionDistance, - // TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities - // are fixed. - }) : super(priority: RenderPriority.plunger) { + }) : super(renderBody: false) { + zIndex = ZIndexes.plunger; layer = Layer.launcher; - renderBody = false; } /// Distance the plunger can lower. @@ -80,7 +79,7 @@ class Plunger extends BodyComponent with InitialPosition, Layered { /// The velocity's magnitude depends on how far the [Plunger] has been pulled /// from its original [initialPosition]. void release() { - final velocity = (initialPosition.y - body.position.y) * 5; + final velocity = (initialPosition.y - body.position.y) * 7; body.linearVelocity = Vector2(0, velocity); _spriteComponent.release(); } @@ -219,7 +218,7 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef { plunger.body, anchor.body, plunger.body.position + anchor.body.position, - Vector2(18.6, BoardDimensions.bounds.height), + Vector2(16, BoardDimensions.bounds.height), ); enableLimit = true; lowerTranslation = double.negativeInfinity; diff --git a/packages/pinball_components/lib/src/components/render_priority.dart b/packages/pinball_components/lib/src/components/render_priority.dart deleted file mode 100644 index d2438db9..00000000 --- a/packages/pinball_components/lib/src/components/render_priority.dart +++ /dev/null @@ -1,121 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:pinball_components/pinball_components.dart'; - -/// {@template render_priority} -/// Priorities for the component rendering order in the pinball game. -/// {@endtemplate} -// TODO(allisonryan0002): find alternative to section comments. -abstract class RenderPriority { - static const _base = 0; - static const _above = 1; - static const _below = -1; - - // Ball - - /// Render priority for the [Ball] while it's on the board. - static const int ballOnBoard = _base; - - /// Render priority for the [Ball] while it's on the [SpaceshipRamp]. - static const int ballOnSpaceshipRamp = - _above + spaceshipRampBackgroundRailing; - - /// Render priority for the [Ball] while it's on the [Spaceship]. - static const int ballOnSpaceship = _above + spaceshipSaucer; - - /// Render priority for the [Ball] while it's on the [SpaceshipRail]. - static const int ballOnSpaceshipRail = _below + spaceshipSaucer; - - /// Render priority for the [Ball] while it's on the [LaunchRamp]. - static const int ballOnLaunchRamp = _above + launchRamp; - - // Background - - // TODO(allisonryan0002): fix this magic priority. Could bump all priorities - // so there are no negatives. - static const int background = 3 * _below + _base; - - // Boundaries - - static const int bottomBoundary = _above + dinoBottomWall; - - static const int outerBoundary = _above + background; - - static const int outerBottomBoundary = _above + rocket; - - // Bottom Group - - static const int bottomGroup = _above + ballOnBoard; - - // Launcher - - static const int launchRamp = _above + outerBoundary; - - static const int launchRampForegroundRailing = _below + ballOnBoard; - - static const int plunger = _above + launchRamp; - - static const int rocket = _above + bottomBoundary; - - // Dino Land - - static const int dinoTopWall = _above + ballOnBoard; - - static const int dino = _above + dinoTopWall; - - static const int dinoBottomWall = _above + dino; - - static const int slingshot = _above + dinoBottomWall; - - // Flutter Forest - - static const int signpost = _above + launchRampForegroundRailing; - - static const int dashBumper = _above + ballOnBoard; - - static const int dashAnimatronic = 2 * _above + launchRamp; - - // Sparky Fire Zone - - static const int computerBase = _below + ballOnBoard; - - static const int computerTop = _above + ballOnBoard; - - static const int sparkyAnimatronic = _above + spaceshipRampForegroundRailing; - - static const int sparkyBumper = _above + ballOnBoard; - - static const int turboChargeFlame = _above + ballOnBoard; - - // Android Spaceship - - static const int spaceshipRail = _above + bottomGroup; - - static const int spaceshipRailForeground = _above + spaceshipRail; - - static const int spaceshipSaucer = _above + spaceshipRail; - - static const int spaceshipSaucerWall = _above + spaceshipSaucer; - - static const int androidHead = _above + spaceshipSaucer; - - static const int spaceshipRamp = _above + ballOnBoard; - - static const int spaceshipRampBackgroundRailing = _above + spaceshipRamp; - - static const int spaceshipRampArrow = _above + spaceshipRamp; - - static const int spaceshipRampForegroundRailing = - _above + ballOnSpaceshipRamp; - - static const int spaceshipRampBoardOpening = _below + ballOnBoard; - - static const int alienBumper = _above + ballOnBoard; - - // Score Text - - static const int scoreText = _above + spaceshipRampForegroundRailing; - - // Debug information - static const int debugInfo = _above + scoreText; -} diff --git a/packages/pinball_components/lib/src/components/rocket.dart b/packages/pinball_components/lib/src/components/rocket.dart index 3f9161ca..07133fc5 100644 --- a/packages/pinball_components/lib/src/components/rocket.dart +++ b/packages/pinball_components/lib/src/components/rocket.dart @@ -1,24 +1,26 @@ import 'package:flame/components.dart'; import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template rocket_sprite_component} /// A [SpriteComponent] for the rocket over [Plunger]. /// {@endtemplate} -class RocketSpriteComponent extends SpriteComponent with HasGameRef { - // TODO(ruimiguel): change this priority to be over launcher ramp and bottom - // wall. +class RocketSpriteComponent extends SpriteComponent with HasGameRef, ZIndex { /// {@macro rocket_sprite_component} - RocketSpriteComponent() : super(priority: RenderPriority.rocket); + RocketSpriteComponent() : super(anchor: Anchor.center) { + zIndex = ZIndexes.rocket; + } @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.plunger.rocket.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.plunger.rocket.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; } } diff --git a/packages/pinball_components/lib/src/components/score_text.dart b/packages/pinball_components/lib/src/components/score_text.dart index a81b4a6f..6dcba4b1 100644 --- a/packages/pinball_components/lib/src/components/score_text.dart +++ b/packages/pinball_components/lib/src/components/score_text.dart @@ -4,11 +4,12 @@ import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template score_text} /// A [TextComponent] that spawns at a given [position] with a moving animation. /// {@endtemplate} -class ScoreText extends TextComponent { +class ScoreText extends TextComponent with ZIndex { /// {@macro score_text} ScoreText({ required String text, @@ -18,8 +19,9 @@ class ScoreText extends TextComponent { text: text, position: position, anchor: Anchor.center, - priority: RenderPriority.scoreText, - ); + ) { + zIndex = ZIndexes.scoreText; + } late final Effect _effect; diff --git a/packages/pinball_components/lib/src/components/signpost.dart b/packages/pinball_components/lib/src/components/signpost.dart index 175c3382..13425342 100644 --- a/packages/pinball_components/lib/src/components/signpost.dart +++ b/packages/pinball_components/lib/src/components/signpost.dart @@ -46,13 +46,15 @@ extension on SignpostSpriteState { /// {@endtemplate} class Signpost extends BodyComponent with InitialPosition { /// {@macro signpost} - Signpost() - : super( - priority: RenderPriority.signpost, - children: [_SignpostSpriteComponent()], - ) { - renderBody = false; - } + Signpost({ + Iterable? children, + }) : super( + renderBody: false, + children: [ + _SignpostSpriteComponent(), + ...?children, + ], + ); /// Forwards the sprite to the next [SignpostSpriteState]. /// diff --git a/packages/pinball_components/lib/src/components/slingshot.dart b/packages/pinball_components/lib/src/components/slingshot.dart index 35346584..8acf0f9a 100644 --- a/packages/pinball_components/lib/src/components/slingshot.dart +++ b/packages/pinball_components/lib/src/components/slingshot.dart @@ -4,14 +4,13 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template slingshots} -/// A [Blueprint] which creates the pair of [Slingshot]s on the right side of -/// the board. +/// A collection of [Slingshot]s. /// {@endtemplate} -class Slingshots extends Blueprint { +class Slingshots extends Component with ZIndex { /// {@macro slingshots} Slingshots() : super( - components: [ + children: [ Slingshot( length: 5.64, angle: -0.017, @@ -23,7 +22,9 @@ class Slingshots extends Blueprint { spritePath: Assets.images.slingshot.lower.keyName, )..initialPosition = Vector2(24.7, 6.2), ], - ); + ) { + zIndex = ZIndexes.slingshots; + } } /// {@template slingshot} @@ -38,11 +39,9 @@ class Slingshot extends BodyComponent with InitialPosition { }) : _length = length, _angle = angle, super( - priority: RenderPriority.slingshot, children: [_SlinghsotSpriteComponent(spritePath, angle: angle)], - ) { - renderBody = false; - } + renderBody: false, + ); final double _length; diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart deleted file mode 100644 index 4ea4e05a..00000000 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/gen/assets.gen.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template spaceship} -/// A [Blueprint] which creates the spaceship feature. -/// {@endtemplate} -class Spaceship extends Blueprint { - /// {@macro spaceship} - Spaceship({required Vector2 position}) - : super( - components: [ - SpaceshipSaucer()..initialPosition = position, - _SpaceshipEntrance()..initialPosition = position, - AndroidHead()..initialPosition = position, - _SpaceshipHole( - outsideLayer: Layer.spaceshipExitRail, - outsidePriority: RenderPriority.ballOnSpaceshipRail, - )..initialPosition = position - Vector2(5.2, -4.8), - _SpaceshipHole( - outsideLayer: Layer.board, - outsidePriority: RenderPriority.ballOnBoard, - )..initialPosition = position - Vector2(-7.2, -0.8), - SpaceshipWall()..initialPosition = position, - ], - ); - - /// Total size of the spaceship. - static final size = Vector2(25, 19); -} - -/// {@template spaceship_saucer} -/// A [BodyComponent] for the base, or the saucer of the spaceship -/// {@endtemplate} -class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { - /// {@macro spaceship_saucer} - SpaceshipSaucer() - : super( - priority: RenderPriority.spaceshipSaucer, - children: [ - _SpaceshipSaucerSpriteComponent(), - ], - ) { - layer = Layer.spaceship; - renderBody = false; - } - - @override - Future onLoad() async { - await super.onLoad(); - - gameRef - ..addContactCallback( - LayerSensorBallContactCallback<_SpaceshipEntrance>(), - ) - ..addContactCallback( - LayerSensorBallContactCallback<_SpaceshipHole>(), - ); - } - - @override - Body createBody() { - final shape = CircleShape()..radius = 3; - final fixtureDef = FixtureDef( - shape, - isSensor: true, - ); - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} - -class _SpaceshipSaucerSpriteComponent extends SpriteComponent with HasGameRef { - _SpaceshipSaucerSpriteComponent() - : super( - anchor: Anchor.center, - // TODO(alestiago): Refactor to use sprite orignial size instead. - size: Spaceship.size, - ); - - @override - Future onLoad() async { - await super.onLoad(); - - // TODO(alestiago): Use cached sprite. - sprite = await gameRef.loadSprite( - Assets.images.spaceship.saucer.keyName, - ); - } -} - -/// {@template spaceship_bridge} -/// A [BodyComponent] that provides both the collision and the rotation -/// animation for the bridge. -/// {@endtemplate} -class AndroidHead extends BodyComponent with InitialPosition, Layered { - /// {@macro spaceship_bridge} - AndroidHead() - : super( - priority: RenderPriority.androidHead, - children: [_AndroidHeadSpriteAnimation()], - ) { - renderBody = false; - layer = Layer.spaceship; - } - - @override - Body createBody() { - final circleShape = CircleShape()..radius = 2; - - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - - return world.createBody(bodyDef) - ..createFixture( - FixtureDef(circleShape)..restitution = 0.4, - ); - } -} - -class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent - with HasGameRef { - @override - Future onLoad() async { - await super.onLoad(); - - final image = await gameRef.images.load( - Assets.images.spaceship.bridge.keyName, - ); - size = Vector2(8.2, 10); - position = Vector2(0, -2); - anchor = Anchor.center; - - final data = SpriteAnimationData.sequenced( - amount: 72, - amountPerRow: 24, - stepTime: 0.05, - textureSize: size * 10, - ); - animation = SpriteAnimation.fromFrameData(image, data); - } -} - -class _SpaceshipEntrance extends LayerSensor { - _SpaceshipEntrance() - : super( - insideLayer: Layer.spaceship, - orientation: LayerEntranceOrientation.up, - insidePriority: RenderPriority.ballOnSpaceship, - ) { - layer = Layer.spaceship; - } - - @override - Shape get shape { - renderBody = false; - final radius = Spaceship.size.y / 2; - return PolygonShape() - ..setAsEdge( - Vector2( - radius * cos(20 * pi / 180), - radius * sin(20 * pi / 180), - )..rotate(90 * pi / 180), - Vector2( - radius * cos(340 * pi / 180), - radius * sin(340 * pi / 180), - )..rotate(90 * pi / 180), - ); - } -} - -class _SpaceshipHole extends LayerSensor { - _SpaceshipHole({required Layer outsideLayer, required int outsidePriority}) - : super( - insideLayer: Layer.spaceship, - outsideLayer: outsideLayer, - orientation: LayerEntranceOrientation.down, - insidePriority: RenderPriority.ballOnSpaceship, - outsidePriority: outsidePriority, - ) { - renderBody = false; - layer = Layer.spaceship; - } - - @override - Shape get shape { - return ArcShape( - center: Vector2(0, -3.2), - arcRadius: 5, - angle: 1, - rotation: -2, - ); - } -} - -/// {@template spaceship_wall_shape} -/// The [ChainShape] that defines the shape of the [SpaceshipWall]. -/// {@endtemplate} -class _SpaceshipWallShape extends ChainShape { - /// {@macro spaceship_wall_shape} - _SpaceshipWallShape() { - final minorRadius = (Spaceship.size.y - 2) / 2; - final majorRadius = (Spaceship.size.x - 2) / 2; - - createChain( - [ - // TODO(alestiago): Try converting this logic to radian. - for (var angle = 20; angle <= 340; angle++) - Vector2( - minorRadius * cos(angle * pi / 180), - majorRadius * sin(angle * pi / 180), - ), - ], - ); - } -} - -/// {@template spaceship_wall} -/// A [BodyComponent] that provides the collision for the wall -/// surrounding the spaceship. -/// -/// It has a small opening to allow the [Ball] to get inside the spaceship -/// saucer. -/// -/// It also contains the [SpriteComponent] for the lower wall -/// {@endtemplate} -class SpaceshipWall extends BodyComponent with InitialPosition, Layered { - /// {@macro spaceship_wall} - SpaceshipWall() : super(priority: RenderPriority.spaceshipSaucerWall) { - layer = Layer.spaceship; - } - - @override - Body createBody() { - renderBody = false; - - final shape = _SpaceshipWallShape(); - final fixtureDef = FixtureDef(shape); - - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - angle: -1.7, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} diff --git a/packages/pinball_components/lib/src/components/spaceship_rail.dart b/packages/pinball_components/lib/src/components/spaceship_rail.dart index 1175384b..7dbabc93 100644 --- a/packages/pinball_components/lib/src/components/spaceship_rail.dart +++ b/packages/pinball_components/lib/src/components/spaceship_rail.dart @@ -2,88 +2,71 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball_components/gen/assets.gen.dart'; -import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship_rail} -/// A [Blueprint] for the spaceship drop tube. +/// Rail exiting the [AndroidSpaceship]. /// {@endtemplate} -class SpaceshipRail extends Blueprint { +class SpaceshipRail extends Component { /// {@macro spaceship_rail} SpaceshipRail() : super( - components: [ - _SpaceshipRailRamp(), + children: [ + _SpaceshipRail(), _SpaceshipRailExit(), - _SpaceshipRailBase(radius: 0.55) - ..initialPosition = Vector2(-26.15, -18.65), - _SpaceshipRailBase(radius: 0.8) - ..initialPosition = Vector2(-25.5, 12.9), - _SpaceshipRailForeground() + _SpaceshipRailExitSpriteComponent() ], ); } -class _SpaceshipRailRamp extends BodyComponent with Layered { - _SpaceshipRailRamp() +class _SpaceshipRail extends BodyComponent with Layered, ZIndex { + _SpaceshipRail() : super( - priority: RenderPriority.spaceshipRail, - children: [_SpaceshipRailRampSpriteComponent()], + children: [_SpaceshipRailSpriteComponent()], + renderBody: false, ) { layer = Layer.spaceshipExitRail; - renderBody = false; + zIndex = ZIndexes.spaceshipRail; } List _createFixtureDefs() { - final fixturesDefs = []; - final topArcShape = ArcShape( - center: Vector2(-35.5, -30.9), + center: Vector2(-35.1, -30.9), arcRadius: 2.5, angle: math.pi, rotation: 0.2, ); - final topArcFixtureDef = FixtureDef(topArcShape); - fixturesDefs.add(topArcFixtureDef); final topLeftCurveShape = BezierCurveShape( controlPoints: [ - Vector2(-37.9, -30.4), - Vector2(-38, -23.9), + Vector2(-37.6, -30.4), + Vector2(-37.8, -23.9), Vector2(-30.93, -18.2), ], ); - final topLeftCurveFixtureDef = FixtureDef(topLeftCurveShape); - fixturesDefs.add(topLeftCurveFixtureDef); final middleLeftCurveShape = BezierCurveShape( controlPoints: [ topLeftCurveShape.vertices.last, Vector2(-22.6, -10.3), - Vector2(-30, -0.2), + Vector2(-29.5, -0.2), ], ); - final middleLeftCurveFixtureDef = FixtureDef(middleLeftCurveShape); - fixturesDefs.add(middleLeftCurveFixtureDef); final bottomLeftCurveShape = BezierCurveShape( controlPoints: [ middleLeftCurveShape.vertices.last, - Vector2(-36, 8.6), - Vector2(-32.04, 18.3), + Vector2(-35.6, 8.6), + Vector2(-31.3, 18.3), ], ); - final bottomLeftCurveFixtureDef = FixtureDef(bottomLeftCurveShape); - fixturesDefs.add(bottomLeftCurveFixtureDef); final topRightStraightShape = EdgeShape() ..set( - Vector2(-33, -31.3), Vector2(-27.2, -21.3), + Vector2(-33, -31.3), ); - final topRightStraightFixtureDef = FixtureDef(topRightStraightShape); - fixturesDefs.add(topRightStraightFixtureDef); final middleRightCurveShape = BezierCurveShape( controlPoints: [ @@ -92,8 +75,6 @@ class _SpaceshipRailRamp extends BodyComponent with Layered { Vector2(-25.29, 1.7), ], ); - final middleRightCurveFixtureDef = FixtureDef(middleRightCurveShape); - fixturesDefs.add(middleRightCurveFixtureDef); final bottomRightCurveShape = BezierCurveShape( controlPoints: [ @@ -102,10 +83,16 @@ class _SpaceshipRailRamp extends BodyComponent with Layered { Vector2(-26.8, 15.7), ], ); - final bottomRightCurveFixtureDef = FixtureDef(bottomRightCurveShape); - fixturesDefs.add(bottomRightCurveFixtureDef); - return fixturesDefs; + return [ + FixtureDef(topArcShape), + FixtureDef(topLeftCurveShape), + FixtureDef(middleLeftCurveShape), + FixtureDef(bottomLeftCurveShape), + FixtureDef(topRightStraightShape), + FixtureDef(middleRightCurveShape), + FixtureDef(bottomRightCurveShape), + ]; } @override @@ -114,67 +101,50 @@ class _SpaceshipRailRamp extends BodyComponent with Layered { _createFixtureDefs().forEach(body.createFixture); return body; } - - @override - Future onLoad() async { - await super.onLoad(); - gameRef.addContactCallback( - LayerSensorBallContactCallback<_SpaceshipRailExit>(), - ); - } } -class _SpaceshipRailRampSpriteComponent extends SpriteComponent - with HasGameRef { +class _SpaceshipRailSpriteComponent extends SpriteComponent with HasGameRef { + _SpaceshipRailSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-29.4, -5.7), + ); + @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.spaceship.rail.main.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.android.rail.main.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(-29.4, -5.7); } } -class _SpaceshipRailForeground extends SpriteComponent with HasGameRef { - _SpaceshipRailForeground() - : super(priority: RenderPriority.spaceshipRailForeground); +class _SpaceshipRailExitSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _SpaceshipRailExitSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(-28, 19.4), + ) { + zIndex = ZIndexes.spaceshipRailExit; + } @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.spaceship.rail.foreground.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.android.rail.exit.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - anchor = Anchor.center; - position = Vector2(-28.5, 19.7); - } -} - -/// Represents the ground bases of the [_SpaceshipRailRamp]. -class _SpaceshipRailBase extends BodyComponent with InitialPosition { - _SpaceshipRailBase({required this.radius}) { - renderBody = false; - } - - final double radius; - - @override - Body createBody() { - final shape = CircleShape()..radius = radius; - final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef( - position: initialPosition, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); } } @@ -183,9 +153,8 @@ class _SpaceshipRailExit extends LayerSensor { : super( orientation: LayerEntranceOrientation.down, insideLayer: Layer.spaceshipExitRail, - insidePriority: RenderPriority.ballOnSpaceshipRail, + insideZIndex: ZIndexes.ballOnSpaceshipRail, ) { - renderBody = false; layer = Layer.spaceshipExitRail; } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp.dart index 30211251..c1be0943 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp.dart @@ -8,22 +8,22 @@ import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_flame/pinball_flame.dart'; /// {@template spaceship_ramp} -/// A [Blueprint] which creates the ramp leading into the [Spaceship]. +/// Ramp leading into the [AndroidSpaceship]. /// {@endtemplate} -class SpaceshipRamp extends Blueprint { +class SpaceshipRamp extends Component { /// {@macro spaceship_ramp} SpaceshipRamp() : super( - components: [ + children: [ _SpaceshipRampOpening( - outsidePriority: RenderPriority.ballOnBoard, + outsidePriority: ZIndexes.ballOnBoard, rotation: math.pi, ) ..initialPosition = Vector2(1.7, -19.8) ..layer = Layer.opening, _SpaceshipRampOpening( outsideLayer: Layer.spaceship, - outsidePriority: RenderPriority.ballOnSpaceship, + outsidePriority: ZIndexes.ballOnSpaceship, rotation: math.pi, ) ..initialPosition = Vector2(-13.7, -18.6) @@ -41,10 +41,8 @@ class SpaceshipRamp extends Blueprint { /// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. /// /// If the current state is the last one it cycles back to the initial state. - void progress() => components - .whereType<_SpaceshipRampArrowSpriteComponent>() - .first - .progress(); + void progress() => + firstChild<_SpaceshipRampArrowSpriteComponent>()?.progress(); } /// Indicates the state of the arrow on the [SpaceshipRamp]. @@ -73,17 +71,17 @@ extension on SpaceshipRampArrowSpriteState { String get path { switch (this) { case SpaceshipRampArrowSpriteState.inactive: - return Assets.images.spaceship.ramp.arrow.inactive.keyName; + return Assets.images.android.ramp.arrow.inactive.keyName; case SpaceshipRampArrowSpriteState.active1: - return Assets.images.spaceship.ramp.arrow.active1.keyName; + return Assets.images.android.ramp.arrow.active1.keyName; case SpaceshipRampArrowSpriteState.active2: - return Assets.images.spaceship.ramp.arrow.active2.keyName; + return Assets.images.android.ramp.arrow.active2.keyName; case SpaceshipRampArrowSpriteState.active3: - return Assets.images.spaceship.ramp.arrow.active3.keyName; + return Assets.images.android.ramp.arrow.active3.keyName; case SpaceshipRampArrowSpriteState.active4: - return Assets.images.spaceship.ramp.arrow.active4.keyName; + return Assets.images.android.ramp.arrow.active4.keyName; case SpaceshipRampArrowSpriteState.active5: - return Assets.images.spaceship.ramp.arrow.active5.keyName; + return Assets.images.android.ramp.arrow.active5.keyName; } } @@ -94,16 +92,16 @@ extension on SpaceshipRampArrowSpriteState { } class _SpaceshipRampBackground extends BodyComponent - with InitialPosition, Layered { + with InitialPosition, Layered, ZIndex { _SpaceshipRampBackground() : super( - priority: RenderPriority.spaceshipRamp, + renderBody: false, children: [ _SpaceshipRampBackgroundRampSpriteComponent(), ], ) { layer = Layer.spaceshipEntranceRamp; - renderBody = false; + zIndex = ZIndexes.spaceshipRamp; } /// Width between walls of the ramp. @@ -145,31 +143,24 @@ class _SpaceshipRampBackground extends BodyComponent return body; } - - @override - Future onLoad() async { - await super.onLoad(); - gameRef.addContactCallback( - LayerSensorBallContactCallback<_SpaceshipRampOpening>(), - ); - } } class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent - with HasGameRef { + with HasGameRef, ZIndex { _SpaceshipRampBackgroundRailingSpriteComponent() : super( anchor: Anchor.center, position: Vector2(-11.7, -54.3), - priority: RenderPriority.spaceshipRampBackgroundRailing, - ); + ) { + zIndex = ZIndexes.spaceshipRampBackgroundRailing; + } @override Future onLoad() async { await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.ramp.railingBackground.keyName, + Assets.images.android.ramp.railingBackground.keyName, ), ); this.sprite = sprite; @@ -190,7 +181,7 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.ramp.main.keyName, + Assets.images.android.ramp.main.keyName, ), ); this.sprite = sprite; @@ -205,14 +196,15 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent /// {@endtemplate} class _SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent - with HasGameRef { + with HasGameRef, ZIndex { /// {@macro spaceship_ramp_arrow_sprite_component} _SpaceshipRampArrowSpriteComponent() : super( anchor: Anchor.center, position: Vector2(-3.9, -56.5), - priority: RenderPriority.spaceshipRampArrow, - ); + ) { + zIndex = ZIndexes.spaceshipRampArrow; + } /// Changes arrow image to the next [Sprite]. void progress() => current = current?.next; @@ -234,15 +226,17 @@ class _SpaceshipRampArrowSpriteComponent } class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent - with HasGameRef { - _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center); + with HasGameRef, ZIndex { + _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) { + zIndex = ZIndexes.spaceshipRampBoardOpening; + } @override Future onLoad() async { await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.ramp.boardOpening.keyName, + Assets.images.android.ramp.boardOpening.keyName, ), ); this.sprite = sprite; @@ -251,14 +245,14 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent } class _SpaceshipRampForegroundRailing extends BodyComponent - with InitialPosition, Layered { + with InitialPosition, Layered, ZIndex { _SpaceshipRampForegroundRailing() : super( - priority: RenderPriority.spaceshipRampForegroundRailing, + renderBody: false, children: [_SpaceshipRampForegroundRailingSpriteComponent()], ) { layer = Layer.spaceshipEntranceRamp; - renderBody = false; + zIndex = ZIndexes.spaceshipRampForegroundRailing; } List _createFixtureDefs() { @@ -312,7 +306,7 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent await super.onLoad(); final sprite = Sprite( gameRef.images.fromCache( - Assets.images.spaceship.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingForeground.keyName, ), ); this.sprite = sprite; @@ -321,8 +315,7 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent } class _SpaceshipRampBase extends BodyComponent with InitialPosition, Layered { - _SpaceshipRampBase() { - renderBody = false; + _SpaceshipRampBase() : super(renderBody: false) { layer = Layer.board; } @@ -361,11 +354,9 @@ class _SpaceshipRampOpening extends LayerSensor { insideLayer: Layer.spaceshipEntranceRamp, outsideLayer: outsideLayer, orientation: LayerEntranceOrientation.down, - insidePriority: RenderPriority.ballOnSpaceshipRamp, - outsidePriority: outsidePriority, - ) { - renderBody = false; - } + insideZIndex: ZIndexes.ballOnSpaceshipRamp, + outsideZIndex: outsidePriority, + ); final double _rotation; diff --git a/packages/pinball_components/lib/src/components/sparky_animatronic.dart b/packages/pinball_components/lib/src/components/sparky_animatronic.dart index 714a5700..2ee2803c 100644 --- a/packages/pinball_components/lib/src/components/sparky_animatronic.dart +++ b/packages/pinball_components/lib/src/components/sparky_animatronic.dart @@ -1,17 +1,20 @@ import 'package:flame/components.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_animatronic} /// Animated Sparky that sits on top of the [SparkyComputer]. /// {@endtemplate} -class SparkyAnimatronic extends SpriteAnimationComponent with HasGameRef { +class SparkyAnimatronic extends SpriteAnimationComponent + with HasGameRef, ZIndex { /// {@macro sparky_animatronic} SparkyAnimatronic() : super( anchor: Anchor.center, playing: false, - priority: RenderPriority.sparkyAnimatronic, - ); + ) { + zIndex = ZIndexes.sparkyAnimatronic; + } @override Future onLoad() async { diff --git a/packages/pinball_components/lib/src/components/sparky_bumper.dart b/packages/pinball_components/lib/src/components/sparky_bumper.dart deleted file mode 100644 index becac26b..00000000 --- a/packages/pinball_components/lib/src/components/sparky_bumper.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'dart:math' as math; - -import 'package:flame/components.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; - -/// {@template sparky_bumper} -/// Bumper for Sparky area. -/// {@endtemplate} -class SparkyBumper extends BodyComponent with InitialPosition { - /// {@macro sparky_bumper} - SparkyBumper._({ - required double majorRadius, - required double minorRadius, - required String onAssetPath, - required String offAssetPath, - required Vector2 spritePosition, - }) : _majorRadius = majorRadius, - _minorRadius = minorRadius, - super( - priority: RenderPriority.sparkyBumper, - children: [ - _SparkyBumperSpriteGroupComponent( - onAssetPath: onAssetPath, - offAssetPath: offAssetPath, - position: spritePosition, - ), - ], - ) { - renderBody = false; - } - - /// {@macro sparky_bumper} - SparkyBumper.a() - : this._( - majorRadius: 2.9, - minorRadius: 2.1, - onAssetPath: Assets.images.sparky.bumper.a.active.keyName, - offAssetPath: Assets.images.sparky.bumper.a.inactive.keyName, - spritePosition: Vector2(0, -0.25), - ); - - /// {@macro sparky_bumper} - SparkyBumper.b() - : this._( - majorRadius: 2.85, - minorRadius: 2, - onAssetPath: Assets.images.sparky.bumper.b.active.keyName, - offAssetPath: Assets.images.sparky.bumper.b.inactive.keyName, - spritePosition: Vector2(0, -0.35), - ); - - /// {@macro sparky_bumper} - SparkyBumper.c() - : this._( - majorRadius: 3, - minorRadius: 2.2, - onAssetPath: Assets.images.sparky.bumper.c.active.keyName, - offAssetPath: Assets.images.sparky.bumper.c.inactive.keyName, - spritePosition: Vector2(0, -0.4), - ); - - final double _majorRadius; - final double _minorRadius; - - @override - Body createBody() { - renderBody = false; - - final shape = EllipseShape( - center: Vector2.zero(), - majorRadius: _majorRadius, - minorRadius: _minorRadius, - )..rotate(math.pi / 2.1); - final fixtureDef = FixtureDef( - shape, - restitution: 4, - ); - final bodyDef = BodyDef() - ..position = initialPosition - ..userData = this; - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } - - /// Animates the [DashNestBumper]. - Future animate() async { - final spriteGroupComponent = firstChild<_SparkyBumperSpriteGroupComponent>() - ?..current = SparkyBumperSpriteState.inactive; - await Future.delayed(const Duration(milliseconds: 50)); - spriteGroupComponent?.current = SparkyBumperSpriteState.active; - } -} - -/// Indicates the [SparkyBumper]'s current sprite state. -@visibleForTesting -enum SparkyBumperSpriteState { - /// A lit up bumper. - active, - - /// A dimmed bumper. - inactive, -} - -class _SparkyBumperSpriteGroupComponent - extends SpriteGroupComponent with HasGameRef { - _SparkyBumperSpriteGroupComponent({ - required String onAssetPath, - required String offAssetPath, - required Vector2 position, - }) : _onAssetPath = onAssetPath, - _offAssetPath = offAssetPath, - super( - anchor: Anchor.center, - position: position, - ); - - final String _onAssetPath; - final String _offAssetPath; - - @override - Future onLoad() async { - await super.onLoad(); - final sprites = { - SparkyBumperSpriteState.active: - Sprite(gameRef.images.fromCache(_onAssetPath)), - SparkyBumperSpriteState.inactive: - Sprite(gameRef.images.fromCache(_offAssetPath)), - }; - this.sprites = sprites; - - current = SparkyBumperSpriteState.active; - size = sprites[current]!.originalSize / 10; - } -} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/behaviors.dart new file mode 100644 index 00000000..faaa510b --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'sparky_bumper_ball_contact_behavior.dart'; +export 'sparky_bumper_blinking_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior.dart b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior.dart new file mode 100644 index 00000000..57db300c --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior.dart @@ -0,0 +1,14 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class SparkyBumperBallContactBehavior extends ContactBehavior { + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + if (other is! Ball) return; + parent.bloc.onBallContacted(); + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart new file mode 100644 index 00000000..2c2c50fe --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior.dart @@ -0,0 +1,39 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template sparky_bumper_blinking_behavior} +/// Makes a [SparkyBumper] blink back to [SparkyBumperState.lit] when +/// [SparkyBumperState.dimmed]. +/// {@endtemplate} +class SparkyBumperBlinkingBehavior extends TimerComponent + with ParentIsA { + /// {@macro sparky_bumper_blinking_behavior} + SparkyBumperBlinkingBehavior() : super(period: 0.05); + + void _onNewState(SparkyBumperState state) { + switch (state) { + case SparkyBumperState.lit: + break; + case SparkyBumperState.dimmed: + timer + ..reset() + ..start(); + break; + } + } + + @override + Future onLoad() async { + await super.onLoad(); + timer.stop(); + parent.bloc.stream.listen(_onNewState); + } + + @override + void onTick() { + super.onTick(); + timer.stop(); + parent.bloc.onBlinked(); + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart new file mode 100644 index 00000000..2f7ba7c4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_cubit.dart @@ -0,0 +1,17 @@ +// ignore_for_file: public_member_api_docs + +import 'package:bloc/bloc.dart'; + +part 'sparky_bumper_state.dart'; + +class SparkyBumperCubit extends Cubit { + SparkyBumperCubit() : super(SparkyBumperState.lit); + + void onBallContacted() { + emit(SparkyBumperState.dimmed); + } + + void onBlinked() { + emit(SparkyBumperState.lit); + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart new file mode 100644 index 00000000..096af299 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/cubit/sparky_bumper_state.dart @@ -0,0 +1,8 @@ +// ignore_for_file: public_member_api_docs + +part of 'sparky_bumper_cubit.dart'; + +enum SparkyBumperState { + lit, + dimmed, +} diff --git a/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart b/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart new file mode 100644 index 00000000..205dab62 --- /dev/null +++ b/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart @@ -0,0 +1,165 @@ +import 'dart:math' as math; + +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/sparky_bumper/behaviors/behaviors.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +export 'cubit/sparky_bumper_cubit.dart'; + +/// {@template sparky_bumper} +/// Bumper for Sparky area. +/// {@endtemplate} +class SparkyBumper extends BodyComponent with InitialPosition, ZIndex { + /// {@macro sparky_bumper} + SparkyBumper._({ + required double majorRadius, + required double minorRadius, + required String litAssetPath, + required String dimmedAssetPath, + required Vector2 spritePosition, + required this.bloc, + Iterable? children, + }) : _majorRadius = majorRadius, + _minorRadius = minorRadius, + super( + renderBody: false, + children: [ + SparkyBumperBallContactBehavior(), + SparkyBumperBlinkingBehavior(), + _SparkyBumperSpriteGroupComponent( + litAssetPath: litAssetPath, + dimmedAssetPath: dimmedAssetPath, + position: spritePosition, + state: bloc.state, + ), + ...?children, + ], + ) { + zIndex = ZIndexes.sparkyBumper; + } + + /// {@macro sparky_bumper} + SparkyBumper.a({ + Iterable? children, + }) : this._( + majorRadius: 2.9, + minorRadius: 2.1, + litAssetPath: Assets.images.sparky.bumper.a.lit.keyName, + dimmedAssetPath: Assets.images.sparky.bumper.a.dimmed.keyName, + spritePosition: Vector2(0, -0.25), + bloc: SparkyBumperCubit(), + children: children, + ); + + /// {@macro sparky_bumper} + SparkyBumper.b({ + Iterable? children, + }) : this._( + majorRadius: 2.85, + minorRadius: 2, + litAssetPath: Assets.images.sparky.bumper.b.lit.keyName, + dimmedAssetPath: Assets.images.sparky.bumper.b.dimmed.keyName, + spritePosition: Vector2(0, -0.35), + bloc: SparkyBumperCubit(), + children: children, + ); + + /// {@macro sparky_bumper} + SparkyBumper.c({ + Iterable? children, + }) : this._( + majorRadius: 3, + minorRadius: 2.2, + litAssetPath: Assets.images.sparky.bumper.c.lit.keyName, + dimmedAssetPath: Assets.images.sparky.bumper.c.dimmed.keyName, + spritePosition: Vector2(0, -0.4), + bloc: SparkyBumperCubit(), + children: children, + ); + + /// Creates an [SparkyBumper] without any children. + /// + /// This can be used for testing [SparkyBumper]'s behaviors in isolation. + // TODO(alestiago): Refactor injecting bloc once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + @visibleForTesting + SparkyBumper.test({ + required this.bloc, + }) : _majorRadius = 3, + _minorRadius = 2.2; + + final double _majorRadius; + final double _minorRadius; + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final SparkyBumperCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } + + @override + Body createBody() { + final shape = EllipseShape( + center: Vector2.zero(), + majorRadius: _majorRadius, + minorRadius: _minorRadius, + )..rotate(math.pi / 2.1); + final fixtureDef = FixtureDef( + shape, + restitution: 4, + ); + final bodyDef = BodyDef() + ..position = initialPosition + ..userData = this; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class _SparkyBumperSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _SparkyBumperSpriteGroupComponent({ + required String litAssetPath, + required String dimmedAssetPath, + required Vector2 position, + required SparkyBumperState state, + }) : _litAssetPath = litAssetPath, + _dimmedAssetPath = dimmedAssetPath, + super( + anchor: Anchor.center, + position: position, + current: state, + ); + + final String _litAssetPath; + final String _dimmedAssetPath; + + @override + Future onLoad() async { + await super.onLoad(); + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + parent.bloc.stream.listen((state) => current = state); + + final sprites = { + SparkyBumperState.lit: Sprite( + gameRef.images.fromCache(_litAssetPath), + ), + SparkyBumperState.dimmed: Sprite( + gameRef.images.fromCache(_dimmedAssetPath), + ), + }; + this.sprites = sprites; + size = sprites[current]!.originalSize / 10; + } +} diff --git a/packages/pinball_components/lib/src/components/sparky_computer.dart b/packages/pinball_components/lib/src/components/sparky_computer.dart index 481de63d..512c9d48 100644 --- a/packages/pinball_components/lib/src/components/sparky_computer.dart +++ b/packages/pinball_components/lib/src/components/sparky_computer.dart @@ -8,24 +8,25 @@ import 'package:pinball_flame/pinball_flame.dart'; /// {@template sparky_computer} /// A computer owned by Sparky. /// {@endtemplate} -class SparkyComputer extends Blueprint { +class SparkyComputer extends Component { /// {@macro sparky_computer} SparkyComputer() : super( - components: [ + children: [ _ComputerBase(), _ComputerTopSpriteComponent(), + _ComputerGlowSpriteComponent(), ], ); } -class _ComputerBase extends BodyComponent with InitialPosition { +class _ComputerBase extends BodyComponent with InitialPosition, ZIndex { _ComputerBase() : super( - priority: RenderPriority.computerBase, + renderBody: false, children: [_ComputerBaseSpriteComponent()], ) { - renderBody = false; + zIndex = ZIndexes.computerBase; } List _createFixtureDefs() { @@ -66,35 +67,65 @@ class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef { _ComputerBaseSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(-11.95, -48.35), + position: Vector2(-12.1, -48.15), ); @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.sparky.computer.base.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.sparky.computer.base.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; } } -class _ComputerTopSpriteComponent extends SpriteComponent with HasGameRef { +class _ComputerTopSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { _ComputerTopSpriteComponent() : super( anchor: Anchor.center, - position: Vector2(-12.45, -49.75), - priority: RenderPriority.computerTop, - ); + position: Vector2(-12.52, -49.37), + ) { + zIndex = ZIndexes.computerTop; + } + + @override + Future onLoad() async { + await super.onLoad(); + + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.sparky.computer.top.keyName, + ), + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + } +} + +class _ComputerGlowSpriteComponent extends SpriteComponent + with HasGameRef, ZIndex { + _ComputerGlowSpriteComponent() + : super( + anchor: Anchor.center, + position: Vector2(7.4, 10), + ) { + zIndex = ZIndexes.computerGlow; + } @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.sparky.computer.top.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.sparky.computer.glow.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; diff --git a/packages/pinball_components/lib/src/components/z_indexes.dart b/packages/pinball_components/lib/src/components/z_indexes.dart new file mode 100644 index 00000000..e38683a2 --- /dev/null +++ b/packages/pinball_components/lib/src/components/z_indexes.dart @@ -0,0 +1,110 @@ +// ignore_for_file: public_member_api_docs + +/// Z-Indexes for the component rendering order in the pinball game. +// TODO(allisonryan0002): find alternative to section comments. +abstract class ZIndexes { + static const _base = 0; + static const _above = 1; + static const _below = -1; + + // Ball + + static const ballOnBoard = _base; + + static const ballOnSpaceshipRamp = _above + spaceshipRampBackgroundRailing; + + static const ballOnSpaceship = _above + spaceshipSaucer; + + static const ballOnSpaceshipRail = _above + spaceshipRail; + + static const ballOnLaunchRamp = _above + launchRamp; + + // Background + + // TODO(allisonryan0002): fix this magic zindex. Could bump all priorities so + // there are no negatives. + static const boardBackground = 3 * _below + _base; + + static const decal = _above + boardBackground; + + // Boundaries + + static const bottomBoundary = _above + dinoBottomWall; + + static const outerBoundary = _above + boardBackground; + + static const outerBottomBoundary = _above + rocket; + + // Bottom Group + + static const bottomGroup = _above + ballOnBoard; + + // Launcher + + static const launchRamp = _above + outerBoundary; + + static const launchRampForegroundRailing = _above + ballOnLaunchRamp; + + static const plunger = _above + launchRamp; + + static const rocket = _below + bottomBoundary; + + // Dino Desert + + static const dinoTopWall = _above + ballOnBoard; + + static const dino = _above + dinoTopWall; + + static const dinoBottomWall = _above + dino; + + static const slingshots = _above + dinoBottomWall; + + // Flutter Forest + + static const flutterForest = _above + launchRampForegroundRailing; + + // Sparky Scorch + + static const computerBase = _below + ballOnBoard; + + static const computerTop = _above + ballOnBoard; + + static const computerGlow = _above + ballOnBoard; + + static const sparkyAnimatronic = _above + spaceshipRampForegroundRailing; + + static const sparkyBumper = _above + ballOnBoard; + + static const turboChargeFlame = _above + ballOnBoard; + + // Android Acres + + static const spaceshipRail = _above + bottomGroup; + + static const spaceshipRailExit = _above + ballOnSpaceshipRail; + + static const spaceshipSaucer = _above + ballOnSpaceshipRail; + + static const spaceshipLightBeam = _below + spaceshipSaucer; + + static const androidHead = _above + ballOnSpaceship; + + static const spaceshipRamp = _above + sparkyBumper; + + static const spaceshipRampBackgroundRailing = _above + spaceshipRamp; + + static const spaceshipRampArrow = _above + spaceshipRamp; + + static const spaceshipRampForegroundRailing = _above + ballOnSpaceshipRamp; + + static const spaceshipRampBoardOpening = _below + ballOnBoard; + + static const androidBumper = _above + ballOnBoard; + + // Score Text + + static const scoreText = _above + spaceshipRampForegroundRailing; + + // Debug information + static const debugInfo = _above + scoreText; +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index 9017b931..4ca7f28e 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -7,8 +7,13 @@ environment: sdk: ">=2.16.0 <3.0.0" dependencies: + bloc: ^8.0.3 flame: ^1.1.1 - flame_forge2d: ^0.11.0 + flame_forge2d: + git: + url: https://github.com/flame-engine/flame/ + path: packages/flame_forge2d/ + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f flutter: sdk: flutter geometry: @@ -19,8 +24,8 @@ dependencies: pinball_theme: path: ../pinball_theme - dev_dependencies: + bloc_test: ^9.0.3 flame_test: ^1.3.0 flutter_test: sdk: flutter @@ -45,22 +50,24 @@ flutter: - assets/images/baseboard/ - assets/images/boundary/ - assets/images/dino/ + - assets/images/dino/animatronic/ - assets/images/flipper/ - assets/images/launch_ramp/ - assets/images/dash/ - assets/images/dash/bumper/a/ - assets/images/dash/bumper/b/ - assets/images/dash/bumper/main/ - - assets/images/spaceship/ - - assets/images/spaceship/rail/ - - assets/images/spaceship/ramp/ - - assets/images/spaceship/ramp/arrow/ - - assets/images/chrome_dino/ - - assets/images/kicker/ + - assets/images/android/spaceship/ + - assets/images/android/rail/ + - assets/images/android/ramp/ + - assets/images/android/ramp/arrow/ + - assets/images/android/bumper/a/ + - assets/images/android/bumper/b/ + - assets/images/android/bumper/cow/ + - assets/images/kicker/left/ + - assets/images/kicker/right/ - assets/images/plunger/ - assets/images/slingshot/ - - assets/images/alien_bumper/a/ - - assets/images/alien_bumper/b/ - assets/images/sparky/ - assets/images/sparky/computer/ - assets/images/sparky/bumper/a/ @@ -74,6 +81,11 @@ flutter: - assets/images/google_word/letter5/ - assets/images/google_word/letter6/ - assets/images/signpost/ + - assets/images/multiplier/x2/ + - assets/images/multiplier/x3/ + - assets/images/multiplier/x4/ + - assets/images/multiplier/x5/ + - assets/images/multiplier/x6/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 8709d694..c123c2d9 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -6,7 +6,6 @@ // https://opensource.org/licenses/MIT. import 'package:dashbook/dashbook.dart'; import 'package:flutter/material.dart'; -import 'package:sandbox/stories/kicker/stories.dart'; import 'package:sandbox/stories/stories.dart'; void main() { @@ -15,20 +14,20 @@ void main() { addBallStories(dashbook); addLayerStories(dashbook); addEffectsStories(dashbook); - addFlipperStories(dashbook); - addBaseboardStories(dashbook); addChromeDinoStories(dashbook); - addDashNestBumperStories(dashbook); - addKickerStories(dashbook); + addFlutterForestStories(dashbook); + addBottomGroupStories(dashbook); addPlungerStories(dashbook); addSlingshotStories(dashbook); - addSparkyBumperStories(dashbook); - addAlienZoneStories(dashbook); + addSparkyScorchStories(dashbook); + addAndroidAcresStories(dashbook); addBoundariesStories(dashbook); addGoogleWordStories(dashbook); addLaunchRampStories(dashbook); addScoreTextStories(dashbook); addBackboardStories(dashbook); + addDinoWallStories(dashbook); + addMultipliersStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_game.dart deleted file mode 100644 index ad897dd4..00000000 --- a/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_game.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:async'; - -import 'package:flame/input.dart'; -import 'package:flutter/material.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; -import 'package:sandbox/common/common.dart'; - -class SpaceshipGame extends AssetsGame with TapDetector { - static const description = ''' - Shows how a Spaceship works. - - - Tap anywhere on the screen to spawn a Ball into the game. -'''; - - @override - Future onLoad() async { - await super.onLoad(); - - camera.followVector2(Vector2.zero()); - await addFromBlueprint( - Spaceship(position: Vector2.zero()), - ); - await ready(); - } - - @override - void onTapUp(TapUpInfo info) { - add( - Ball(baseColor: Colors.blue) - ..initialPosition = info.eventPosition.game - ..layer = Layer.spaceshipEntranceRamp, - ); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart b/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart deleted file mode 100644 index b4e7c1b6..00000000 --- a/packages/pinball_components/sandbox/lib/stories/alien_zone/stories.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/alien_zone/alien_bumper_a_game.dart'; -import 'package:sandbox/stories/alien_zone/alien_bumper_b_game.dart'; -import 'package:sandbox/stories/alien_zone/spaceship_game.dart'; -import 'package:sandbox/stories/alien_zone/spaceship_rail_game.dart'; -import 'package:sandbox/stories/alien_zone/spaceship_ramp_game.dart'; - -void addAlienZoneStories(Dashbook dashbook) { - dashbook.storiesOf('Alien Zone') - ..addGame( - title: 'Alien Bumper A', - description: AlienBumperAGame.description, - gameBuilder: (_) => AlienBumperAGame(), - ) - ..addGame( - title: 'Alien Bumper B', - description: AlienBumperBGame.description, - gameBuilder: (_) => AlienBumperBGame(), - ) - ..addGame( - title: 'Spaceship', - description: SpaceshipGame.description, - gameBuilder: (_) => SpaceshipGame(), - ) - ..addGame( - title: 'Spaceship Rail', - description: SpaceshipRailGame.description, - gameBuilder: (_) => SpaceshipRailGame(), - ) - ..addGame( - title: 'Spaceship Ramp', - description: SpaceshipRampGame.description, - gameBuilder: (_) => SpaceshipRampGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart similarity index 67% rename from packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart rename to packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart index abb206ca..32638c2d 100644 --- a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_b_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_a_game.dart @@ -4,18 +4,18 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class AlienBumperBGame extends BallGame { - AlienBumperBGame() +class AndroidBumperAGame extends BallGame { + AndroidBumperAGame() : super( color: const Color(0xFF0000FF), imagesFileNames: [ - Assets.images.alienBumper.b.active.keyName, - Assets.images.alienBumper.b.inactive.keyName, + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, ], ); static const description = ''' - Shows how a AlienBumperB is rendered. + Shows how a AndroidBumperA is rendered. - Activate the "trace" parameter to overlay the body. '''; @@ -26,7 +26,7 @@ class AlienBumperBGame extends BallGame { camera.followVector2(Vector2.zero()); await add( - AlienBumper.b()..priority = 1, + AndroidBumper.a()..priority = 1, ); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart similarity index 67% rename from packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart rename to packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart index 4832a468..bfd4206c 100644 --- a/packages/pinball_components/sandbox/lib/stories/alien_zone/alien_bumper_a_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_b_game.dart @@ -4,18 +4,18 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class AlienBumperAGame extends BallGame { - AlienBumperAGame() +class AndroidBumperBGame extends BallGame { + AndroidBumperBGame() : super( color: const Color(0xFF0000FF), imagesFileNames: [ - Assets.images.alienBumper.a.active.keyName, - Assets.images.alienBumper.a.inactive.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, ], ); static const description = ''' - Shows how a AlienBumperA is rendered. + Shows how a AndroidBumperB is rendered. - Activate the "trace" parameter to overlay the body. '''; @@ -26,7 +26,7 @@ class AlienBumperAGame extends BallGame { camera.followVector2(Vector2.zero()); await add( - AlienBumper.a()..priority = 1, + AndroidBumper.b()..priority = 1, ); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_cow_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_cow_game.dart new file mode 100644 index 00000000..ac1bc6fe --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_bumper_cow_game.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:flame/extensions.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class AndroidBumperCowGame extends BallGame { + AndroidBumperCowGame() + : super( + imagesFileNames: [ + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, + ], + ); + + static const description = ''' + Shows how a AndroidBumper.cow is rendered. + + - Activate the "trace" parameter to overlay the body. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + await add( + AndroidBumper.cow()..priority = 1, + ); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart new file mode 100644 index 00000000..6799ef29 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/android_spaceship_game.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class AndroidSpaceshipGame extends BallGame { + AndroidSpaceshipGame() + : super( + ballPriority: ZIndexes.ballOnSpaceship, + ballLayer: Layer.spaceship, + imagesFileNames: [ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + ], + ); + + static const description = ''' + Shows how the AndroidSpaceship is rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a Ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + await add( + AndroidSpaceship(position: Vector2.zero()), + ); + + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_rail_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart similarity index 76% rename from packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_rail_game.dart rename to packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart index 2a13fb5e..dee83e26 100644 --- a/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_rail_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_rail_game.dart @@ -3,15 +3,18 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRailGame extends BallGame { SpaceshipRailGame() : super( color: Colors.blue, - ballPriority: RenderPriority.ballOnSpaceshipRail, + ballPriority: ZIndexes.ballOnSpaceshipRail, ballLayer: Layer.spaceshipExitRail, + imagesFileNames: [ + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + ], ); static const description = ''' @@ -26,7 +29,7 @@ class SpaceshipRailGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2(-30, -10)); - await addFromBlueprint(SpaceshipRail()); + await add(SpaceshipRail()); await ready(); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart similarity index 62% rename from packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_ramp_game.dart rename to packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart index 1817f40a..cabe4d54 100644 --- a/packages/pinball_components/sandbox/lib/stories/alien_zone/spaceship_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/spaceship_ramp_game.dart @@ -4,26 +4,25 @@ import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SpaceshipRampGame extends BallGame with KeyboardEvents { SpaceshipRampGame() : super( color: Colors.blue, - ballPriority: RenderPriority.ballOnSpaceshipRamp, + ballPriority: ZIndexes.ballOnSpaceshipRamp, ballLayer: Layer.spaceshipEntranceRamp, imagesFileNames: [ - Assets.images.spaceship.ramp.railingBackground.keyName, - Assets.images.spaceship.ramp.main.keyName, - Assets.images.spaceship.ramp.boardOpening.keyName, - Assets.images.spaceship.ramp.railingForeground.keyName, - Assets.images.spaceship.ramp.arrow.inactive.keyName, - Assets.images.spaceship.ramp.arrow.active1.keyName, - Assets.images.spaceship.ramp.arrow.active2.keyName, - Assets.images.spaceship.ramp.arrow.active3.keyName, - Assets.images.spaceship.ramp.arrow.active4.keyName, - Assets.images.spaceship.ramp.arrow.active5.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, ], ); @@ -42,7 +41,7 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents { await super.onLoad(); camera.followVector2(Vector2(-12, -50)); - await addFromBlueprint( + await add( _spaceshipRamp = SpaceshipRamp(), ); await traceAllBodies(); diff --git a/packages/pinball_components/sandbox/lib/stories/android_acres/stories.dart b/packages/pinball_components/sandbox/lib/stories/android_acres/stories.dart new file mode 100644 index 00000000..ec4a783e --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/android_acres/stories.dart @@ -0,0 +1,42 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/android_acres/android_bumper_a_game.dart'; +import 'package:sandbox/stories/android_acres/android_bumper_b_game.dart'; +import 'package:sandbox/stories/android_acres/android_bumper_cow_game.dart'; +import 'package:sandbox/stories/android_acres/android_spaceship_game.dart'; +import 'package:sandbox/stories/android_acres/spaceship_rail_game.dart'; +import 'package:sandbox/stories/android_acres/spaceship_ramp_game.dart'; + +void addAndroidAcresStories(Dashbook dashbook) { + dashbook.storiesOf('Android Acres') + ..addGame( + title: 'Android Bumper A', + description: AndroidBumperAGame.description, + gameBuilder: (_) => AndroidBumperAGame(), + ) + ..addGame( + title: 'Android Bumper B', + description: AndroidBumperBGame.description, + gameBuilder: (_) => AndroidBumperBGame(), + ) + ..addGame( + title: 'Android Bumper Cow', + description: AndroidBumperCowGame.description, + gameBuilder: (_) => AndroidBumperCowGame(), + ) + ..addGame( + title: 'Android Spaceship', + description: AndroidSpaceshipGame.description, + gameBuilder: (_) => AndroidSpaceshipGame(), + ) + ..addGame( + title: 'Spaceship Rail', + description: SpaceshipRailGame.description, + gameBuilder: (_) => SpaceshipRailGame(), + ) + ..addGame( + title: 'Spaceship Ramp', + description: SpaceshipRampGame.description, + gameBuilder: (_) => SpaceshipRampGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart b/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart deleted file mode 100644 index b07e3a73..00000000 --- a/packages/pinball_components/sandbox/lib/stories/baseboard/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/baseboard/baseboard_game.dart'; - -void addBaseboardStories(Dashbook dashbook) { - dashbook.storiesOf('Baseboard').addGame( - title: 'Traced', - description: BaseboardGame.description, - gameBuilder: (_) => BaseboardGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/baseboard_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/baseboard/baseboard_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/baseboard_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart new file mode 100644 index 00000000..d0cc7322 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/bottom_group.dart @@ -0,0 +1 @@ +export 'stories.dart'; diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart similarity index 100% rename from packages/pinball_components/sandbox/lib/stories/flipper/flipper_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart similarity index 61% rename from packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart rename to packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart index 9b7d96cc..590638e0 100644 --- a/packages/pinball_components/sandbox/lib/stories/kicker/kicker_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/kicker_game.dart @@ -3,6 +3,16 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class KickerGame extends BallGame { + KickerGame() + : super( + imagesFileNames: [ + Assets.images.kicker.left.lit.keyName, + Assets.images.kicker.left.dimmed.keyName, + Assets.images.kicker.right.lit.keyName, + Assets.images.kicker.right.dimmed.keyName, + ], + ); + static const description = ''' Shows how Kickers are rendered. @@ -18,9 +28,9 @@ class KickerGame extends BallGame { await addAll( [ Kicker(side: BoardSide.left) - ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y), + ..initialPosition = Vector2(center.x - 8.8, center.y), Kicker(side: BoardSide.right) - ..initialPosition = Vector2(center.x + (Kicker.size.x * 2), center.y), + ..initialPosition = Vector2(center.x + 8.8, center.y), ], ); diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart new file mode 100644 index 00000000..7712ca79 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/stories.dart @@ -0,0 +1,24 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/bottom_group/baseboard_game.dart'; +import 'package:sandbox/stories/bottom_group/flipper_game.dart'; +import 'package:sandbox/stories/bottom_group/kicker_game.dart'; + +void addBottomGroupStories(Dashbook dashbook) { + dashbook.storiesOf('Bottom Group') + ..addGame( + title: 'Flipper', + description: FlipperGame.description, + gameBuilder: (_) => FlipperGame(), + ) + ..addGame( + title: 'Kicker', + description: KickerGame.description, + gameBuilder: (_) => KickerGame(), + ) + ..addGame( + title: 'Baseboard', + description: BaseboardGame.description, + gameBuilder: (_) => BaseboardGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart b/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart index cf78750d..12e8ec26 100644 --- a/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/boundaries/boundaries_game.dart @@ -1,6 +1,5 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class BoundariesGame extends BallGame { @@ -27,7 +26,7 @@ class BoundariesGame extends BallGame { camera ..followVector2(Vector2.zero()) ..zoom = 6; - await addFromBlueprint(Boundaries()); + await add(Boundaries()); await ready(); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart index 2e6831e3..d6e7ef95 100644 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/chrome_dino/chrome_dino_game.dart @@ -1,8 +1,22 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class ChromeDinoGame extends Forge2DGame { - static const description = 'Shows how a ChromeDino is rendered.'; +class ChromeDinoGame extends BallGame { + ChromeDinoGame() + : super( + imagesFileNames: [ + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.animatronic.head.keyName, + ], + ); + + static const description = ''' + Shows how ChromeDino is rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; @override Future onLoad() async { @@ -10,5 +24,7 @@ class ChromeDinoGame extends Forge2DGame { camera.followVector2(Vector2.zero()); await add(ChromeDino()); + + await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart index 391cdca7..a4c70c03 100644 --- a/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/chrome_dino/stories.dart @@ -4,7 +4,7 @@ import 'package:sandbox/stories/chrome_dino/chrome_dino_game.dart'; void addChromeDinoStories(Dashbook dashbook) { dashbook.storiesOf('Chrome Dino').addGame( - title: 'Trace', + title: 'Traced', description: ChromeDinoGame.description, gameBuilder: (_) => ChromeDinoGame(), ); diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart new file mode 100644 index 00000000..0d213aa4 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class DinoWallGame extends BallGame { + DinoWallGame() : super(); + + static const description = ''' + Shows how DinoWalls are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + await images.loadAll([ + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, + ]); + + await add(DinoWalls()); + camera.followVector2(Vector2.zero()); + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart new file mode 100644 index 00000000..e24d26cc --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/dino_wall/dino_wall_game.dart'; + +void addDinoWallStories(Dashbook dashbook) { + dashbook.storiesOf('DinoWall').addGame( + title: 'Traced', + description: DinoWallGame.description, + gameBuilder: (_) => DinoWallGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart b/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart deleted file mode 100644 index 2ef2a4b6..00000000 --- a/packages/pinball_components/sandbox/lib/stories/flipper/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/flipper/flipper_game.dart'; - -void addFlipperStories(Dashbook dashbook) { - dashbook.storiesOf('Flipper').addGame( - title: 'Traced', - description: FlipperGame.description, - gameBuilder: (_) => FlipperGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart index ef9c1ffb..dd557a27 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart @@ -5,7 +5,7 @@ import 'package:sandbox/stories/flutter_forest/signpost_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart'; -void addDashNestBumperStories(Dashbook dashbook) { +void addFlutterForestStories(Dashbook dashbook) { dashbook.storiesOf('Flutter Forest') ..addGame( title: 'Signpost', diff --git a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart index d8022e57..be90fdb9 100644 --- a/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/google_word/google_letter_game.dart @@ -17,7 +17,6 @@ class GoogleLetterGame extends BallGame { @override Future onLoad() async { await super.onLoad(); - addContactCallback(_BallGoogleLetterContactCallback()); camera.followVector2(Vector2.zero()); await add(GoogleLetter(0)); @@ -25,12 +24,3 @@ class GoogleLetterGame extends BallGame { await traceAllBodies(); } } - -class _BallGoogleLetterContactCallback - extends ContactCallback { - @override - void begin(Ball a, GoogleLetter b, Contact contact) { - super.begin(a, b, contact); - b.activate(); - } -} diff --git a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart b/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart deleted file mode 100644 index cfebb7e4..00000000 --- a/packages/pinball_components/sandbox/lib/stories/kicker/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/kicker/kicker_game.dart'; - -void addKickerStories(Dashbook dashbook) { - dashbook.storiesOf('Kickers').addGame( - title: 'Traced', - description: KickerGame.description, - gameBuilder: (_) => KickerGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart index 1be94133..ea3bd4db 100644 --- a/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/launch_ramp/launch_ramp_game.dart @@ -3,19 +3,18 @@ import 'dart:async'; import 'package:flame/input.dart'; import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class LaunchRampGame extends BallGame { LaunchRampGame() : super( color: Colors.blue, - ballPriority: RenderPriority.ballOnLaunchRamp, + ballPriority: ZIndexes.ballOnLaunchRamp, ballLayer: Layer.launcher, ); static const description = ''' - Shows how LaunchRamp are rendered. + Shows how the LaunchRamp is rendered. - Activate the "trace" parameter to overlay the body. - Tap anywhere on the screen to spawn a ball into the game. @@ -26,9 +25,9 @@ class LaunchRampGame extends BallGame { await super.onLoad(); camera - ..followVector2(Vector2(0, 0)) + ..followVector2(Vector2.zero()) ..zoom = 7.5; - await addFromBlueprint(LaunchRamp()); + await add(LaunchRamp()); await ready(); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/multipliers/multipliers_game.dart b/packages/pinball_components/sandbox/lib/stories/multipliers/multipliers_game.dart new file mode 100644 index 00000000..ae641623 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multipliers/multipliers_game.dart @@ -0,0 +1,97 @@ +import 'dart:math' as math; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class MultipliersGame extends BallGame with KeyboardEvents { + MultipliersGame() + : super( + imagesFileNames: [ + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ], + ); + + static const description = ''' + Shows how the Multipliers are rendered. + + - Tap anywhere on the screen to spawn a ball into the game. + - Press digits 2 to 6 for toggle state multipliers 2 to 6. +'''; + + final List multipliers = [ + Multiplier.x2( + position: Vector2(-20, 0), + angle: -15 * math.pi / 180, + ), + Multiplier.x3( + position: Vector2(20, -5), + angle: 15 * math.pi / 180, + ), + Multiplier.x4( + position: Vector2(0, -15), + angle: 0, + ), + Multiplier.x5( + position: Vector2(-10, -25), + angle: -3 * math.pi / 180, + ), + Multiplier.x6( + position: Vector2(10, -35), + angle: 8 * math.pi / 180, + ), + ]; + + @override + Future onLoad() async { + await super.onLoad(); + + camera.followVector2(Vector2.zero()); + + await addAll(multipliers); + await traceAllBodies(); + } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + if (event is RawKeyDownEvent) { + var currentMultiplier = 1; + + if (event.logicalKey == LogicalKeyboardKey.digit2) { + currentMultiplier = 2; + } + if (event.logicalKey == LogicalKeyboardKey.digit3) { + currentMultiplier = 3; + } + if (event.logicalKey == LogicalKeyboardKey.digit4) { + currentMultiplier = 4; + } + if (event.logicalKey == LogicalKeyboardKey.digit5) { + currentMultiplier = 5; + } + if (event.logicalKey == LogicalKeyboardKey.digit6) { + currentMultiplier = 6; + } + + for (final multiplier in multipliers) { + multiplier.bloc.next(currentMultiplier); + } + + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/multipliers/stories.dart b/packages/pinball_components/sandbox/lib/stories/multipliers/stories.dart new file mode 100644 index 00000000..48b6da6d --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/multipliers/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/multipliers/multipliers_game.dart'; + +void addMultipliersStories(Dashbook dashbook) { + dashbook.storiesOf('Multipliers').addGame( + title: 'Multipliers', + description: MultipliersGame.description, + gameBuilder: (_) => MultipliersGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart index 29eded8c..0f1ec2e4 100644 --- a/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/plunger/plunger_game.dart @@ -29,7 +29,7 @@ class PlungerGame extends BallGame with KeyboardEvents, Traceable { final center = screenToWorld(camera.viewport.canvasSize! / 2); await add( plunger = Plunger(compressionDistance: 29) - ..initialPosition = Vector2(center.x - (Kicker.size.x * 2), center.y), + ..initialPosition = Vector2(center.x - 8.8, center.y), ); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart index 28858088..11c38c0c 100644 --- a/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/slingshot/slingshot_game.dart @@ -1,6 +1,5 @@ import 'package:flame/extensions.dart'; import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; class SlingshotGame extends BallGame { @@ -24,7 +23,7 @@ class SlingshotGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2.zero()); - await addFromBlueprint(Slingshots()); + await add(Slingshots()); await ready(); await traceAllBodies(); } diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart b/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart deleted file mode 100644 index 418636db..00000000 --- a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/stories.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/sparky_bumper/sparky_bumper_game.dart'; - -void addSparkyBumperStories(Dashbook dashbook) { - dashbook.storiesOf('Sparky Bumpers').addGame( - title: 'Traced', - description: SparkyBumperGame.description, - gameBuilder: (_) => SparkyBumperGame(), - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_bumper_game.dart similarity index 74% rename from packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart rename to packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_bumper_game.dart index 9bebde9e..c45c4895 100644 --- a/packages/pinball_components/sandbox/lib/stories/sparky_bumper/sparky_bumper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_bumper_game.dart @@ -9,6 +9,7 @@ class SparkyBumperGame extends BallGame { Shows how a SparkyBumper is rendered. - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. '''; @override @@ -16,12 +17,12 @@ class SparkyBumperGame extends BallGame { await super.onLoad(); await images.loadAll([ - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, ]); final center = screenToWorld(camera.viewport.canvasSize! / 2); diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_computer_game.dart b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_computer_game.dart new file mode 100644 index 00000000..b4002479 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/sparky_computer_game.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SparkyComputerGame extends BallGame { + static const description = ''' + Shows how the SparkyComputer is rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + await images.loadAll([ + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.glow.keyName, + ]); + + camera.followVector2(Vector2(-10, -40)); + await add(SparkyComputer()); + await ready(); + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/sparky_scorch/stories.dart b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/stories.dart new file mode 100644 index 00000000..9fb47f57 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/sparky_scorch/stories.dart @@ -0,0 +1,18 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/sparky_scorch/sparky_bumper_game.dart'; +import 'package:sandbox/stories/sparky_scorch/sparky_computer_game.dart'; + +void addSparkyScorchStories(Dashbook dashbook) { + dashbook.storiesOf('Sparky Scorch') + ..addGame( + title: 'Sparky Computer', + description: SparkyComputerGame.description, + gameBuilder: (_) => SparkyComputerGame(), + ) + ..addGame( + title: 'Sparky Bumper', + description: SparkyBumperGame.description, + gameBuilder: (_) => SparkyBumperGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 90c38150..d8641b9c 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,16 +1,17 @@ -export 'alien_zone/stories.dart'; +export 'android_acres/stories.dart'; export 'backboard/stories.dart'; export 'ball/stories.dart'; -export 'baseboard/stories.dart'; +export 'bottom_group/stories.dart'; export 'boundaries/stories.dart'; export 'chrome_dino/stories.dart'; +export 'dino_wall/stories.dart'; export 'effects/stories.dart'; -export 'flipper/stories.dart'; export 'flutter_forest/stories.dart'; export 'google_word/stories.dart'; export 'launch_ramp/stories.dart'; export 'layer/stories.dart'; +export 'multipliers/stories.dart'; export 'plunger/stories.dart'; export 'score_text/stories.dart'; export 'slingshot/stories.dart'; -export 'sparky_bumper/stories.dart'; +export 'sparky_scorch/stories.dart'; diff --git a/packages/pinball_components/sandbox/pubspec.lock b/packages/pinball_components/sandbox/pubspec.lock index d7ab8901..d2500fbe 100644 --- a/packages/pinball_components/sandbox/pubspec.lock +++ b/packages/pinball_components/sandbox/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + bloc: + dependency: transitive + description: + name: bloc + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.3" boolean_selector: dependency: transitive description: @@ -102,9 +109,11 @@ packages: flame_forge2d: dependency: "direct main" description: - name: flame_forge2d - url: "https://pub.dartlang.org" - source: hosted + path: "packages/flame_forge2d" + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f + resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f + url: "https://github.com/flame-engine/flame/" + source: git version: "0.11.0" flutter: dependency: "direct main" diff --git a/packages/pinball_components/sandbox/pubspec.yaml b/packages/pinball_components/sandbox/pubspec.yaml index dd9f8259..d663cb04 100644 --- a/packages/pinball_components/sandbox/pubspec.yaml +++ b/packages/pinball_components/sandbox/pubspec.yaml @@ -9,7 +9,11 @@ environment: dependencies: dashbook: ^0.1.7 flame: ^1.1.1 - flame_forge2d: ^0.11.0 + flame_forge2d: + git: + url: https://github.com/flame-engine/flame/ + path: packages/flame_forge2d/ + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f flutter: sdk: flutter pinball_components: diff --git a/packages/pinball_components/test/helpers/mocks.dart b/packages/pinball_components/test/helpers/mocks.dart index 520555df..33c5670d 100644 --- a/packages/pinball_components/test/helpers/mocks.dart +++ b/packages/pinball_components/test/helpers/mocks.dart @@ -15,7 +15,14 @@ class MockGame extends Mock implements Forge2DGame {} class MockContact extends Mock implements Contact {} -class MockContactCallback extends Mock - implements ContactCallback {} - class MockComponent extends Mock implements Component {} + +class MockAndroidBumperCubit extends Mock implements AndroidBumperCubit {} + +class MockGoogleLetterCubit extends Mock implements GoogleLetterCubit {} + +class MockSparkyBumperCubit extends Mock implements SparkyBumperCubit {} + +class MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} + +class MockMultiplierCubit extends Mock implements MultiplierCubit {} diff --git a/packages/pinball_components/test/src/components/alien_bumper_test.dart b/packages/pinball_components/test/src/components/alien_bumper_test.dart deleted file mode 100644 index c6384759..00000000 --- a/packages/pinball_components/test/src/components/alien_bumper_test.dart +++ /dev/null @@ -1,61 +0,0 @@ -// 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 '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.alienBumper.a.active.keyName, - Assets.images.alienBumper.a.inactive.keyName, - Assets.images.alienBumper.b.active.keyName, - Assets.images.alienBumper.b.inactive.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); - - group('AlienBumper', () { - flameTester.test('"a" loads correctly', (game) async { - final bumper = AlienBumper.a(); - await game.ensureAdd(bumper); - - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('"b" loads correctly', (game) async { - final bumper = AlienBumper.b(); - await game.ensureAdd(bumper); - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('animate switches between on and off sprites', - (game) async { - final bumper = AlienBumper.a(); - await game.ensureAdd(bumper); - - final spriteGroupComponent = bumper.firstChild()!; - - expect( - spriteGroupComponent.current, - equals(AlienBumperSpriteState.active), - ); - - final future = bumper.animate(); - - expect( - spriteGroupComponent.current, - equals(AlienBumperSpriteState.inactive), - ); - - await future; - - expect( - spriteGroupComponent.current, - equals(AlienBumperSpriteState.active), - ); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart b/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart new file mode 100644 index 00000000..a5256b79 --- /dev/null +++ b/packages/pinball_components/test/src/components/android_bumper/android_bumper_test.dart @@ -0,0 +1,86 @@ +// 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/android_bumper/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('AndroidBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final androidBumper = AndroidBumper.a(); + await game.ensureAdd(androidBumper); + expect(game.contains(androidBumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final androidBumper = AndroidBumper.b(); + await game.ensureAdd(androidBumper); + expect(game.contains(androidBumper), isTrue); + }); + + flameTester.test('"cow" loads correctly', (game) async { + final androidBumper = AndroidBumper.cow(); + await game.ensureAdd(androidBumper); + expect(game.contains(androidBumper), isTrue); + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = MockAndroidBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: AndroidBumperState.lit, + ); + when(bloc.close).thenAnswer((_) async {}); + final androidBumper = AndroidBumper.test(bloc: bloc); + + await game.ensureAdd(androidBumper); + game.remove(androidBumper); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final androidBumper = AndroidBumper.a( + children: [component], + ); + await game.ensureAdd(androidBumper); + expect(androidBumper.children, contains(component)); + }); + + flameTester.test('an AndroidBumperBallContactBehavior', (game) async { + final androidBumper = AndroidBumper.a(); + await game.ensureAdd(androidBumper); + expect( + androidBumper.children + .whereType() + .single, + isNotNull, + ); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior_test.dart new file mode 100644 index 00000000..69e6ce43 --- /dev/null +++ b/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_ball_contact_behavior_test.dart @@ -0,0 +1,48 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.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/android_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'AndroidBumperBallContactBehavior', + () { + test('can be instantiated', () { + expect( + AndroidBumperBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact emits onBallContacted when contacts with a ball', + (game) async { + final behavior = AndroidBumperBallContactBehavior(); + final bloc = MockAndroidBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: AndroidBumperState.lit, + ); + + final androidBumper = AndroidBumper.test(bloc: bloc); + await androidBumper.add(behavior); + await game.ensureAdd(androidBumper); + + behavior.beginContact(MockBall(), MockContact()); + + verify(androidBumper.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_blinking_behavior_test.dart new file mode 100644 index 00000000..f7b09dfb --- /dev/null +++ b/packages/pinball_components/test/src/components/android_bumper/behaviors/android_bumper_blinking_behavior_test.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.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/android_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'AndroidBumperBlinkingBehavior', + () { + flameTester.testGameWidget( + 'calls onBlinked after 0.05 seconds when dimmed', + setUp: (game, tester) async { + final behavior = AndroidBumperBlinkingBehavior(); + final bloc = MockAndroidBumperCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: AndroidBumperState.lit, + ); + + final androidBumper = AndroidBumper.test(bloc: bloc); + await androidBumper.add(behavior); + await game.ensureAdd(androidBumper); + + streamController.add(AndroidBumperState.dimmed); + await tester.pump(); + game.update(0.05); + + await streamController.close(); + verify(bloc.onBlinked).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/android_bumper/cubit/android_bumper_cubit_test.dart b/packages/pinball_components/test/src/components/android_bumper/cubit/android_bumper_cubit_test.dart new file mode 100644 index 00000000..06095228 --- /dev/null +++ b/packages/pinball_components/test/src/components/android_bumper/cubit/android_bumper_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'AndroidBumperCubit', + () { + blocTest( + 'onBallContacted emits dimmed', + build: AndroidBumperCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [AndroidBumperState.dimmed], + ); + + blocTest( + 'onBlinked emits lit', + build: AndroidBumperCubit.new, + act: (bloc) => bloc.onBlinked(), + expect: () => [AndroidBumperState.lit], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/android_spaceship_test.dart b/packages/pinball_components/test/src/components/android_spaceship_test.dart new file mode 100644 index 00000000..7e7eda96 --- /dev/null +++ b/packages/pinball_components/test/src/components/android_spaceship_test.dart @@ -0,0 +1,67 @@ +// 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_flame/pinball_flame.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('AndroidSpaceship', () { + final assets = [ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('loads correctly', (game) async { + final component = AndroidSpaceship(position: Vector2.zero()); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final canvas = ZCanvasComponent( + children: [AndroidSpaceship(position: Vector2.zero())], + ); + await game.ensureAdd(canvas); + game.camera.followVector2(Vector2.zero()); + await game.ready(); + await tester.pump(); + }, + verify: (game, tester) async { + final animationDuration = game + .descendants() + .whereType() + .last + .animation! + .totalDuration(); + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/android_spaceship/start.png'), + ); + + game.update(animationDuration * 0.5); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/android_spaceship/middle.png'), + ); + + game.update(animationDuration * 0.5); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/android_spaceship/end.png'), + ); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/board_background_sprite_component_test.dart b/packages/pinball_components/test/src/components/board_background_sprite_component_test.dart new file mode 100644 index 00000000..df35594f --- /dev/null +++ b/packages/pinball_components/test/src/components/board_background_sprite_component_test.dart @@ -0,0 +1,48 @@ +// 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 '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.boardBackground.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('BoardBackgroundSpriteComponent', () { + flameTester.test( + 'loads correctly', + (game) async { + final boardBackground = BoardBackgroundSpriteComponent(); + await game.ensureAdd(boardBackground); + + expect(game.contains(boardBackground), isTrue); + }, + ); + + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + final boardBackground = BoardBackgroundSpriteComponent(); + await game.ensureAdd(boardBackground); + await tester.pump(); + + game.camera + ..followVector2(Vector2.zero()) + ..zoom = 3.7; + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/board-background.png'), + ); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/boundaries_test.dart b/packages/pinball_components/test/src/components/boundaries_test.dart index 4e2fb497..c119719e 100644 --- a/packages/pinball_components/test/src/components/boundaries_test.dart +++ b/packages/pinball_components/test/src/components/boundaries_test.dart @@ -17,17 +17,24 @@ void main() { Assets.images.boundary.outerBottom.keyName, Assets.images.boundary.bottom.keyName, ]; - final flameTester = FlameTester(TestGame.new); + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('loads correctly', (game) async { + final component = Boundaries(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); flameTester.testGameWidget( 'render correctly', setUp: (game, tester) async { await game.images.loadAll(assets); - await game.addFromBlueprint(Boundaries()); - await game.ready(); + final canvas = ZCanvasComponent(children: [Boundaries()]); + await game.ensureAdd(canvas); game.camera.followVector2(Vector2.zero()); game.camera.zoom = 3.2; + await tester.pump(); }, verify: (game, tester) async { await expectLater( diff --git a/packages/pinball_components/test/src/components/bumping_behavior_test.dart b/packages/pinball_components/test/src/components/bumping_behavior_test.dart new file mode 100644 index 00000000..dd0493f7 --- /dev/null +++ b/packages/pinball_components/test/src/components/bumping_behavior_test.dart @@ -0,0 +1,74 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/src/components/bumping_behavior.dart'; + +import '../../helpers/helpers.dart'; + +class _MockContact extends Mock implements Contact {} + +class _MockContactImpulse extends Mock implements ContactImpulse {} + +class _TestBodyComponent extends BodyComponent { + @override + Body createBody() => world.createBody( + BodyDef(type: BodyType.dynamic), + )..createFixtureFromShape(CircleShape(), 1); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('BumpingBehavior', () { + flameTester.test('can be added', (game) async { + final behavior = BumpingBehavior(strength: 0); + final component = _TestBodyComponent(); + await component.add(behavior); + await game.ensureAdd(component); + }); + + flameTester.testGameWidget( + 'the bump is greater when the strengh is greater', + setUp: (game, tester) async { + final component1 = _TestBodyComponent(); + final behavior1 = BumpingBehavior(strength: 1) + ..worldManifold.normal.setFrom(Vector2.all(1)); + await component1.add(behavior1); + + final component2 = _TestBodyComponent(); + final behavior2 = BumpingBehavior(strength: 2) + ..worldManifold.normal.setFrom(Vector2.all(1)); + await component2.add(behavior2); + + final dummy1 = _TestBodyComponent(); + final dummy2 = _TestBodyComponent(); + + await game.ensureAddAll([ + component1, + component2, + dummy1, + dummy2, + ]); + + final contact = _MockContact(); + final contactImpulse = _MockContactImpulse(); + + behavior1.postSolve(dummy1, contact, contactImpulse); + behavior2.postSolve(dummy2, contact, contactImpulse); + + expect( + dummy2.body.linearVelocity.x, + greaterThan(dummy1.body.linearVelocity.x), + ); + expect( + dummy2.body.linearVelocity.y, + greaterThan(dummy1.body.linearVelocity.y), + ); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/chrome_dino_test.dart b/packages/pinball_components/test/src/components/chrome_dino_test.dart index 8a0adb85..f97270b9 100644 --- a/packages/pinball_components/test/src/components/chrome_dino_test.dart +++ b/packages/pinball_components/test/src/components/chrome_dino_test.dart @@ -1,13 +1,19 @@ // ignore_for_file: cascade_invocations -import 'package:flame_forge2d/flame_forge2d.dart'; +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 '../../helpers/helpers.dart'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(Forge2DGame.new); + final assets = [ + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.animatronic.head.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); group('ChromeDino', () { flameTester.test( @@ -20,19 +26,84 @@ void main() { }, ); - flameTester.test( - 'swivels', - (game) async { - // TODO(alestiago): Write golden tests to check the - // swivel animation. - final chromeDino = ChromeDino(); - await game.ensureAdd(chromeDino); + flameTester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + await game.images.loadAll(assets); + await game.ensureAdd(ChromeDino()); + game.camera.followVector2(Vector2.zero()); + await tester.pump(); + }, + verify: (game, tester) async { + final sweepAnimationDuration = game + .descendants() + .whereType() + .first + .animation! + .totalDuration() / + 2; + + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/up.png'), + ); - final previousPosition = chromeDino.body.position.clone(); - game.update(64); + game.update(sweepAnimationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/middle.png'), + ); - expect(chromeDino.body.position, isNot(equals(previousPosition))); + game.update(sweepAnimationDuration * 0.25); + await tester.pump(); + await expectLater( + find.byGame(), + matchesGoldenFile('golden/chrome_dino/down.png'), + ); }, ); + + group('swivels', () { + flameTester.test( + 'up', + (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + game.camera.followVector2(Vector2.zero()); + + final sweepAnimationDuration = game + .descendants() + .whereType() + .first + .animation! + .totalDuration() / + 2; + game.update(sweepAnimationDuration * 1.5); + + expect(chromeDino.body.angularVelocity, isPositive); + }, + ); + + flameTester.test( + 'down', + (game) async { + final chromeDino = ChromeDino(); + await game.ensureAdd(chromeDino); + game.camera.followVector2(Vector2.zero()); + + final sweepAnimationDuration = game + .descendants() + .whereType() + .first + .animation! + .totalDuration() / + 2; + game.update(sweepAnimationDuration * 0.5); + + expect(chromeDino.body.angularVelocity, isNegative); + }, + ); + }); }); } diff --git a/packages/pinball_components/test/src/components/dash_animatronic_test.dart b/packages/pinball_components/test/src/components/dash_animatronic_test.dart index d0707223..d64c3f07 100644 --- a/packages/pinball_components/test/src/components/dash_animatronic_test.dart +++ b/packages/pinball_components/test/src/components/dash_animatronic_test.dart @@ -45,6 +45,7 @@ void main() { ); }, ); + flameTester.test( 'loads correctly', (game) async { diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart new file mode 100644 index 00000000..bf7513bd --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart @@ -0,0 +1,48 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.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/dash_nest_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'DashNestBumperBallContactBehavior', + () { + test('can be instantiated', () { + expect( + DashNestBumperBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact emits onBallContacted when contacts with a ball', + (game) async { + final behavior = DashNestBumperBallContactBehavior(); + final bloc = MockDashNestBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: DashNestBumperState.active, + ); + + final dashNestBumper = DashNestBumper.test(bloc: bloc); + await dashNestBumper.add(behavior); + await game.ensureAdd(dashNestBumper); + + behavior.beginContact(MockBall(), MockContact()); + + verify(dashNestBumper.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit_test.dart new file mode 100644 index 00000000..7e26bbf3 --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'DashNestBumperCubit', + () { + blocTest( + 'onBallContacted emits active', + build: DashNestBumperCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [DashNestBumperState.active], + ); + + blocTest( + 'onReset emits inactive', + build: DashNestBumperCubit.new, + act: (bloc) => bloc.onReset(), + expect: () => [DashNestBumperState.inactive], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart new file mode 100644 index 00000000..67764951 --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart @@ -0,0 +1,88 @@ +// 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/dash_nest_bumper/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('DashNestBumper', () { + final assets = [ + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.dash.bumper.a.active.keyName, + Assets.images.dash.bumper.a.inactive.keyName, + Assets.images.dash.bumper.b.active.keyName, + Assets.images.dash.bumper.b.inactive.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('"main" loads correctly', (game) async { + final bumper = DashNestBumper.main(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"a" loads correctly', (game) async { + final bumper = DashNestBumper.a(); + await game.ensureAdd(bumper); + + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final bumper = DashNestBumper.b(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = MockDashNestBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: DashNestBumperState.inactive, + ); + when(bloc.close).thenAnswer((_) async {}); + final dashNestBumper = DashNestBumper.test(bloc: bloc); + + await game.ensureAdd(dashNestBumper); + game.remove(dashNestBumper); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('adds new children', (game) async { + final component = Component(); + final dashNestBumper = DashNestBumper.a( + children: [component], + ); + await game.ensureAdd(dashNestBumper); + expect(dashNestBumper.children, contains(component)); + }); + + flameTester.test('a DashNestBumperBallContactBehavior', (game) async { + final dashNestBumper = DashNestBumper.a(); + await game.ensureAdd(dashNestBumper); + expect( + dashNestBumper.children + .whereType() + .single, + isNotNull, + ); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart deleted file mode 100644 index ac036ef4..00000000 --- a/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -// 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 '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('DashNestBumper', () { - final assets = [ - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, - Assets.images.dash.bumper.a.active.keyName, - Assets.images.dash.bumper.a.inactive.keyName, - Assets.images.dash.bumper.b.active.keyName, - Assets.images.dash.bumper.b.inactive.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); - - flameTester.test('"main" loads correctly', (game) async { - final bumper = DashNestBumper.main(); - await game.ensureAdd(bumper); - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('"a" loads correctly', (game) async { - final bumper = DashNestBumper.a(); - await game.ensureAdd(bumper); - - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('"b" loads correctly', (game) async { - final bumper = DashNestBumper.b(); - await game.ensureAdd(bumper); - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('activate switches to active sprite', (game) async { - final bumper = DashNestBumper.main(); - await game.ensureAdd(bumper); - - final spriteGroupComponent = bumper.firstChild()!; - - expect( - spriteGroupComponent.current, - equals(DashNestBumperSpriteState.inactive), - ); - - bumper.activate(); - - expect( - spriteGroupComponent.current, - equals(DashNestBumperSpriteState.active), - ); - }); - - flameTester.test('deactivate switches to inactive sprite', (game) async { - final bumper = DashNestBumper.main(); - await game.ensureAdd(bumper); - - final spriteGroupComponent = bumper.firstChild()! - ..current = DashNestBumperSpriteState.active; - - bumper.deactivate(); - - expect( - spriteGroupComponent.current, - equals(DashNestBumperSpriteState.inactive), - ); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart index b3a58264..a93c6a3c 100644 --- a/packages/pinball_components/test/src/components/dino_walls_test.dart +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -4,22 +4,34 @@ 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'; -import 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; void main() { group('DinoWalls', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('loads correctly', (game) async { + final component = DinoWalls(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - await game.addFromBlueprint(DinoWalls()); + await game.images.loadAll(assets); + await game.ensureAdd(DinoWalls()); + game.camera.followVector2(Vector2.zero()); game.camera.zoom = 6.5; - await game.ready(); + + await tester.pump(); }, verify: (game, tester) async { await expectLater( @@ -28,18 +40,5 @@ void main() { ); }, ); - - flameTester.test( - 'loads correctly', - (game) async { - final dinoWalls = DinoWalls(); - await game.addFromBlueprint(dinoWalls); - await game.ready(); - - for (final wall in dinoWalls.components) { - expect(game.contains(wall), isTrue); - } - }, - ); }); } diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/end.png b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png new file mode 100644 index 00000000..c2a0631a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_spaceship/end.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png new file mode 100644 index 00000000..c6651abd Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_spaceship/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/android_spaceship/start.png b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png new file mode 100644 index 00000000..25e8863a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/android_spaceship/start.png differ diff --git a/packages/pinball_components/test/src/components/golden/board-background.png b/packages/pinball_components/test/src/components/golden/board-background.png new file mode 100644 index 00000000..789c5465 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/board-background.png differ diff --git a/packages/pinball_components/test/src/components/golden/boundaries.png b/packages/pinball_components/test/src/components/golden/boundaries.png index 6cb24bbd..9e9b5633 100644 Binary files a/packages/pinball_components/test/src/components/golden/boundaries.png and b/packages/pinball_components/test/src/components/golden/boundaries.png differ diff --git a/packages/pinball_components/test/src/components/golden/chrome_dino/down.png b/packages/pinball_components/test/src/components/golden/chrome_dino/down.png new file mode 100644 index 00000000..ef91da0a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/chrome_dino/down.png differ diff --git a/packages/pinball_components/test/src/components/golden/chrome_dino/middle.png b/packages/pinball_components/test/src/components/golden/chrome_dino/middle.png new file mode 100644 index 00000000..d4e6286a Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/chrome_dino/middle.png differ diff --git a/packages/pinball_components/test/src/components/golden/chrome_dino/up.png b/packages/pinball_components/test/src/components/golden/chrome_dino/up.png new file mode 100644 index 00000000..042028d1 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/chrome_dino/up.png differ diff --git a/packages/pinball_components/test/src/components/golden/dino-walls.png b/packages/pinball_components/test/src/components/golden/dino-walls.png index 8c2ee569..592c83fb 100644 Binary files a/packages/pinball_components/test/src/components/golden/dino-walls.png and b/packages/pinball_components/test/src/components/golden/dino-walls.png differ diff --git a/packages/pinball_components/test/src/components/golden/kickers.png b/packages/pinball_components/test/src/components/golden/kickers.png index 23176923..1b019de9 100644 Binary files a/packages/pinball_components/test/src/components/golden/kickers.png and b/packages/pinball_components/test/src/components/golden/kickers.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x2-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x2-dimmed.png new file mode 100644 index 00000000..ca2d8bf1 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x2-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x2-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x2-lit.png new file mode 100644 index 00000000..94001e27 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x2-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x3-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x3-dimmed.png new file mode 100644 index 00000000..4727ea3e Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x3-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x3-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x3-lit.png new file mode 100644 index 00000000..f2f84178 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x3-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x4-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x4-dimmed.png new file mode 100644 index 00000000..76c84994 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x4-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x4-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x4-lit.png new file mode 100644 index 00000000..b4918e62 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x4-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x5-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x5-dimmed.png new file mode 100644 index 00000000..2bbbf1ef Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x5-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x5-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x5-lit.png new file mode 100644 index 00000000..5e750af8 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x5-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x6-dimmed.png b/packages/pinball_components/test/src/components/golden/multipliers/x6-dimmed.png new file mode 100644 index 00000000..aff09619 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x6-dimmed.png differ diff --git a/packages/pinball_components/test/src/components/golden/multipliers/x6-lit.png b/packages/pinball_components/test/src/components/golden/multipliers/x6-lit.png new file mode 100644 index 00000000..7e5edc10 Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/multipliers/x6-lit.png differ diff --git a/packages/pinball_components/test/src/components/golden/rocket.png b/packages/pinball_components/test/src/components/golden/rocket.png index 9511f3d5..62ba4e61 100644 Binary files a/packages/pinball_components/test/src/components/golden/rocket.png and b/packages/pinball_components/test/src/components/golden/rocket.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship-rail.png b/packages/pinball_components/test/src/components/golden/spaceship-rail.png index d81f7dba..d8ce5fca 100644 Binary files a/packages/pinball_components/test/src/components/golden/spaceship-rail.png and b/packages/pinball_components/test/src/components/golden/spaceship-rail.png differ diff --git a/packages/pinball_components/test/src/components/golden/spaceship.png b/packages/pinball_components/test/src/components/golden/spaceship.png deleted file mode 100644 index d43db8c7..00000000 Binary files a/packages/pinball_components/test/src/components/golden/spaceship.png and /dev/null differ diff --git a/packages/pinball_components/test/src/components/golden/sparky-computer.png b/packages/pinball_components/test/src/components/golden/sparky-computer.png index 165a79da..1ade03c2 100644 Binary files a/packages/pinball_components/test/src/components/golden/sparky-computer.png and b/packages/pinball_components/test/src/components/golden/sparky-computer.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 new file mode 100644 index 00000000..bf261460 --- /dev/null +++ b/packages/pinball_components/test/src/components/google_letter/behaviors/google_letter_ball_contact_behavior_test.dart @@ -0,0 +1,48 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.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'; + +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.active, + ); + + 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 new file mode 100644 index 00000000..390aa192 --- /dev/null +++ b/packages/pinball_components/test/src/components/google_letter/cubit/google_letter_cubit_test.dart @@ -0,0 +1,24 @@ +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.active], + ); + + blocTest( + 'onReset emits inactive', + build: GoogleLetterCubit.new, + act: (bloc) => bloc.onReset(), + expect: () => [GoogleLetterState.inactive], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/google_letter_test.dart b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart similarity index 58% rename from packages/pinball_components/test/src/components/google_letter_test.dart rename to packages/pinball_components/test/src/components/google_letter/google_letter_test.dart index cdfd3c4a..7ad0e64b 100644 --- a/packages/pinball_components/test/src/components/google_letter_test.dart +++ b/packages/pinball_components/test/src/components/google_letter/google_letter_test.dart @@ -1,11 +1,14 @@ // ignore_for_file: cascade_invocations -import 'package:flame/effects.dart'; +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'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -78,49 +81,50 @@ void main() { }, ); + flameTester.test('adds new children', (game) async { + final component = Component(); + final googleLetter = GoogleLetter( + 1, + children: [component], + ); + await game.ensureAdd(googleLetter); + expect(googleLetter.children, contains(component)); + }); + test('throws error when index out of range', () { expect(() => GoogleLetter(-1), throwsA(isA())); expect(() => GoogleLetter(6), throwsA(isA())); }); - group('activate', () { - flameTester.test('returns normally', (game) async { - final googleLetter = GoogleLetter(0); - await game.ensureAdd(googleLetter); - await expectLater(googleLetter.activate, returnsNormally); - }); - - flameTester.test('adds an Effect', (game) async { - final googleLetter = GoogleLetter(0); - await game.ensureAdd(googleLetter); - await googleLetter.activate(); - await game.ready(); - - expect( - googleLetter.descendants().whereType().length, - equals(1), - ); - }); + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = MockGoogleLetterCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: GoogleLetterState.active, + ); + 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('deactivate', () { - flameTester.test('returns normally', (game) async { - final googleLetter = GoogleLetter(0); - await game.ensureAdd(googleLetter); - await expectLater(googleLetter.deactivate, returnsNormally); - }); - - flameTester.test('adds an Effect', (game) async { - final googleLetter = GoogleLetter(0); - await game.ensureAdd(googleLetter); - await googleLetter.deactivate(); - await game.ready(); - - expect( - googleLetter.descendants().whereType().length, - equals(1), - ); - }); + flameTester.test('adds 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/kicker/behaviors/kicker_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/kicker/behaviors/kicker_ball_contact_behavior_test.dart new file mode 100644 index 00000000..6c04cdcb --- /dev/null +++ b/packages/pinball_components/test/src/components/kicker/behaviors/kicker_ball_contact_behavior_test.dart @@ -0,0 +1,53 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.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/kicker/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockKickerCubit extends Mock implements KickerCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'KickerBallContactBehavior', + () { + test('can be instantiated', () { + expect( + KickerBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact emits onBallContacted when contacts with a ball', + (game) async { + final behavior = KickerBallContactBehavior(); + final bloc = _MockKickerCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: KickerState.lit, + ); + + final kicker = Kicker.test( + side: BoardSide.left, + bloc: bloc, + ); + await kicker.add(behavior); + await game.ensureAdd(kicker); + + behavior.beginContact(MockBall(), MockContact()); + + verify(kicker.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/kicker/behaviors/kicker_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/kicker/behaviors/kicker_blinking_behavior_test.dart new file mode 100644 index 00000000..3b6f0c20 --- /dev/null +++ b/packages/pinball_components/test/src/components/kicker/behaviors/kicker_blinking_behavior_test.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.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/kicker/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockKickerCubit extends Mock implements KickerCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'KickerBlinkingBehavior', + () { + flameTester.testGameWidget( + 'calls onBlinked after 0.05 seconds when dimmed', + setUp: (game, tester) async { + final behavior = KickerBlinkingBehavior(); + final bloc = _MockKickerCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: KickerState.lit, + ); + + final kicker = Kicker.test( + side: BoardSide.left, + bloc: bloc, + ); + await kicker.add(behavior); + await game.ensureAdd(kicker); + + streamController.add(KickerState.dimmed); + await tester.pump(); + game.update(0.05); + + await streamController.close(); + verify(bloc.onBlinked).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/kicker/cubit/kicker_cubit_test.dart b/packages/pinball_components/test/src/components/kicker/cubit/kicker_cubit_test.dart new file mode 100644 index 00000000..ed1d4a46 --- /dev/null +++ b/packages/pinball_components/test/src/components/kicker/cubit/kicker_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'KickerCubit', + () { + blocTest( + 'onBallContacted emits dimmed', + build: KickerCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [KickerState.dimmed], + ); + + blocTest( + 'onBlinked emits lit', + build: KickerCubit.new, + act: (bloc) => bloc.onBlinked(), + expect: () => [KickerState.lit], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/kicker_test.dart b/packages/pinball_components/test/src/components/kicker_test.dart index 8c48a1fb..4d3fc14d 100644 --- a/packages/pinball_components/test/src/components/kicker_test.dart +++ b/packages/pinball_components/test/src/components/kicker_test.dart @@ -1,28 +1,44 @@ // ignore_for_file: cascade_invocations -import 'package:flame_forge2d/flame_forge2d.dart'; +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/bumping_behavior.dart'; +import 'package:pinball_components/src/components/kicker/behaviors/behaviors.dart'; import '../../helpers/helpers.dart'; +class _MockKickerCubit extends Mock implements KickerCubit {} + void main() { group('Kicker', () { - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.kicker.left.lit.keyName, + Assets.images.kicker.left.dimmed.keyName, + Assets.images.kicker.right.lit.keyName, + Assets.images.kicker.right.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { + await game.images.loadAll(assets); final leftKicker = Kicker( side: BoardSide.left, - )..initialPosition = Vector2(-20, 0); + ) + ..initialPosition = Vector2(-20, 0) + ..renderBody = false; final rightKicker = Kicker( side: BoardSide.right, )..initialPosition = Vector2(20, 0); await game.ensureAddAll([leftKicker, rightKicker]); game.camera.followVector2(Vector2.zero()); + await tester.pump(); }, verify: (game, tester) async { await expectLater( @@ -35,8 +51,9 @@ void main() { flameTester.test( 'loads correctly', (game) async { - final kicker = Kicker( + final kicker = Kicker.test( side: BoardSide.left, + bloc: KickerCubit(), ); await game.ensureAdd(kicker); @@ -44,48 +61,72 @@ void main() { }, ); - flameTester.test( - 'body is static', - (game) async { + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = _MockKickerCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: KickerState.lit, + ); + when(bloc.close).thenAnswer((_) async {}); + final kicker = Kicker.test( + side: BoardSide.left, + bloc: bloc, + ); + + await game.ensureAdd(kicker); + game.remove(kicker); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); final kicker = Kicker( side: BoardSide.left, + children: [component], ); await game.ensureAdd(kicker); + expect(kicker.children, contains(component)); + }); - expect(kicker.body.bodyType, equals(BodyType.static)); - }, - ); - - flameTester.test( - 'has restitution', - (game) async { + flameTester.test('a BumpingBehavior', (game) async { final kicker = Kicker( side: BoardSide.left, ); await game.ensureAdd(kicker); - - final totalRestitution = kicker.body.fixtures.fold( - 0, - (total, fixture) => total + fixture.restitution, + expect( + kicker.children.whereType().single, + isNotNull, ); - expect(totalRestitution, greaterThan(0)); - }, - ); + }); - flameTester.test( - 'has no friction', - (game) async { + flameTester.test('a KickerBallContactBehavior', (game) async { final kicker = Kicker( side: BoardSide.left, ); await game.ensureAdd(kicker); + expect( + kicker.children.whereType().single, + isNotNull, + ); + }); - final totalFriction = kicker.body.fixtures.fold( - 0, - (total, fixture) => total + fixture.friction, + flameTester.test('a KickerBlinkingBehavior', (game) async { + final kicker = Kicker( + side: BoardSide.left, ); - expect(totalFriction, equals(0)); - }, - ); + await game.ensureAdd(kicker); + expect( + kicker.children.whereType().single, + isNotNull, + ); + }); + }); }); } diff --git a/packages/pinball_components/test/src/components/launch_ramp_test.dart b/packages/pinball_components/test/src/components/launch_ramp_test.dart index 2defc168..44fa8609 100644 --- a/packages/pinball_components/test/src/components/launch_ramp_test.dart +++ b/packages/pinball_components/test/src/components/launch_ramp_test.dart @@ -4,19 +4,23 @@ 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'; -import 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; void main() { group('LaunchRamp', () { - final tester = FlameTester(TestGame.new); + final flameTester = FlameTester(TestGame.new); - tester.testGameWidget( + flameTester.test('loads correctly', (game) async { + final component = LaunchRamp(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - await game.addFromBlueprint(LaunchRamp()); - await game.ready(); + await game.ensureAdd(LaunchRamp()); game.camera.followVector2(Vector2.zero()); game.camera.zoom = 4.1; }, diff --git a/packages/pinball_components/test/src/components/layer_sensor_test.dart b/packages/pinball_components/test/src/components/layer_sensor_test.dart index f91a6bcb..9103a966 100644 --- a/packages/pinball_components/test/src/components/layer_sensor_test.dart +++ b/packages/pinball_components/test/src/components/layer_sensor_test.dart @@ -10,11 +10,11 @@ import '../../helpers/helpers.dart'; class TestLayerSensor extends LayerSensor { TestLayerSensor({ required LayerEntranceOrientation orientation, - required int insidePriority, + required int insideZIndex, required Layer insideLayer, }) : super( insideLayer: insideLayer, - insidePriority: insidePriority, + insideZIndex: insideZIndex, orientation: orientation, ); @@ -22,11 +22,6 @@ class TestLayerSensor extends LayerSensor { Shape get shape => PolygonShape()..setAsBoxXY(1, 1); } -class TestLayerSensorBallContactCallback - extends LayerSensorBallContactCallback { - TestLayerSensorBallContactCallback() : super(); -} - void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(TestGame.new); @@ -38,7 +33,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: Layer.spaceshipEntranceRamp, ); await game.ensureAdd(layerSensor); @@ -53,7 +48,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: Layer.spaceshipEntranceRamp, ); await game.ensureAdd(layerSensor); @@ -71,7 +66,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: pathwayLayer, )..layer = openingLayer; await game.ensureAdd(layerSensor); @@ -85,7 +80,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: pathwayLayer, )..layer = openingLayer; await game.ensureAdd(layerSensor); @@ -100,7 +95,7 @@ void main() { (game) async { final layerSensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, + insideZIndex: insidePriority, insideLayer: pathwayLayer, )..layer = openingLayer; await game.ensureAdd(layerSensor); @@ -113,69 +108,66 @@ void main() { }); }); - group('LayerSensorBallContactCallback', () { + group('beginContact', () { late Ball ball; late Body body; + late int insideZIndex; + late Layer insideLayer; setUp(() { ball = MockBall(); body = MockBody(); + insideZIndex = 1; + insideLayer = Layer.spaceshipEntranceRamp; when(() => ball.body).thenReturn(body); - when(() => ball.priority).thenReturn(1); when(() => ball.layer).thenReturn(Layer.board); }); flameTester.test( - 'changes ball layer and priority ' + 'changes ball layer and zIndex ' 'when a ball enters and exits a downward oriented LayerSensor', (game) async { final sensor = TestLayerSensor( orientation: LayerEntranceOrientation.down, - insidePriority: insidePriority, - insideLayer: Layer.spaceshipEntranceRamp, + insideZIndex: insidePriority, + insideLayer: insideLayer, )..initialPosition = Vector2(0, 10); - final callback = TestLayerSensorBallContactCallback(); when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); - callback.begin(ball, sensor, MockContact()); - verify(() => ball.layer = sensor.insideLayer).called(1); - verify(() => ball.priority = sensor.insidePriority).called(1); - verify(ball.reorderChildren).called(1); + sensor.beginContact(ball, MockContact()); + verify(() => ball.layer = insideLayer).called(1); + verify(() => ball.zIndex = insideZIndex).called(1); - when(() => ball.layer).thenReturn(sensor.insideLayer); + when(() => ball.layer).thenReturn(insideLayer); - callback.begin(ball, sensor, MockContact()); + sensor.beginContact(ball, MockContact()); verify(() => ball.layer = Layer.board); - verify(() => ball.priority = RenderPriority.ballOnBoard).called(1); - verify(ball.reorderChildren).called(1); + verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); }); flameTester.test( - 'changes ball layer and priority ' + 'changes ball layer and zIndex ' 'when a ball enters and exits an upward oriented LayerSensor', (game) async { final sensor = TestLayerSensor( orientation: LayerEntranceOrientation.up, - insidePriority: insidePriority, - insideLayer: Layer.spaceshipEntranceRamp, + insideZIndex: insidePriority, + insideLayer: insideLayer, )..initialPosition = Vector2(0, 10); - final callback = TestLayerSensorBallContactCallback(); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - callback.begin(ball, sensor, MockContact()); - verify(() => ball.layer = sensor.insideLayer).called(1); - verify(() => ball.priority = sensor.insidePriority).called(1); - verify(ball.reorderChildren).called(1); + sensor.beginContact(ball, MockContact()); + verify(() => ball.layer = insideLayer).called(1); + verify(() => ball.zIndex = insidePriority).called(1); - when(() => ball.layer).thenReturn(sensor.insideLayer); + when(() => ball.layer).thenReturn(insideLayer); - callback.begin(ball, sensor, MockContact()); + sensor.beginContact(ball, MockContact()); verify(() => ball.layer = Layer.board); - verify(() => ball.priority = RenderPriority.ballOnBoard).called(1); - verify(ball.reorderChildren).called(1); + verify(() => ball.zIndex = ZIndexes.ballOnBoard).called(1); }); }); } diff --git a/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_cubit_test.dart b/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_cubit_test.dart new file mode 100644 index 00000000..35ed652e --- /dev/null +++ b/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_cubit_test.dart @@ -0,0 +1,118 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'MultiplierCubit', + () { + blocTest( + "emits [lit] when 'next' on x2 dimmed with x2 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x2), + act: (bloc) => bloc.next(2), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [lit] when 'next' on x3 dimmed with x3 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x3), + act: (bloc) => bloc.next(3), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [lit] when 'next' on x4 dimmed with x4 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x4), + act: (bloc) => bloc.next(4), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [lit] when 'next' on x5 dimmed with x5 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x5), + act: (bloc) => bloc.next(5), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [lit] when 'next' on x6 dimmed with x6 multiplier value", + build: () => MultiplierCubit(MultiplierValue.x6), + act: (bloc) => bloc.next(6), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.lit, + ), + ], + ); + + blocTest( + "emits [dimmed] when 'next' on lit with different multiplier value", + build: () => MultiplierCubit(MultiplierValue.x2), + seed: () => MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + act: (bloc) => bloc.next(3), + expect: () => [ + isA() + ..having( + (state) => state.spriteState, + 'spriteState', + MultiplierSpriteState.dimmed, + ), + ], + ); + + blocTest( + "emits nothing when 'next' on lit with same multiplier value", + build: () => MultiplierCubit(MultiplierValue.x2), + seed: () => MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + act: (bloc) => bloc.next(2), + expect: () => [], + ); + + blocTest( + "emits nothing when 'next' on dimmed with different multiplier value", + build: () => MultiplierCubit(MultiplierValue.x2), + act: (bloc) => bloc.next(3), + expect: () => [], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_state_test.dart b/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_state_test.dart new file mode 100644 index 00000000..9789d7c5 --- /dev/null +++ b/packages/pinball_components/test/src/components/multiplier/cubit/multiplier_state_test.dart @@ -0,0 +1,75 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/pinball_components.dart'; + +void main() { + group('MultiplierState', () { + test('supports value equality', () { + expect( + MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + equals( + MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ), + isNotNull, + ); + }); + }); + + group('copyWith', () { + test( + 'copies correctly ' + 'when no argument specified', + () { + const multiplierState = MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ); + expect( + multiplierState.copyWith(), + equals(multiplierState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + const multiplierState = MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.lit, + ); + final otherMultiplierState = MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.dimmed, + ); + expect(multiplierState, isNot(equals(otherMultiplierState))); + + expect( + multiplierState.copyWith( + spriteState: MultiplierSpriteState.dimmed, + ), + equals(otherMultiplierState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart b/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart new file mode 100644 index 00000000..edc2735f --- /dev/null +++ b/packages/pinball_components/test/src/components/multiplier/multiplier_test.dart @@ -0,0 +1,517 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +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 '../../../helpers/helpers.dart'; + +void main() { + final bloc = MockMultiplierCubit(); + + group('Multiplier', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('"x2" loads correctly', (game) async { + final multiplier = Multiplier.x2( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + flameTester.test('"x3" loads correctly', (game) async { + final multiplier = Multiplier.x3( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + flameTester.test('"x4" loads correctly', (game) async { + final multiplier = Multiplier.x4( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + flameTester.test('"x5" loads correctly', (game) async { + final multiplier = Multiplier.x5( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + flameTester.test('"x6" loads correctly', (game) async { + final multiplier = Multiplier.x6( + position: Vector2.zero(), + angle: 0, + ); + await game.ensureAdd(multiplier); + expect(game.contains(multiplier), isTrue); + }); + + group('renders correctly', () { + group('x2', () { + const multiplierValue = MultiplierValue.x2; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x2-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x2-dimmed.png'), + ); + }, + ); + }); + + group('x3', () { + const multiplierValue = MultiplierValue.x3; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x3-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x3-dimmed.png'), + ); + }, + ); + }); + + group('x4', () { + const multiplierValue = MultiplierValue.x4; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x4-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x4-dimmed.png'), + ); + }, + ); + }); + + group('x5', () { + const multiplierValue = MultiplierValue.x5; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x5-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x5-dimmed.png'), + ); + }, + ); + }); + + group('x6', () { + const multiplierValue = MultiplierValue.x6; + + flameTester.testGameWidget( + 'lit when bloc state is lit', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.lit, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.lit, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x6-lit.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'dimmed when bloc state is dimmed', + setUp: (game, tester) async { + await game.images.loadAll(assets); + + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: multiplierValue, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + + final multiplier = Multiplier.test( + value: multiplierValue, + bloc: bloc, + ); + await game.ensureAdd(multiplier); + await tester.pump(); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + expect( + game + .descendants() + .whereType() + .first + .current, + MultiplierSpriteState.dimmed, + ); + + await expectLater( + find.byGame(), + matchesGoldenFile('../golden/multipliers/x6-dimmed.png'), + ); + }, + ); + }); + }); + + flameTester.test('closes bloc when removed', (game) async { + whenListen( + bloc, + const Stream.empty(), + initialState: MultiplierState( + value: MultiplierValue.x2, + spriteState: MultiplierSpriteState.dimmed, + ), + ); + when(bloc.close).thenAnswer((_) async {}); + final multiplier = Multiplier.test(value: MultiplierValue.x2, bloc: bloc); + + await game.ensureAdd(multiplier); + game.remove(multiplier); + await game.ready(); + + verify(bloc.close).called(1); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/rocket_test.dart b/packages/pinball_components/test/src/components/rocket_test.dart index 87cfe515..5bc9b136 100644 --- a/packages/pinball_components/test/src/components/rocket_test.dart +++ b/packages/pinball_components/test/src/components/rocket_test.dart @@ -8,14 +8,24 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; void main() { - group('RocketSpriteComponent', () { - final tester = FlameTester(TestGame.new); + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.plunger.rocket.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); - tester.testGameWidget( + group('RocketSpriteComponent', () { + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - game.camera.followVector2(Vector2.zero()); + await game.images.loadAll(assets); await game.ensureAdd(RocketSpriteComponent()); + + game.camera + ..followVector2(Vector2.zero()) + ..zoom = 8; + + await tester.pump(); }, verify: (game, tester) async { await expectLater( diff --git a/packages/pinball_components/test/src/components/signpost_test.dart b/packages/pinball_components/test/src/components/signpost_test.dart index 018c1bee..23aa6bd0 100644 --- a/packages/pinball_components/test/src/components/signpost_test.dart +++ b/packages/pinball_components/test/src/components/signpost_test.dart @@ -151,5 +151,14 @@ void main() { expect(spriteComponent.current, SignpostSpriteState.inactive); }, ); + + flameTester.test('adds new children', (game) async { + final component = Component(); + final signpost = Signpost( + children: [component], + ); + await game.ensureAdd(signpost); + expect(signpost.children, contains(component)); + }); }); } diff --git a/packages/pinball_components/test/src/components/slingshot_test.dart b/packages/pinball_components/test/src/components/slingshot_test.dart index 69296f78..0c7a29e0 100644 --- a/packages/pinball_components/test/src/components/slingshot_test.dart +++ b/packages/pinball_components/test/src/components/slingshot_test.dart @@ -4,7 +4,6 @@ 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'; -import 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; @@ -18,11 +17,17 @@ void main() { const length = 2.0; const angle = 0.0; + flameTester.test('loads correctly', (game) async { + final component = Slingshots(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { await game.images.loadAll(assets); - await game.addFromBlueprint(Slingshots()); + await game.ensureAdd(Slingshots()); game.camera.followVector2(Vector2.zero()); await game.ready(); await tester.pump(); diff --git a/packages/pinball_components/test/src/components/spaceship_rail_test.dart b/packages/pinball_components/test/src/components/spaceship_rail_test.dart index d3242ff6..65e9dbd7 100644 --- a/packages/pinball_components/test/src/components/spaceship_rail_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_rail_test.dart @@ -4,20 +4,30 @@ 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'; -import 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; void main() { group('SpaceshipRail', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + flameTester.test('loads correctly', (game) async { + final component = SpaceshipRail(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - await game.addFromBlueprint(SpaceshipRail()); - await game.ready(); + await game.images.loadAll(assets); + await game.ensureAdd(SpaceshipRail()); + await tester.pump(); game.camera.followVector2(Vector2.zero()); game.camera.zoom = 8; @@ -29,18 +39,5 @@ void main() { ); }, ); - - flameTester.test( - 'loads correctly', - (game) async { - final spaceshipRail = SpaceshipRail(); - await game.addFromBlueprint(spaceshipRail); - await game.ready(); - - for (final element in spaceshipRail.components) { - expect(game.contains(element), isTrue); - } - }, - ); }); } diff --git a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart index a65ba18b..0f2ce13a 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart @@ -11,32 +11,25 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.spaceship.ramp.boardOpening.keyName, - Assets.images.spaceship.ramp.railingForeground.keyName, - Assets.images.spaceship.ramp.railingBackground.keyName, - Assets.images.spaceship.ramp.main.keyName, - Assets.images.spaceship.ramp.arrow.inactive.keyName, - Assets.images.spaceship.ramp.arrow.active1.keyName, - Assets.images.spaceship.ramp.arrow.active2.keyName, - Assets.images.spaceship.ramp.arrow.active3.keyName, - Assets.images.spaceship.ramp.arrow.active4.keyName, - Assets.images.spaceship.ramp.arrow.active5.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); group('SpaceshipRamp', () { - flameTester.test( - 'loads correctly', - (game) async { - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - - for (final component in spaceshipRamp.components) { - expect(game.contains(component), isTrue); - } - }, - ); + flameTester.test('loads correctly', (game) async { + final component = SpaceshipRamp(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); group('renders correctly', () { const goldenFilePath = 'golden/spaceship_ramp/'; @@ -46,16 +39,14 @@ void main() { 'inactive sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.inactive, ); @@ -73,17 +64,15 @@ void main() { 'active1 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp.progress(); + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component.progress(); await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active1, ); @@ -101,19 +90,17 @@ void main() { 'active2 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component ..progress() ..progress(); await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active2, ); @@ -131,20 +118,18 @@ void main() { 'active3 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component ..progress() ..progress() ..progress(); await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active3, ); @@ -162,10 +147,11 @@ void main() { 'active4 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component ..progress() ..progress() ..progress() @@ -173,10 +159,7 @@ void main() { await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active4, ); @@ -194,10 +177,11 @@ void main() { 'active5 sprite', setUp: (game, tester) async { await game.images.loadAll(assets); - final spaceshipRamp = SpaceshipRamp(); - await game.addFromBlueprint(spaceshipRamp); - await game.ready(); - spaceshipRamp + final component = SpaceshipRamp(); + final canvas = ZCanvasComponent(children: [component]); + await game.ensureAdd(canvas); + + component ..progress() ..progress() ..progress() @@ -206,10 +190,7 @@ void main() { await tester.pump(); expect( - spaceshipRamp.components - .whereType() - .first - .current, + component.children.whereType().first.current, SpaceshipRampArrowSpriteState.active5, ); diff --git a/packages/pinball_components/test/src/components/spaceship_test.dart b/packages/pinball_components/test/src/components/spaceship_test.dart deleted file mode 100644 index c9a90746..00000000 --- a/packages/pinball_components/test/src/components/spaceship_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('Spaceship', () { - late Filter filterData; - late Fixture fixture; - late Body body; - late Ball ball; - late Forge2DGame game; - - setUp(() { - filterData = MockFilter(); - - fixture = MockFixture(); - when(() => fixture.filterData).thenReturn(filterData); - - body = MockBody(); - when(() => body.fixtures).thenReturn([fixture]); - - game = MockGame(); - - ball = MockBall(); - when(() => ball.gameRef).thenReturn(game); - when(() => ball.body).thenReturn(body); - }); - - group('Spaceship', () { - final tester = FlameTester(TestGame.new); - - tester.testGameWidget( - 'renders correctly', - setUp: (game, tester) async { - final position = Vector2(30, -30); - await game.addFromBlueprint(Spaceship(position: position)); - game.camera.followVector2(position); - await game.ready(); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile('golden/spaceship.png'), - ); - }, - ); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart new file mode 100644 index 00000000..f67b28d7 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_ball_contact_behavior_test.dart @@ -0,0 +1,48 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.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/sparky_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'SparkyBumperBallContactBehavior', + () { + test('can be instantiated', () { + expect( + SparkyBumperBallContactBehavior(), + isA(), + ); + }); + + flameTester.test( + 'beginContact emits onBallContacted when contacts with a ball', + (game) async { + final behavior = SparkyBumperBallContactBehavior(); + final bloc = MockSparkyBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SparkyBumperState.lit, + ); + + final sparkyBumper = SparkyBumper.test(bloc: bloc); + await sparkyBumper.add(behavior); + await game.ensureAdd(sparkyBumper); + + behavior.beginContact(MockBall(), MockContact()); + + verify(sparkyBumper.bloc.onBallContacted).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart new file mode 100644 index 00000000..2210754f --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper/behaviors/sparky_bumper_blinking_behavior_test.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.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/sparky_bumper/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group( + 'SparkyBumperBlinkingBehavior', + () { + flameTester.testGameWidget( + 'calls onBlinked after 0.05 seconds when dimmed', + setUp: (game, tester) async { + final behavior = SparkyBumperBlinkingBehavior(); + final bloc = MockSparkyBumperCubit(); + final streamController = StreamController(); + whenListen( + bloc, + streamController.stream, + initialState: SparkyBumperState.lit, + ); + + final sparkyBumper = SparkyBumper.test(bloc: bloc); + await sparkyBumper.add(behavior); + await game.ensureAdd(sparkyBumper); + + streamController.add(SparkyBumperState.dimmed); + await tester.pump(); + game.update(0.05); + + await streamController.close(); + verify(bloc.onBlinked).called(1); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart new file mode 100644 index 00000000..6310dca2 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper/cubit/sparky_bumper_cubit_test.dart @@ -0,0 +1,24 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group( + 'SparkyBumperCubit', + () { + blocTest( + 'onBallContacted emits dimmed', + build: SparkyBumperCubit.new, + act: (bloc) => bloc.onBallContacted(), + expect: () => [SparkyBumperState.dimmed], + ); + + blocTest( + 'onBlinked emits lit', + build: SparkyBumperCubit.new, + act: (bloc) => bloc.onBlinked(), + expect: () => [SparkyBumperState.lit], + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart b/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart new file mode 100644 index 00000000..0d255454 --- /dev/null +++ b/packages/pinball_components/test/src/components/sparky_bumper/sparky_bumper_test.dart @@ -0,0 +1,86 @@ +// 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/sparky_bumper/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); + + group('SparkyBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final sparkyBumper = SparkyBumper.a(); + await game.ensureAdd(sparkyBumper); + expect(game.contains(sparkyBumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final sparkyBumper = SparkyBumper.b(); + await game.ensureAdd(sparkyBumper); + expect(game.contains(sparkyBumper), isTrue); + }); + + flameTester.test('"c" loads correctly', (game) async { + final sparkyBumper = SparkyBumper.c(); + await game.ensureAdd(sparkyBumper); + expect(game.contains(sparkyBumper), isTrue); + }); + + // TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = MockSparkyBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SparkyBumperState.lit, + ); + when(bloc.close).thenAnswer((_) async {}); + final sparkyBumper = SparkyBumper.test(bloc: bloc); + + await game.ensureAdd(sparkyBumper); + game.remove(sparkyBumper); + await game.ready(); + + verify(bloc.close).called(1); + }); + + group('adds', () { + flameTester.test('new children', (game) async { + final component = Component(); + final sparkyBumper = SparkyBumper.a( + children: [component], + ); + await game.ensureAdd(sparkyBumper); + expect(sparkyBumper.children, contains(component)); + }); + + flameTester.test('a SparkyBumperBallContactBehavior', (game) async { + final sparkyBumper = SparkyBumper.a(); + await game.ensureAdd(sparkyBumper); + expect( + sparkyBumper.children + .whereType() + .single, + isNotNull, + ); + }); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/sparky_bumper_test.dart b/packages/pinball_components/test/src/components/sparky_bumper_test.dart deleted file mode 100644 index a2fcc5ed..00000000 --- a/packages/pinball_components/test/src/components/sparky_bumper_test.dart +++ /dev/null @@ -1,69 +0,0 @@ -// 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 '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); - - group('SparkyBumper', () { - flameTester.test('"a" loads correctly', (game) async { - final bumper = SparkyBumper.a(); - await game.ensureAdd(bumper); - - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('"b" loads correctly', (game) async { - final bumper = SparkyBumper.b(); - await game.ensureAdd(bumper); - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('"c" loads correctly', (game) async { - final bumper = SparkyBumper.c(); - await game.ensureAdd(bumper); - expect(game.contains(bumper), isTrue); - }); - - flameTester.test('animate switches between on and off sprites', - (game) async { - final bumper = SparkyBumper.a(); - await game.ensureAdd(bumper); - - final spriteGroupComponent = bumper.firstChild()!; - - expect( - spriteGroupComponent.current, - equals(SparkyBumperSpriteState.active), - ); - - final future = bumper.animate(); - - expect( - spriteGroupComponent.current, - equals(SparkyBumperSpriteState.inactive), - ); - - await future; - - expect( - spriteGroupComponent.current, - equals(SparkyBumperSpriteState.active), - ); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/sparky_computer_test.dart b/packages/pinball_components/test/src/components/sparky_computer_test.dart index 6b19481e..ffba79b6 100644 --- a/packages/pinball_components/test/src/components/sparky_computer_test.dart +++ b/packages/pinball_components/test/src/components/sparky_computer_test.dart @@ -4,21 +4,35 @@ 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'; -import 'package:pinball_flame/pinball_flame.dart'; import '../../helpers/helpers.dart'; void main() { group('SparkyComputer', () { - final tester = FlameTester(TestGame.new); + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.glow.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); - tester.testGameWidget( + flameTester.test('loads correctly', (game) async { + final component = SparkyComputer(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { - await game.addFromBlueprint(SparkyComputer()); - await game.ready(); + await game.images.loadAll(assets); + await game.ensureAdd(SparkyComputer()); + await tester.pump(); - game.camera.followVector2(Vector2(-15, -50)); + game.camera + ..followVector2(Vector2(0, -20)) + ..zoom = 7; }, verify: (game, tester) async { await expectLater( diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart index 709e7627..66d34b14 100644 --- a/packages/pinball_flame/lib/pinball_flame.dart +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -1,6 +1,8 @@ library pinball_flame; -export 'src/blueprint.dart'; export 'src/component_controller.dart'; +export 'src/contact_behavior.dart'; export 'src/keyboard_input_controller.dart'; +export 'src/parent_is_a.dart'; export 'src/sprite_animation.dart'; +export 'src/z_canvas_component.dart'; diff --git a/packages/pinball_flame/lib/src/blueprint.dart b/packages/pinball_flame/lib/src/blueprint.dart deleted file mode 100644 index c7bd5a5e..00000000 --- a/packages/pinball_flame/lib/src/blueprint.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; - -// TODO(erickzanardo): Keeping this inside our code base so we can experiment -// with the idea, but this is a potential upstream change on Flame. - -/// {@template blueprint} -/// A [Blueprint] is a virtual way of grouping [Component]s that are related. -/// {@endtemplate blueprint} -class Blueprint { - /// {@macro blueprint} - Blueprint({ - Iterable? components, - Iterable? blueprints, - }) { - if (components != null) _components.addAll(components); - if (blueprints != null) { - _blueprints.addAll(blueprints); - for (final blueprint in blueprints) { - _components.addAll(blueprint.components); - } - } - } - - final List _components = []; - - final List _blueprints = []; - - Future _addToParent(Component parent) async { - await parent.addAll(_components); - } - - /// Returns a copy of the components built by this blueprint. - List get components => List.unmodifiable(_components); - - /// Returns a copy of the blueprints built by this blueprint. - List get blueprints => List.unmodifiable(_blueprints); -} - -/// Adds helper methods regarding [Blueprint]s to [FlameGame]. -extension FlameGameBlueprint on Component { - /// Shortcut to add a [Blueprint]s components to its parent. - Future addFromBlueprint(Blueprint blueprint) async { - await blueprint._addToParent(this); - } -} diff --git a/packages/pinball_flame/lib/src/contact_behavior.dart b/packages/pinball_flame/lib/src/contact_behavior.dart new file mode 100644 index 00000000..ff715b12 --- /dev/null +++ b/packages/pinball_flame/lib/src/contact_behavior.dart @@ -0,0 +1,105 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Appends a new [ContactCallbacks] to the parent. +/// +/// This is a convenience class for adding a [ContactCallbacks] to the parent. +/// In contrast with just assigning a [ContactCallbacks] to a userData, this +/// class respects the previous userData. +/// +/// It does so by grouping the userData in a [_UserData], and resetting the +/// parent's userData accordingly. +// TODO(alestiago): Make use of generics to infer the type of the contact. +// https://github.com/VGVentures/pinball/pull/234#discussion_r859182267 +class ContactBehavior extends Component + with ContactCallbacks, ParentIsA { + final _fixturesUserData = {}; + + /// Specifies which fixtures should be considered for contact. + /// + /// Fixtures are identifiable by their userData. + /// + /// If no fixtures are specified, the [ContactCallbacks] is applied to the + /// entire body, hence all fixtures are considered. + void applyTo(Iterable userData) => _fixturesUserData.addAll(userData); + + @override + Future onLoad() async { + if (_fixturesUserData.isNotEmpty) { + for (final fixture in _targetedFixtures) { + fixture.userData = _UserData.fromFixture(fixture)..add(this); + } + } else { + parent.body.userData = _UserData.fromBody(parent.body)..add(this); + } + } + + Iterable get _targetedFixtures => + parent.body.fixtures.where((fixture) { + if (_fixturesUserData.contains(fixture.userData)) return true; + + final userData = fixture.userData; + if (userData is _UserData) { + return _fixturesUserData.contains(userData.value); + } + + return false; + }); +} + +class _UserData with ContactCallbacks { + _UserData._(Object? userData) : _userData = [userData]; + + factory _UserData._fromUserData(Object? userData) { + if (userData is _UserData) return userData; + return _UserData._(userData); + } + + factory _UserData.fromFixture(Fixture fixture) => + _UserData._fromUserData(fixture.userData); + + factory _UserData.fromBody(Body body) => + _UserData._fromUserData(body.userData); + + final List _userData; + + Iterable get _contactCallbacks => + _userData.whereType(); + + Object? get value => _userData.first; + + void add(Object? userData) => _userData.add(userData); + + @override + void beginContact(Object other, Contact contact) { + super.beginContact(other, contact); + for (final callback in _contactCallbacks) { + callback.beginContact(other, contact); + } + } + + @override + void endContact(Object other, Contact contact) { + super.endContact(other, contact); + for (final callback in _contactCallbacks) { + callback.endContact(other, contact); + } + } + + @override + void preSolve(Object other, Contact contact, Manifold oldManifold) { + super.preSolve(other, contact, oldManifold); + for (final callback in _contactCallbacks) { + callback.preSolve(other, contact, oldManifold); + } + } + + @override + void postSolve(Object other, Contact contact, ContactImpulse impulse) { + super.postSolve(other, contact, impulse); + for (final callback in _contactCallbacks) { + callback.postSolve(other, contact, impulse); + } + } +} diff --git a/packages/pinball_flame/lib/src/parent_is_a.dart b/packages/pinball_flame/lib/src/parent_is_a.dart new file mode 100644 index 00000000..19159c89 --- /dev/null +++ b/packages/pinball_flame/lib/src/parent_is_a.dart @@ -0,0 +1,15 @@ +import 'package:flame/components.dart'; + +// TODO(alestiago): Remove once the following is merged: +// https://github.com/flame-engine/flame/pull/1566 + +/// A mixin that ensures a parent is of the given type [T]. +mixin ParentIsA on Component { + @override + T get parent => super.parent! as T; + + @override + Future? addToParent(covariant T parent) { + return super.addToParent(parent); + } +} diff --git a/packages/pinball_flame/lib/src/z_canvas_component.dart b/packages/pinball_flame/lib/src/z_canvas_component.dart new file mode 100644 index 00000000..911c3e93 --- /dev/null +++ b/packages/pinball_flame/lib/src/z_canvas_component.dart @@ -0,0 +1,249 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flame/components.dart'; + +/// {@template z_canvas_component} +/// Draws [ZIndex] components after the all non-[ZIndex] components have been +/// drawn. +/// {@endtemplate} +class ZCanvasComponent extends Component { + /// {@macro z_canvas_component} + ZCanvasComponent({ + Iterable? children, + }) : _zCanvas = ZCanvas(), + super(children: children); + + final ZCanvas _zCanvas; + + @override + void renderTree(Canvas canvas) { + _zCanvas.canvas = canvas; + super.renderTree(_zCanvas); + _zCanvas.render(); + } +} + +/// Apply to any [Component] that will be rendered according to a +/// [ZIndex.zIndex]. +/// +/// [ZIndex] components must be descendants of a [ZCanvasComponent]. +/// +/// {@macro z_canvas.render} +mixin ZIndex on Component { + /// The z-index of this component. + /// + /// The higher the value, the later the component will be drawn. Hence, + /// rendering in front of [Component]s with lower [zIndex] values. + int zIndex = 0; + + @override + void renderTree( + Canvas canvas, + ) { + if (canvas is ZCanvas) { + canvas.buffer(this); + } else { + super.renderTree(canvas); + } + } +} + +/// The [ZCanvas] allows to postpone the rendering of [ZIndex] components. +/// +/// You should not use this class directly. +class ZCanvas implements Canvas { + /// The [Canvas] to render to. + /// + /// This is set by [ZCanvasComponent] when rendering. + late Canvas canvas; + + final List _zBuffer = []; + + /// Postpones the rendering of [ZIndex] component and its children. + void buffer(ZIndex component) => _zBuffer.add(component); + + /// Renders all [ZIndex] components and their children. + /// + /// {@template z_canvas.render} + /// The rendering order is defined by the parent [ZIndex]. The children of + /// the same parent are rendered in the order they were added. + /// + /// If two [Component]s ever overlap each other, and have the same + /// [ZIndex.zIndex], there is no guarantee that the first one will be rendered + /// before the second one. + /// {@endtemplate} + void render() => _zBuffer + ..sort((a, b) => a.zIndex.compareTo(b.zIndex)) + ..whereType().forEach(_render) + ..clear(); + + void _render(Component component) => component.renderTree(canvas); + + @override + void clipPath(Path path, {bool doAntiAlias = true}) => + canvas.clipPath(path, doAntiAlias: doAntiAlias); + + @override + void clipRRect(RRect rrect, {bool doAntiAlias = true}) => + canvas.clipRRect(rrect, doAntiAlias: doAntiAlias); + + @override + void clipRect( + Rect rect, { + ClipOp clipOp = ClipOp.intersect, + bool doAntiAlias = true, + }) => + canvas.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias); + + @override + void drawArc( + Rect rect, + double startAngle, + double sweepAngle, + bool useCenter, + Paint paint, + ) => + canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint); + + @override + void drawAtlas( + Image atlas, + List transforms, + List rects, + List? colors, + BlendMode? blendMode, + Rect? cullRect, + Paint paint, + ) => + canvas.drawAtlas( + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ); + + @override + void drawCircle(Offset c, double radius, Paint paint) => canvas.drawCircle( + c, + radius, + paint, + ); + + @override + void drawColor(Color color, BlendMode blendMode) => + canvas.drawColor(color, blendMode); + + @override + void drawDRRect(RRect outer, RRect inner, Paint paint) => + canvas.drawDRRect(outer, inner, paint); + + @override + void drawImage(Image image, Offset offset, Paint paint) => + canvas.drawImage(image, offset, paint); + + @override + void drawImageNine(Image image, Rect center, Rect dst, Paint paint) => + canvas.drawImageNine(image, center, dst, paint); + + @override + void drawImageRect(Image image, Rect src, Rect dst, Paint paint) => + canvas.drawImageRect(image, src, dst, paint); + + @override + void drawLine(Offset p1, Offset p2, Paint paint) => + canvas.drawLine(p1, p2, paint); + + @override + void drawOval(Rect rect, Paint paint) => canvas.drawOval(rect, paint); + + @override + void drawPaint(Paint paint) => canvas.drawPaint(paint); + + @override + void drawParagraph(Paragraph paragraph, Offset offset) => + canvas.drawParagraph(paragraph, offset); + + @override + void drawPath(Path path, Paint paint) => canvas.drawPath(path, paint); + + @override + void drawPicture(Picture picture) => canvas.drawPicture(picture); + + @override + void drawPoints(PointMode pointMode, List points, Paint paint) => + canvas.drawPoints(pointMode, points, paint); + + @override + void drawRRect(RRect rrect, Paint paint) => canvas.drawRRect(rrect, paint); + + @override + void drawRawAtlas( + Image atlas, + Float32List rstTransforms, + Float32List rects, + Int32List? colors, + BlendMode? blendMode, + Rect? cullRect, + Paint paint, + ) => + canvas.drawRawAtlas( + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ); + + @override + void drawRawPoints(PointMode pointMode, Float32List points, Paint paint) => + canvas.drawRawPoints(pointMode, points, paint); + + @override + void drawRect(Rect rect, Paint paint) => canvas.drawRect(rect, paint); + + @override + void drawShadow( + Path path, + Color color, + double elevation, + bool transparentOccluder, + ) => + canvas.drawShadow(path, color, elevation, transparentOccluder); + + @override + void drawVertices(Vertices vertices, BlendMode blendMode, Paint paint) => + canvas.drawVertices(vertices, blendMode, paint); + + @override + int getSaveCount() => canvas.getSaveCount(); + + @override + void restore() => canvas.restore(); + + @override + void rotate(double radians) => canvas.rotate(radians); + + @override + void save() => canvas.save(); + + @override + void saveLayer(Rect? bounds, Paint paint) => canvas.saveLayer(bounds, paint); + + @override + void scale(double sx, [double? sy]) => canvas.scale(sx, sy); + + @override + void skew(double sx, double sy) => canvas.skew(sx, sy); + + @override + void transform(Float64List matrix4) => canvas.transform(matrix4); + + @override + void translate(double dx, double dy) => canvas.translate(dx, dy); +} diff --git a/packages/pinball_flame/pubspec.yaml b/packages/pinball_flame/pubspec.yaml index ad8ec131..89caf5bb 100644 --- a/packages/pinball_flame/pubspec.yaml +++ b/packages/pinball_flame/pubspec.yaml @@ -8,7 +8,11 @@ environment: dependencies: flame: ^1.1.1 - flame_forge2d: ^0.11.0 + flame_forge2d: + git: + url: https://github.com/flame-engine/flame/ + path: packages/flame_forge2d/ + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f flutter: sdk: flutter diff --git a/packages/pinball_flame/test/helpers/mocks.dart b/packages/pinball_flame/test/helpers/mocks.dart index bf96390d..1c5042ff 100644 --- a/packages/pinball_flame/test/helpers/mocks.dart +++ b/packages/pinball_flame/test/helpers/mocks.dart @@ -4,7 +4,4 @@ import 'package:mocktail/mocktail.dart'; class MockForge2DGame extends Mock implements Forge2DGame {} -class MockContactCallback extends Mock - implements ContactCallback {} - class MockComponent extends Mock implements Component {} diff --git a/packages/pinball_flame/test/src/blueprint_test.dart b/packages/pinball_flame/test/src/blueprint_test.dart deleted file mode 100644 index 402d5059..00000000 --- a/packages/pinball_flame/test/src/blueprint_test.dart +++ /dev/null @@ -1,86 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('Blueprint', () { - final flameTester = FlameTester(FlameGame.new); - - test('correctly sets and gets components', () { - final component1 = Component(); - final component2 = Component(); - final blueprint = Blueprint( - components: [ - component1, - component2, - ], - ); - - expect(blueprint.components.length, 2); - expect(blueprint.components, contains(component1)); - expect(blueprint.components, contains(component2)); - }); - - test('correctly sets and gets blueprints', () { - final blueprint2 = Blueprint( - components: [Component()], - ); - final blueprint1 = Blueprint( - components: [Component()], - blueprints: [blueprint2], - ); - - expect(blueprint1.blueprints, contains(blueprint2)); - }); - - flameTester.test('adds the components to parent on attach', (game) async { - final blueprint = Blueprint( - components: [ - Component(), - Component(), - ], - ); - await game.addFromBlueprint(blueprint); - await game.ready(); - - for (final component in blueprint.components) { - expect(game.children.contains(component), isTrue); - } - }); - - flameTester.test('adds components from a child Blueprint', (game) async { - final childBlueprint = Blueprint( - components: [ - Component(), - Component(), - ], - ); - final parentBlueprint = Blueprint( - components: [ - Component(), - Component(), - ], - blueprints: [ - childBlueprint, - ], - ); - - await game.addFromBlueprint(parentBlueprint); - await game.ready(); - - for (final component in childBlueprint.components) { - expect(game.children, contains(component)); - expect(parentBlueprint.components, contains(component)); - } - for (final component in parentBlueprint.components) { - expect(game.children, contains(component)); - } - }); - }); -} diff --git a/packages/pinball_flame/test/src/contact_behavior_test.dart b/packages/pinball_flame/test/src/contact_behavior_test.dart new file mode 100644 index 00000000..cf7fe35a --- /dev/null +++ b/packages/pinball_flame/test/src/contact_behavior_test.dart @@ -0,0 +1,256 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestBodyComponent extends BodyComponent { + @override + Body createBody() => world.createBody(BodyDef()); +} + +class _TestContactBehavior extends ContactBehavior { + int beginContactCallsCount = 0; + @override + void beginContact(Object other, Contact contact) { + beginContactCallsCount++; + super.beginContact(other, contact); + } + + int endContactCallsCount = 0; + @override + void endContact(Object other, Contact contact) { + endContactCallsCount++; + super.endContact(other, contact); + } + + int preSolveContactCallsCount = 0; + @override + void preSolve(Object other, Contact contact, Manifold oldManifold) { + preSolveContactCallsCount++; + super.preSolve(other, contact, oldManifold); + } + + int postSolveContactCallsCount = 0; + @override + void postSolve(Object other, Contact contact, ContactImpulse impulse) { + postSolveContactCallsCount++; + super.postSolve(other, contact, impulse); + } +} + +class _MockContactCallbacks extends Mock implements ContactCallbacks {} + +class _MockContact extends Mock implements Contact {} + +class _MockManifold extends Mock implements Manifold {} + +class _MockContactImpulse extends Mock implements ContactImpulse {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(Forge2DGame.new); + + group('ContactBehavior', () { + late Object other; + late Contact contact; + late Manifold manifold; + late ContactImpulse contactImpulse; + late FixtureDef fixtureDef; + + setUp(() { + other = Object(); + contact = _MockContact(); + manifold = _MockManifold(); + contactImpulse = _MockContactImpulse(); + fixtureDef = FixtureDef(CircleShape()); + }); + + flameTester.test( + "should add a new ContactCallbacks to the parent's body userData " + 'when not applied to fixtures', + (game) async { + final parent = _TestBodyComponent(); + final contactBehavior = ContactBehavior(); + await parent.add(contactBehavior); + await game.ensureAdd(parent); + + expect(parent.body.userData, isA()); + }, + ); + + flameTester.test( + 'should add a new ContactCallbacks to the targeted fixture ', + (game) async { + final parent = _TestBodyComponent(); + + await game.ensureAdd(parent); + final fixture1 = + parent.body.createFixture(fixtureDef..userData = 'foo'); + final fixture2 = parent.body.createFixture(fixtureDef..userData = null); + final contactBehavior = ContactBehavior() + ..applyTo( + [fixture1.userData!], + ); + + await parent.ensureAdd(contactBehavior); + + expect(parent.body.userData, isNull); + expect(fixture1.userData, isA()); + expect(fixture2.userData, isNull); + }, + ); + + flameTester.test( + 'should add a new ContactCallbacks to the targeted fixtures ', + (game) async { + final parent = _TestBodyComponent(); + + await game.ensureAdd(parent); + final fixture1 = + parent.body.createFixture(fixtureDef..userData = 'foo'); + final fixture2 = + parent.body.createFixture(fixtureDef..userData = 'boo'); + final contactBehavior = ContactBehavior() + ..applyTo([ + fixture1.userData!, + fixture2.userData!, + ]); + + await parent.ensureAdd(contactBehavior); + + expect(parent.body.userData, isNull); + expect(fixture1.userData, isA()); + expect(fixture2.userData, isA()); + }, + ); + + flameTester.test( + "should respect the previous ContactCallbacks in the parent's userData " + 'when not applied to fixtures', + (game) async { + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + final contactCallbacks1 = _MockContactCallbacks(); + parent.body.userData = contactCallbacks1; + + final contactBehavior = ContactBehavior(); + await parent.ensureAdd(contactBehavior); + + final contactCallbacks = parent.body.userData! as ContactCallbacks; + + contactCallbacks.beginContact(other, contact); + verify( + () => contactCallbacks1.beginContact(other, contact), + ).called(1); + + contactCallbacks.endContact(other, contact); + verify( + () => contactCallbacks1.endContact(other, contact), + ).called(1); + + contactCallbacks.preSolve(other, contact, manifold); + verify( + () => contactCallbacks1.preSolve(other, contact, manifold), + ).called(1); + + contactCallbacks.postSolve(other, contact, contactImpulse); + verify( + () => contactCallbacks1.postSolve(other, contact, contactImpulse), + ).called(1); + }, + ); + + flameTester.test( + 'can group multiple ContactBehaviors and keep listening', + (game) async { + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + + final contactBehavior1 = _TestContactBehavior(); + final contactBehavior2 = _TestContactBehavior(); + final contactBehavior3 = _TestContactBehavior(); + await parent.ensureAddAll([ + contactBehavior1, + contactBehavior2, + contactBehavior3, + ]); + + final contactCallbacks = parent.body.userData! as ContactCallbacks; + + contactCallbacks.beginContact(other, contact); + expect(contactBehavior1.beginContactCallsCount, equals(1)); + expect(contactBehavior2.beginContactCallsCount, equals(1)); + expect(contactBehavior3.beginContactCallsCount, equals(1)); + + contactCallbacks.endContact(other, contact); + expect(contactBehavior1.endContactCallsCount, equals(1)); + expect(contactBehavior2.endContactCallsCount, equals(1)); + expect(contactBehavior3.endContactCallsCount, equals(1)); + + contactCallbacks.preSolve(other, contact, manifold); + expect(contactBehavior1.preSolveContactCallsCount, equals(1)); + expect(contactBehavior2.preSolveContactCallsCount, equals(1)); + expect(contactBehavior3.preSolveContactCallsCount, equals(1)); + + contactCallbacks.postSolve(other, contact, contactImpulse); + expect(contactBehavior1.postSolveContactCallsCount, equals(1)); + expect(contactBehavior2.postSolveContactCallsCount, equals(1)); + expect(contactBehavior3.postSolveContactCallsCount, equals(1)); + }, + ); + + flameTester.test( + 'can group multiple ContactBehaviors and keep listening ' + 'when applied to a fixture', + (game) async { + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + + final fixture = parent.body.createFixture(fixtureDef..userData = 'foo'); + + final contactBehavior1 = _TestContactBehavior() + ..applyTo( + [fixture.userData!], + ); + final contactBehavior2 = _TestContactBehavior() + ..applyTo( + [fixture.userData!], + ); + final contactBehavior3 = _TestContactBehavior() + ..applyTo( + [fixture.userData!], + ); + await parent.ensureAddAll([ + contactBehavior1, + contactBehavior2, + contactBehavior3, + ]); + + final contactCallbacks = fixture.userData! as ContactCallbacks; + + contactCallbacks.beginContact(other, contact); + expect(contactBehavior1.beginContactCallsCount, equals(1)); + expect(contactBehavior2.beginContactCallsCount, equals(1)); + expect(contactBehavior3.beginContactCallsCount, equals(1)); + + contactCallbacks.endContact(other, contact); + expect(contactBehavior1.endContactCallsCount, equals(1)); + expect(contactBehavior2.endContactCallsCount, equals(1)); + expect(contactBehavior3.endContactCallsCount, equals(1)); + + contactCallbacks.preSolve(other, contact, manifold); + expect(contactBehavior1.preSolveContactCallsCount, equals(1)); + expect(contactBehavior2.preSolveContactCallsCount, equals(1)); + expect(contactBehavior3.preSolveContactCallsCount, equals(1)); + + contactCallbacks.postSolve(other, contact, contactImpulse); + expect(contactBehavior1.postSolveContactCallsCount, equals(1)); + expect(contactBehavior2.postSolveContactCallsCount, equals(1)); + expect(contactBehavior3.postSolveContactCallsCount, equals(1)); + }, + ); + }); +} diff --git a/packages/pinball_flame/test/src/rendering/golden/rendering/blue_red.png b/packages/pinball_flame/test/src/rendering/golden/rendering/blue_red.png new file mode 100644 index 00000000..4ca86375 Binary files /dev/null and b/packages/pinball_flame/test/src/rendering/golden/rendering/blue_red.png differ diff --git a/packages/pinball_flame/test/src/rendering/golden/rendering/red_blue.png b/packages/pinball_flame/test/src/rendering/golden/rendering/red_blue.png new file mode 100644 index 00000000..a657024f Binary files /dev/null and b/packages/pinball_flame/test/src/rendering/golden/rendering/red_blue.png differ diff --git a/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart b/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart new file mode 100644 index 00000000..b6007bc5 --- /dev/null +++ b/packages/pinball_flame/test/src/rendering/z_canvas_component_test.dart @@ -0,0 +1,385 @@ +// ignore_for_file: cascade_invocations + +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart' hide Image; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestCircleComponent extends CircleComponent with ZIndex { + _TestCircleComponent(Color color) + : super( + paint: Paint()..color = color, + radius: 10, + ); +} + +class _MockCanvas extends Mock implements Canvas {} + +class _MockImage extends Mock implements Image {} + +class _MockPicture extends Mock implements Picture {} + +class _MockParagraph extends Mock implements Paragraph {} + +class _MockVertices extends Mock implements Vertices {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(FlameGame.new); + const goldenPrefix = 'golden/rendering/'; + + group('ZCanvasComponent', () { + flameTester.test('loads correctly', (game) async { + final component = ZCanvasComponent(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + flameTester.testGameWidget( + 'red circle renders behind blue circle', + setUp: (game, tester) async { + final canvas = ZCanvasComponent( + children: [ + _TestCircleComponent(Colors.blue)..zIndex = 1, + _TestCircleComponent(Colors.red)..zIndex = 0, + ], + ); + await game.ensureAdd(canvas); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenPrefix}red_blue.png'), + ); + }, + ); + + flameTester.testGameWidget( + 'blue circle renders behind red circle', + setUp: (game, tester) async { + final canvas = ZCanvasComponent( + children: [ + _TestCircleComponent(Colors.blue)..zIndex = 0, + _TestCircleComponent(Colors.red)..zIndex = 1 + ], + ); + await game.ensureAdd(canvas); + + game.camera.followVector2(Vector2.zero()); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('${goldenPrefix}blue_red.png'), + ); + }, + ); + }); + + group('ZCanvas', () { + late Canvas canvas; + late Path path; + late RRect rRect; + late Rect rect; + late Paint paint; + late Image atlas; + late BlendMode blendMode; + late Color color; + late Offset offset; + late Float64List float64list; + late Float32List float32list; + late Int32List int32list; + late Picture picture; + late Paragraph paragraph; + late Vertices vertices; + + setUp(() { + canvas = _MockCanvas(); + path = Path(); + rRect = RRect.zero; + rect = Rect.zero; + paint = Paint(); + atlas = _MockImage(); + blendMode = BlendMode.clear; + color = Colors.black; + offset = Offset.zero; + float64list = Float64List(1); + float32list = Float32List(1); + int32list = Int32List(1); + picture = _MockPicture(); + paragraph = _MockParagraph(); + vertices = _MockVertices(); + }); + + test("clipPath calls Canvas's clipPath", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.clipPath(path, doAntiAlias: false); + verify( + () => canvas.clipPath(path, doAntiAlias: false), + ).called(1); + }); + + test("clipRRect calls Canvas's clipRRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.clipRRect(rRect, doAntiAlias: false); + verify( + () => canvas.clipRRect(rRect, doAntiAlias: false), + ).called(1); + }); + + test("clipRect calls Canvas's clipRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.clipRect(rect, doAntiAlias: false); + verify( + () => canvas.clipRect(rect, doAntiAlias: false), + ).called(1); + }); + + test("drawArc calls Canvas's drawArc", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawArc(rect, 0, 1, false, paint); + verify( + () => canvas.drawArc(rect, 0, 1, false, paint), + ).called(1); + }); + + test("drawAtlas calls Canvas's drawAtlas", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint); + verify( + () => canvas.drawAtlas(atlas, [], [], [], blendMode, rect, paint), + ).called(1); + }); + + test("drawCircle calls Canvas's drawCircle", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawCircle(offset, 0, paint); + verify( + () => canvas.drawCircle(offset, 0, paint), + ).called(1); + }); + + test("drawColor calls Canvas's drawColor", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawColor(color, blendMode); + verify( + () => canvas.drawColor(color, blendMode), + ).called(1); + }); + + test("drawDRRect calls Canvas's drawDRRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawDRRect(rRect, rRect, paint); + verify( + () => canvas.drawDRRect(rRect, rRect, paint), + ).called(1); + }); + + test("drawImage calls Canvas's drawImage", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawImage(atlas, offset, paint); + verify( + () => canvas.drawImage(atlas, offset, paint), + ).called(1); + }); + + test("drawImageNine calls Canvas's drawImageNine", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawImageNine(atlas, rect, rect, paint); + verify( + () => canvas.drawImageNine(atlas, rect, rect, paint), + ).called(1); + }); + + test("drawImageRect calls Canvas's drawImageRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawImageRect(atlas, rect, rect, paint); + verify( + () => canvas.drawImageRect(atlas, rect, rect, paint), + ).called(1); + }); + + test("drawLine calls Canvas's drawLine", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawLine(offset, offset, paint); + verify( + () => canvas.drawLine(offset, offset, paint), + ).called(1); + }); + + test("drawOval calls Canvas's drawOval", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawOval(rect, paint); + verify( + () => canvas.drawOval(rect, paint), + ).called(1); + }); + + test("drawPaint calls Canvas's drawPaint", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawPaint(paint); + verify( + () => canvas.drawPaint(paint), + ).called(1); + }); + + test("drawParagraph calls Canvas's drawParagraph", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawParagraph(paragraph, offset); + verify( + () => canvas.drawParagraph(paragraph, offset), + ).called(1); + }); + + test("drawPath calls Canvas's drawPath", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawPath(path, paint); + verify( + () => canvas.drawPath(path, paint), + ).called(1); + }); + + test("drawPicture calls Canvas's drawPicture", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawPicture(picture); + verify( + () => canvas.drawPicture(picture), + ).called(1); + }); + + test("drawPoints calls Canvas's drawPoints", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawPoints(PointMode.points, [offset], paint); + verify( + () => canvas.drawPoints(PointMode.points, [offset], paint), + ).called(1); + }); + + test("drawRRect calls Canvas's drawRRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawRRect(rRect, paint); + verify( + () => canvas.drawRRect(rRect, paint), + ).called(1); + }); + + test("drawRawAtlas calls Canvas's drawRawAtlas", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawRawAtlas( + atlas, + float32list, + float32list, + int32list, + BlendMode.clear, + rect, + paint, + ); + verify( + () => canvas.drawRawAtlas( + atlas, + float32list, + float32list, + int32list, + BlendMode.clear, + rect, + paint, + ), + ).called(1); + }); + + test("drawRawPoints calls Canvas's drawRawPoints", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawRawPoints(PointMode.points, float32list, paint); + verify( + () => canvas.drawRawPoints(PointMode.points, float32list, paint), + ).called(1); + }); + + test("drawRect calls Canvas's drawRect", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawRect(rect, paint); + verify( + () => canvas.drawRect(rect, paint), + ).called(1); + }); + + test("drawShadow calls Canvas's drawShadow", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawShadow(path, color, 0, false); + verify( + () => canvas.drawShadow(path, color, 0, false), + ).called(1); + }); + + test("drawVertices calls Canvas's drawVertices", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.drawVertices(vertices, blendMode, paint); + verify( + () => canvas.drawVertices(vertices, blendMode, paint), + ).called(1); + }); + + test("getSaveCount calls Canvas's getSaveCount", () { + final zcanvas = ZCanvas()..canvas = canvas; + when(() => canvas.getSaveCount()).thenReturn(1); + zcanvas.getSaveCount(); + verify(() => canvas.getSaveCount()).called(1); + }); + + test("restore calls Canvas's restore", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.restore(); + verify(() => canvas.restore()).called(1); + }); + + test("rotate calls Canvas's rotate", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.rotate(0); + verify(() => canvas.rotate(0)).called(1); + }); + + test("save calls Canvas's save", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.save(); + verify(() => canvas.save()).called(1); + }); + + test("saveLayer calls Canvas's saveLayer", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.saveLayer(rect, paint); + verify(() => canvas.saveLayer(rect, paint)).called(1); + }); + + test("scale calls Canvas's scale", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.scale(0, 0); + verify(() => canvas.scale(0, 0)).called(1); + }); + + test("skew calls Canvas's skew", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.skew(0, 0); + verify(() => canvas.skew(0, 0)).called(1); + }); + + test("transform calls Canvas's transform", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.transform(float64list); + verify(() => canvas.transform(float64list)).called(1); + }); + + test("translate calls Canvas's translate", () { + final zcanvas = ZCanvas()..canvas = canvas; + zcanvas.translate(0, 0); + verify(() => canvas.translate(0, 0)).called(1); + }); + }); +} diff --git a/packages/pinball_theme/lib/pinball_theme.dart b/packages/pinball_theme/lib/pinball_theme.dart index 139a70dc..c8f9b53e 100644 --- a/packages/pinball_theme/lib/pinball_theme.dart +++ b/packages/pinball_theme/lib/pinball_theme.dart @@ -1,5 +1,4 @@ library pinball_theme; export 'src/generated/generated.dart'; -export 'src/pinball_theme.dart'; export 'src/themes/themes.dart'; diff --git a/packages/pinball_theme/lib/src/pinball_theme.dart b/packages/pinball_theme/lib/src/pinball_theme.dart deleted file mode 100644 index a766a129..00000000 --- a/packages/pinball_theme/lib/src/pinball_theme.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -/// {@template pinball_theme} -/// Defines all theme assets and attributes. -/// -/// Game components should have a getter specified here to load their -/// corresponding assets for the game. -/// {@endtemplate} -class PinballTheme extends Equatable { - /// {@macro pinball_theme} - const PinballTheme({ - required CharacterTheme characterTheme, - }) : _characterTheme = characterTheme; - - final CharacterTheme _characterTheme; - - /// [CharacterTheme] for the chosen character. - CharacterTheme get characterTheme => _characterTheme; - - @override - List get props => [_characterTheme]; -} diff --git a/packages/pinball_theme/test/src/pinball_theme_test.dart b/packages/pinball_theme/test/src/pinball_theme_test.dart deleted file mode 100644 index 899eec64..00000000 --- a/packages/pinball_theme/test/src/pinball_theme_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -void main() { - group('PinballTheme', () { - const characterTheme = SparkyTheme(); - - test('can be instantiated', () { - expect(PinballTheme(characterTheme: characterTheme), isNotNull); - }); - - test('supports value equality', () { - expect( - PinballTheme(characterTheme: characterTheme), - equals(PinballTheme(characterTheme: characterTheme)), - ); - }); - - test('characterTheme is correct', () { - expect( - PinballTheme(characterTheme: characterTheme).characterTheme, - equals(characterTheme), - ); - }); - }); -} diff --git a/packages/pinball_ui/.gitignore b/packages/pinball_ui/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/pinball_ui/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/pinball_ui/README.md b/packages/pinball_ui/README.md new file mode 100644 index 00000000..cabc194a --- /dev/null +++ b/packages/pinball_ui/README.md @@ -0,0 +1,11 @@ +# pinball_ui + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +UI Toolkit for the Pinball Flutter Application + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/packages/pinball_ui/analysis_options.yaml b/packages/pinball_ui/analysis_options.yaml new file mode 100644 index 00000000..f8155aa6 --- /dev/null +++ b/packages/pinball_ui/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml +analyzer: + exclude: + - lib/**/*.gen.dart diff --git a/packages/pinball_ui/assets/images/button/pinball_button.png b/packages/pinball_ui/assets/images/button/pinball_button.png new file mode 100644 index 00000000..62373b85 Binary files /dev/null and b/packages/pinball_ui/assets/images/button/pinball_button.png differ diff --git a/packages/pinball_ui/assets/images/dialog/background.png b/packages/pinball_ui/assets/images/dialog/background.png new file mode 100644 index 00000000..0aad300f Binary files /dev/null and b/packages/pinball_ui/assets/images/dialog/background.png differ diff --git a/packages/pinball_ui/fonts/PixeloidMono-1G8ae.ttf b/packages/pinball_ui/fonts/PixeloidMono-1G8ae.ttf new file mode 100644 index 00000000..a797c1e1 Binary files /dev/null and b/packages/pinball_ui/fonts/PixeloidMono-1G8ae.ttf differ diff --git a/packages/pinball_ui/fonts/PixeloidSans-nR3g1.ttf b/packages/pinball_ui/fonts/PixeloidSans-nR3g1.ttf new file mode 100644 index 00000000..2f9a03b4 Binary files /dev/null and b/packages/pinball_ui/fonts/PixeloidSans-nR3g1.ttf differ diff --git a/packages/pinball_ui/fonts/PixeloidSansBold-RpeJo.ttf b/packages/pinball_ui/fonts/PixeloidSansBold-RpeJo.ttf new file mode 100644 index 00000000..81194f5d Binary files /dev/null and b/packages/pinball_ui/fonts/PixeloidSansBold-RpeJo.ttf differ diff --git a/packages/pinball_ui/lib/gen/assets.gen.dart b/packages/pinball_ui/lib/gen/assets.gen.dart new file mode 100644 index 00000000..8972e8e0 --- /dev/null +++ b/packages/pinball_ui/lib/gen/assets.gen.dart @@ -0,0 +1,87 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// ignore_for_file: directives_ordering,unnecessary_import + +import 'package:flutter/widgets.dart'; + +class $AssetsImagesGen { + const $AssetsImagesGen(); + + $AssetsImagesButtonGen get button => const $AssetsImagesButtonGen(); + $AssetsImagesDialogGen get dialog => const $AssetsImagesDialogGen(); +} + +class $AssetsImagesButtonGen { + const $AssetsImagesButtonGen(); + + /// File path: assets/images/button/pinball_button.png + AssetGenImage get pinballButton => + const AssetGenImage('assets/images/button/pinball_button.png'); +} + +class $AssetsImagesDialogGen { + const $AssetsImagesDialogGen(); + + /// File path: assets/images/dialog/background.png + AssetGenImage get background => + const AssetGenImage('assets/images/dialog/background.png'); +} + +class Assets { + Assets._(); + + static const $AssetsImagesGen images = $AssetsImagesGen(); +} + +class AssetGenImage extends AssetImage { + const AssetGenImage(String assetName) + : super(assetName, package: 'pinball_ui'); + + Image image({ + Key? key, + ImageFrameBuilder? frameBuilder, + ImageLoadingBuilder? loadingBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? width, + double? height, + Color? color, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + }) { + return Image( + key: key, + image: this, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + color: color, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + ); + } + + String get path => assetName; +} diff --git a/packages/pinball_ui/lib/gen/fonts.gen.dart b/packages/pinball_ui/lib/gen/fonts.gen.dart new file mode 100644 index 00000000..5f77da16 --- /dev/null +++ b/packages/pinball_ui/lib/gen/fonts.gen.dart @@ -0,0 +1,16 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// ignore_for_file: directives_ordering,unnecessary_import + +class FontFamily { + FontFamily._(); + + /// Font family: PixeloidMono + static const String pixeloidMono = 'PixeloidMono'; + + /// Font family: PixeloidSans + static const String pixeloidSans = 'PixeloidSans'; +} diff --git a/packages/pinball_ui/lib/gen/gen.dart b/packages/pinball_ui/lib/gen/gen.dart new file mode 100644 index 00000000..e7ad4c54 --- /dev/null +++ b/packages/pinball_ui/lib/gen/gen.dart @@ -0,0 +1 @@ +export 'assets.gen.dart'; diff --git a/packages/pinball_ui/lib/pinball_ui.dart b/packages/pinball_ui/lib/pinball_ui.dart new file mode 100644 index 00000000..eacb5681 --- /dev/null +++ b/packages/pinball_ui/lib/pinball_ui.dart @@ -0,0 +1,9 @@ +library pinball_ui; + +export 'package:url_launcher/url_launcher.dart'; +export 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +export 'src/dialog/dialog.dart'; +export 'src/external_links/external_links.dart'; +export 'src/theme/theme.dart'; +export 'src/widgets/widgets.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/dialog.dart b/packages/pinball_ui/lib/src/dialog/dialog.dart new file mode 100644 index 00000000..4731eb5f --- /dev/null +++ b/packages/pinball_ui/lib/src/dialog/dialog.dart @@ -0,0 +1,2 @@ +export 'pinball_dialog.dart'; +export 'pixelated_decoration.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/pinball_dialog.dart b/packages/pinball_ui/lib/src/dialog/pinball_dialog.dart new file mode 100644 index 00000000..8ff04754 --- /dev/null +++ b/packages/pinball_ui/lib/src/dialog/pinball_dialog.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template pinball_dialog} +/// Pinball-themed dialog. +/// {@endtemplate} +class PinballDialog extends StatelessWidget { + /// {@macro pinball_dialog} + const PinballDialog({ + Key? key, + required this.title, + required this.child, + this.subtitle, + }) : super(key: key); + + /// Title shown in the dialog. + final String title; + + /// Optional subtitle shown below the [title]. + final String? subtitle; + + /// Body of the dialog. + final Widget child; + + @override + Widget build(BuildContext context) { + final height = MediaQuery.of(context).size.height * 0.5; + return Center( + child: SizedBox( + height: height, + width: height * 1.4, + child: PixelatedDecoration( + header: subtitle != null + ? _TitleAndSubtitle(title: title, subtitle: subtitle!) + : _Title(title: title), + body: child, + ), + ), + ); + } +} + +class _Title extends StatelessWidget { + const _Title({Key? key, required this.title}) : super(key: key); + + final String title; + + @override + Widget build(BuildContext context) => Text( + title, + style: Theme.of(context).textTheme.headline3!.copyWith( + fontWeight: FontWeight.bold, + color: PinballColors.darkBlue, + ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ); +} + +class _TitleAndSubtitle extends StatelessWidget { + const _TitleAndSubtitle({ + Key? key, + required this.title, + required this.subtitle, + }) : super(key: key); + + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _Title(title: title), + Text( + subtitle, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: textTheme.headline3!.copyWith(fontWeight: FontWeight.normal), + ), + ], + ); + } +} diff --git a/packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart b/packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart new file mode 100644 index 00000000..542a00db --- /dev/null +++ b/packages/pinball_ui/lib/src/dialog/pixelated_decoration.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/gen/gen.dart'; + +/// {@template pixelated_decoration} +/// Widget with pixelated background and layout defined for dialog displays. +/// {@endtemplate} +class PixelatedDecoration extends StatelessWidget { + /// {@macro pixelated_decoration} + const PixelatedDecoration({ + Key? key, + required Widget header, + required Widget body, + }) : _header = header, + _body = body, + super(key: key); + + final Widget _header; + final Widget _body; + + @override + Widget build(BuildContext context) { + const radius = BorderRadius.all(Radius.circular(12)); + + return Material( + borderRadius: radius, + child: Padding( + padding: const EdgeInsets.all(5), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: radius, + image: DecorationImage( + fit: BoxFit.fill, + image: AssetImage(Assets.images.dialog.background.keyName), + ), + ), + child: ClipRRect( + borderRadius: radius, + child: Column( + children: [ + Expanded( + child: Center( + child: _header, + ), + ), + Expanded( + flex: 4, + child: _body, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/pinball_ui/lib/src/external_links/external_links.dart b/packages/pinball_ui/lib/src/external_links/external_links.dart new file mode 100644 index 00000000..8e4792ea --- /dev/null +++ b/packages/pinball_ui/lib/src/external_links/external_links.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// Opens the given [url] in a new tab of the host browser +Future openLink(String url, {VoidCallback? onError}) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else if (onError != null) { + onError(); + } +} diff --git a/lib/theme/app_colors.dart b/packages/pinball_ui/lib/src/theme/pinball_colors.dart similarity index 68% rename from lib/theme/app_colors.dart rename to packages/pinball_ui/lib/src/theme/pinball_colors.dart index 2d3899a6..5db27229 100644 --- a/lib/theme/app_colors.dart +++ b/packages/pinball_ui/lib/src/theme/pinball_colors.dart @@ -1,15 +1,11 @@ // ignore_for_file: public_member_api_docs - import 'package:flutter/material.dart'; -abstract class AppColors { +abstract class PinballColors { static const Color white = Color(0xFFFFFFFF); - static const Color darkBlue = Color(0xFF0C32A4); - - static const Color orange = Color(0xFFFFEE02); - + static const Color yellow = Color(0xFFFFEE02); + static const Color orange = Color(0xFFE5AB05); static const Color blue = Color(0xFF4B94F6); - static const Color transparent = Color(0x00000000); } diff --git a/packages/pinball_ui/lib/src/theme/pinball_text_style.dart b/packages/pinball_ui/lib/src/theme/pinball_text_style.dart new file mode 100644 index 00000000..5e0a7fa2 --- /dev/null +++ b/packages/pinball_ui/lib/src/theme/pinball_text_style.dart @@ -0,0 +1,53 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/widgets.dart'; +import 'package:pinball_ui/gen/fonts.gen.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +const _fontPackage = 'pinball_components'; +const _primaryFontFamily = FontFamily.pixeloidSans; + +abstract class PinballTextStyle { + static const headline1 = TextStyle( + fontSize: 28, + package: _fontPackage, + fontFamily: _primaryFontFamily, + color: PinballColors.white, + ); + + static const headline2 = TextStyle( + fontSize: 24, + package: _fontPackage, + fontFamily: _primaryFontFamily, + color: PinballColors.white, + ); + + static const headline3 = TextStyle( + color: PinballColors.darkBlue, + fontSize: 20, + package: _fontPackage, + fontFamily: _primaryFontFamily, + fontWeight: FontWeight.bold, + ); + + static const headline4 = TextStyle( + color: PinballColors.white, + fontSize: 16, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const subtitle2 = TextStyle( + color: PinballColors.white, + fontSize: 16, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const subtitle1 = TextStyle( + fontSize: 10, + fontFamily: _primaryFontFamily, + package: _fontPackage, + color: PinballColors.yellow, + ); +} diff --git a/packages/pinball_ui/lib/src/theme/pinball_theme.dart b/packages/pinball_ui/lib/src/theme/pinball_theme.dart new file mode 100644 index 00000000..cf62b2ef --- /dev/null +++ b/packages/pinball_ui/lib/src/theme/pinball_theme.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// Pinball theme +class PinballTheme { + /// Standard [ThemeData] for Pinball UI + static ThemeData get standard { + return ThemeData( + textTheme: _textTheme, + ); + } + + static TextTheme get _textTheme { + return const TextTheme( + headline1: PinballTextStyle.headline1, + headline2: PinballTextStyle.headline2, + headline3: PinballTextStyle.headline3, + headline4: PinballTextStyle.headline4, + subtitle1: PinballTextStyle.subtitle1, + subtitle2: PinballTextStyle.subtitle2, + ); + } +} diff --git a/packages/pinball_ui/lib/src/theme/theme.dart b/packages/pinball_ui/lib/src/theme/theme.dart new file mode 100644 index 00000000..71c78942 --- /dev/null +++ b/packages/pinball_ui/lib/src/theme/theme.dart @@ -0,0 +1,3 @@ +export 'pinball_colors.dart'; +export 'pinball_text_style.dart'; +export 'pinball_theme.dart'; diff --git a/packages/pinball_ui/lib/src/widgets/pinball_button.dart b/packages/pinball_ui/lib/src/widgets/pinball_button.dart new file mode 100644 index 00000000..585a8d54 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/pinball_button.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/gen/gen.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template pinball_button} +/// Pinball-themed button with pixel art. +/// {@endtemplate} +class PinballButton extends StatelessWidget { + /// {@macro pinball_button} + const PinballButton({ + Key? key, + required this.text, + required this.onTap, + }) : super(key: key); + + /// Text of the button. + final String text; + + /// Tap callback. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(Assets.images.button.pinballButton.keyName), + ), + ), + child: Center( + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: Text( + text, + style: Theme.of(context) + .textTheme + .headline3! + .copyWith(color: PinballColors.white), + ), + ), + ), + ), + ); + } +} diff --git a/packages/pinball_ui/lib/src/widgets/widgets.dart b/packages/pinball_ui/lib/src/widgets/widgets.dart new file mode 100644 index 00000000..34d952b6 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/widgets.dart @@ -0,0 +1 @@ +export 'pinball_button.dart'; diff --git a/packages/pinball_ui/pubspec.yaml b/packages/pinball_ui/pubspec.yaml new file mode 100644 index 00000000..747b1b8f --- /dev/null +++ b/packages/pinball_ui/pubspec.yaml @@ -0,0 +1,40 @@ +name: pinball_ui +description: UI Toolkit for the Pinball Flutter Application +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + url_launcher: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^0.3.0 + test: ^1.19.2 + very_good_analysis: ^2.4.0 + +flutter: + uses-material-design: true + generate: true + assets: + - assets/images/dialog/ + - assets/images/button/ + fonts: + - family: PixeloidSans + fonts: + - asset: fonts/PixeloidSans-nR3g1.ttf + - asset: fonts/PixeloidSansBold-RpeJo.ttf + weight: 700 + - family: PixeloidMono + fonts: + - asset: fonts/PixeloidMono-1G8ae.ttf + +flutter_gen: + line_length: 80 + assets: + package_parameter_enabled: true diff --git a/packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart b/packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart new file mode 100644 index 00000000..85e2c4da --- /dev/null +++ b/packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart @@ -0,0 +1,44 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballDialog', () { + group('with title only', () { + testWidgets('renders the title and the body', (tester) async { + tester.binding.window.physicalSizeTestValue = const Size(2000, 4000); + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + await tester.pumpWidget( + const MaterialApp( + home: PinballDialog(title: 'title', child: Placeholder()), + ), + ); + expect(find.byType(PixelatedDecoration), findsOneWidget); + expect(find.text('title'), findsOneWidget); + expect(find.byType(Placeholder), findsOneWidget); + }); + }); + + group('with title and subtitle', () { + testWidgets('renders the title, subtitle and the body', (tester) async { + tester.binding.window.physicalSizeTestValue = const Size(2000, 4000); + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + await tester.pumpWidget( + MaterialApp( + home: PinballDialog( + title: 'title', + subtitle: 'sub', + child: Icon(Icons.home), + ), + ), + ); + expect(find.byType(PixelatedDecoration), findsOneWidget); + expect(find.text('title'), findsOneWidget); + expect(find.text('sub'), findsOneWidget); + expect(find.byType(Icon), findsOneWidget); + }); + }); + }); +} diff --git a/packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart b/packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart new file mode 100644 index 00000000..772f2570 --- /dev/null +++ b/packages/pinball_ui/test/src/dialog/pixelated_decoration_test.dart @@ -0,0 +1,26 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PixelatedDecoration', () { + testWidgets('renders header and body', (tester) async { + const headerText = 'header'; + const bodyText = 'body'; + + await tester.pumpWidget( + MaterialApp( + home: PixelatedDecoration( + header: Text(headerText), + body: Text(bodyText), + ), + ), + ); + + expect(find.text(headerText), findsOneWidget); + expect(find.text(bodyText), findsOneWidget); + }); + }); +} diff --git a/packages/pinball_ui/test/src/external_links/external_links_test.dart b/packages/pinball_ui/test/src/external_links/external_links_test.dart new file mode 100644 index 00000000..83cc2d63 --- /dev/null +++ b/packages/pinball_ui/test/src/external_links/external_links_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_ui/pinball_ui.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockUrlLauncher extends Mock + with MockPlatformInterfaceMixin + implements UrlLauncherPlatform {} + +void main() { + late UrlLauncherPlatform urlLauncher; + + setUp(() { + urlLauncher = MockUrlLauncher(); + UrlLauncherPlatform.instance = urlLauncher; + }); + + group('openLink', () { + test('launches the link', () async { + when( + () => urlLauncher.canLaunch(any()), + ).thenAnswer( + (_) async => true, + ); + when( + () => urlLauncher.launch( + any(), + useSafariVC: any(named: 'useSafariVC'), + useWebView: any(named: 'useWebView'), + enableJavaScript: any(named: 'enableJavaScript'), + enableDomStorage: any(named: 'enableDomStorage'), + universalLinksOnly: any(named: 'universalLinksOnly'), + headers: any(named: 'headers'), + ), + ).thenAnswer( + (_) async => true, + ); + await openLink('uri'); + verify( + () => urlLauncher.launch( + any(), + useSafariVC: any(named: 'useSafariVC'), + useWebView: any(named: 'useWebView'), + enableJavaScript: any(named: 'enableJavaScript'), + enableDomStorage: any(named: 'enableDomStorage'), + universalLinksOnly: any(named: 'universalLinksOnly'), + headers: any(named: 'headers'), + ), + ); + }); + + test('executes the onError callback when it cannot launch', () async { + var wasCalled = false; + when( + () => urlLauncher.canLaunch(any()), + ).thenAnswer( + (_) async => false, + ); + when( + () => urlLauncher.launch( + any(), + useSafariVC: any(named: 'useSafariVC'), + useWebView: any(named: 'useWebView'), + enableJavaScript: any(named: 'enableJavaScript'), + enableDomStorage: any(named: 'enableDomStorage'), + universalLinksOnly: any(named: 'universalLinksOnly'), + headers: any(named: 'headers'), + ), + ).thenAnswer( + (_) async => true, + ); + await openLink( + 'url', + onError: () { + wasCalled = true; + }, + ); + await expectLater(wasCalled, isTrue); + }); + }); +} diff --git a/packages/pinball_ui/test/src/theme/pinball_colors_test.dart b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart new file mode 100644 index 00000000..36e45c0d --- /dev/null +++ b/packages/pinball_ui/test/src/theme/pinball_colors_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballColors', () { + test('white is 0xFFFFFFFF', () { + expect(PinballColors.white, const Color(0xFFFFFFFF)); + }); + + test('darkBlue is 0xFF0C32A4', () { + expect(PinballColors.darkBlue, const Color(0xFF0C32A4)); + }); + + test('yellow is 0xFFFFEE02', () { + expect(PinballColors.yellow, const Color(0xFFFFEE02)); + }); + + test('orange is 0xFFE5AB05', () { + expect(PinballColors.orange, const Color(0xFFE5AB05)); + }); + + test('blue is 0xFF4B94F6', () { + expect(PinballColors.blue, const Color(0xFF4B94F6)); + }); + + test('transparent is 0x00000000', () { + expect(PinballColors.transparent, const Color(0x00000000)); + }); + }); +} diff --git a/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart b/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart new file mode 100644 index 00000000..2af092b2 --- /dev/null +++ b/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballTextStyle', () { + test('headline1 has fontSize 28 and white color', () { + const style = PinballTextStyle.headline1; + expect(style.fontSize, 28); + expect(style.color, PinballColors.white); + }); + + test('headline2 has fontSize 24', () { + const style = PinballTextStyle.headline2; + expect(style.fontSize, 24); + }); + + test('headline3 has fontSize 20 and dark blue color', () { + const style = PinballTextStyle.headline3; + expect(style.fontSize, 20); + expect(style.color, PinballColors.darkBlue); + }); + + test('headline4 has fontSize 16 and white color', () { + const style = PinballTextStyle.headline4; + expect(style.fontSize, 16); + expect(style.color, PinballColors.white); + }); + + test('subtitle1 has fontSize 10 and yellow color', () { + const style = PinballTextStyle.subtitle1; + expect(style.fontSize, 10); + expect(style.color, PinballColors.yellow); + }); + + test('subtitle2 has fontSize 16 and white color', () { + const style = PinballTextStyle.subtitle2; + expect(style.fontSize, 16); + expect(style.color, PinballColors.white); + }); + }); +} diff --git a/packages/pinball_ui/test/src/theme/pinball_theme_test.dart b/packages/pinball_ui/test/src/theme/pinball_theme_test.dart new file mode 100644 index 00000000..915927f8 --- /dev/null +++ b/packages/pinball_ui/test/src/theme/pinball_theme_test.dart @@ -0,0 +1,98 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballTheme', () { + group('standard', () { + test('headline1 matches PinballTextStyle#headline1', () { + expect( + PinballTheme.standard.textTheme.headline1!.fontSize, + PinballTextStyle.headline1.fontSize, + ); + expect( + PinballTheme.standard.textTheme.headline1!.color, + PinballTextStyle.headline1.color, + ); + expect( + PinballTheme.standard.textTheme.headline1!.fontFamily, + PinballTextStyle.headline1.fontFamily, + ); + }); + + test('headline2 matches PinballTextStyle#headline2', () { + expect( + PinballTheme.standard.textTheme.headline2!.fontSize, + PinballTextStyle.headline2.fontSize, + ); + expect( + PinballTheme.standard.textTheme.headline2!.fontFamily, + PinballTextStyle.headline2.fontFamily, + ); + expect( + PinballTheme.standard.textTheme.headline2!.fontWeight, + PinballTextStyle.headline2.fontWeight, + ); + }); + + test('headline3 matches PinballTextStyle#headline3', () { + expect( + PinballTheme.standard.textTheme.headline3!.fontSize, + PinballTextStyle.headline3.fontSize, + ); + expect( + PinballTheme.standard.textTheme.headline3!.color, + PinballTextStyle.headline3.color, + ); + expect( + PinballTheme.standard.textTheme.headline3!.fontFamily, + PinballTextStyle.headline3.fontFamily, + ); + }); + + test('headline4 matches PinballTextStyle#headline4', () { + expect( + PinballTheme.standard.textTheme.headline4!.fontSize, + PinballTextStyle.headline4.fontSize, + ); + expect( + PinballTheme.standard.textTheme.headline4!.color, + PinballTextStyle.headline4.color, + ); + expect( + PinballTheme.standard.textTheme.headline4!.fontFamily, + PinballTextStyle.headline4.fontFamily, + ); + }); + + test('subtitle1 matches PinballTextStyle#subtitle1', () { + expect( + PinballTheme.standard.textTheme.subtitle1!.fontSize, + PinballTextStyle.subtitle1.fontSize, + ); + expect( + PinballTheme.standard.textTheme.subtitle1!.color, + PinballTextStyle.subtitle1.color, + ); + expect( + PinballTheme.standard.textTheme.subtitle1!.fontFamily, + PinballTextStyle.subtitle1.fontFamily, + ); + }); + + test('subtitle2 matches PinballTextStyle#subtitle2', () { + expect( + PinballTheme.standard.textTheme.subtitle2!.fontSize, + PinballTextStyle.subtitle2.fontSize, + ); + expect( + PinballTheme.standard.textTheme.subtitle2!.color, + PinballTextStyle.subtitle2.color, + ); + expect( + PinballTheme.standard.textTheme.subtitle2!.fontFamily, + PinballTextStyle.subtitle2.fontFamily, + ); + }); + }); + }); +} diff --git a/packages/pinball_ui/test/src/widgets/pinball_button_test.dart b/packages/pinball_ui/test/src/widgets/pinball_button_test.dart new file mode 100644 index 00000000..064fbb6a --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/pinball_button_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballButton', () { + testWidgets('renders the given text and responds to taps', (tester) async { + var wasTapped = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PinballButton( + text: 'test', + onTap: () { + wasTapped = true; + }, + ), + ), + ), + ), + ); + await tester.tap(find.text('test')); + expect(wasTapped, isTrue); + }); + }); +} diff --git a/packages/platform_helper/.gitignore b/packages/platform_helper/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/platform_helper/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/platform_helper/README.md b/packages/platform_helper/README.md new file mode 100644 index 00000000..7a96e658 --- /dev/null +++ b/packages/platform_helper/README.md @@ -0,0 +1,11 @@ +# platform_helper + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Platform helper for Pinball application. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/platform_helper/analysis_options.yaml b/packages/platform_helper/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/platform_helper/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/platform_helper/lib/platform_helper.dart b/packages/platform_helper/lib/platform_helper.dart new file mode 100644 index 00000000..4b6d7f48 --- /dev/null +++ b/packages/platform_helper/lib/platform_helper.dart @@ -0,0 +1,3 @@ +library platform_helper; + +export 'src/platform_helper.dart'; diff --git a/packages/platform_helper/lib/src/platform_helper.dart b/packages/platform_helper/lib/src/platform_helper.dart new file mode 100644 index 00000000..638d1ab6 --- /dev/null +++ b/packages/platform_helper/lib/src/platform_helper.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; + +/// {@template platform_helper} +/// Returns whether the current platform is running on a mobile device. +/// {@endtemplate} +class PlatformHelper { + /// {@macro platform_helper} + bool get isMobile { + return defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android; + } +} diff --git a/packages/platform_helper/pubspec.yaml b/packages/platform_helper/pubspec.yaml new file mode 100644 index 00000000..edff346a --- /dev/null +++ b/packages/platform_helper/pubspec.yaml @@ -0,0 +1,16 @@ +name: platform_helper +description: Platform helper for Pinball application. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^2.4.0 \ No newline at end of file diff --git a/packages/platform_helper/test/src/platform_helper_test.dart b/packages/platform_helper/test/src/platform_helper_test.dart new file mode 100644 index 00000000..69bec3a8 --- /dev/null +++ b/packages/platform_helper/test/src/platform_helper_test.dart @@ -0,0 +1,39 @@ +// ignore_for_file: prefer_const_constructors +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform_helper/platform_helper.dart'; + +void main() { + group('PlatformHelper', () { + test('can be instantiated', () { + expect(PlatformHelper(), isNotNull); + }); + + group('isMobile', () { + tearDown(() async { + debugDefaultTargetPlatformOverride = null; + }); + + test('returns true when defaultTargetPlatform is iOS', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + expect(PlatformHelper().isMobile, isTrue); + debugDefaultTargetPlatformOverride = null; + }); + + test('returns true when defaultTargetPlatform is android', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + expect(PlatformHelper().isMobile, isTrue); + debugDefaultTargetPlatformOverride = null; + }); + + test( + 'returns false when defaultTargetPlatform is niether iOS nor android', + () async { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + expect(PlatformHelper().isMobile, isFalse); + debugDefaultTargetPlatformOverride = null; + }, + ); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 1a502f37..db5233c3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -36,6 +36,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.20.1" + authentication_repository: + dependency: "direct main" + description: + path: "packages/authentication_repository" + relative: true + source: path + version: "1.0.0+1" bloc: dependency: "direct main" description: @@ -169,13 +176,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" - firebase_core: + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.16" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "6.2.4" + firebase_auth_web: dependency: transitive + description: + name: firebase_auth_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.13" + firebase_core: + dependency: "direct main" description: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.13.1" + version: "1.15.0" firebase_core_platform_interface: dependency: transitive description: @@ -189,7 +217,7 @@ packages: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.6.1" + version: "1.6.2" flame: dependency: "direct main" description: @@ -214,9 +242,11 @@ packages: flame_forge2d: dependency: "direct main" description: - name: flame_forge2d - url: "https://pub.dartlang.org" - source: hosted + path: "packages/flame_forge2d" + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f + resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f + url: "https://github.com/flame-engine/flame/" + source: git version: "0.11.0" flame_test: dependency: "direct dev" @@ -497,6 +527,13 @@ packages: relative: true source: path version: "1.0.0+1" + pinball_ui: + dependency: "direct main" + description: + path: "packages/pinball_ui" + relative: true + source: path + version: "1.0.0+1" platform: dependency: transitive description: @@ -504,6 +541,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + platform_helper: + dependency: "direct main" + description: + path: "packages/platform_helper" + relative: true + source: path + version: "1.0.0+1" plugin_platform_interface: dependency: transitive description: @@ -656,6 +700,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.15" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" uuid: dependency: transitive description: @@ -728,4 +828,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.16.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index f17ea07a..fa08f453 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,12 +7,20 @@ environment: sdk: ">=2.16.0 <3.0.0" dependencies: + authentication_repository: + path: packages/authentication_repository bloc: ^8.0.2 cloud_firestore: ^3.1.10 equatable: ^2.0.3 + firebase_auth: ^3.3.16 + firebase_core: ^1.15.0 flame: ^1.1.1 flame_bloc: ^1.2.0 - flame_forge2d: ^0.11.0 + flame_forge2d: + git: + url: https://github.com/flame-engine/flame/ + path: packages/flame_forge2d/ + ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f flutter: sdk: flutter flutter_bloc: ^8.0.1 @@ -31,6 +39,10 @@ dependencies: path: packages/pinball_flame pinball_theme: path: packages/pinball_theme + pinball_ui: + path: packages/pinball_ui + platform_helper: + path: packages/platform_helper dev_dependencies: bloc_test: ^9.0.2 @@ -48,6 +60,7 @@ flutter: assets: - assets/images/components/ - assets/images/bonus_animation/ + - assets/images/score/ flutter_gen: line_length: 80 diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 9fc79b5d..83e37499 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -5,6 +5,7 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -16,19 +17,21 @@ import '../../helpers/mocks.dart'; void main() { group('App', () { + late AuthenticationRepository authenticationRepository; late LeaderboardRepository leaderboardRepository; late PinballAudio pinballAudio; setUp(() { + authenticationRepository = MockAuthenticationRepository(); leaderboardRepository = MockLeaderboardRepository(); pinballAudio = MockPinballAudio(); - when(pinballAudio.load).thenAnswer((_) => Future.value()); }); testWidgets('renders PinballGamePage', (tester) async { await tester.pumpWidget( App( + authenticationRepository: authenticationRepository, leaderboardRepository: leaderboardRepository, pinballAudio: pinballAudio, ), diff --git a/test/footer/footer_test.dart b/test/footer/footer_test.dart new file mode 100644 index 00000000..c18d76e7 --- /dev/null +++ b/test/footer/footer_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/footer/footer.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +import '../helpers/helpers.dart'; + +void main() { + group('Footer', () { + late UrlLauncherPlatform urlLauncher; + + setUp(() async { + urlLauncher = MockUrlLauncher(); + UrlLauncherPlatform.instance = urlLauncher; + }); + testWidgets('renders "Made with..." and "Google I/O"', (tester) async { + await tester.pumpApp(const Footer()); + expect(find.text('Google I/O'), findsOneWidget); + expect( + find.byWidgetPredicate( + (widget) => + widget is RichText && + widget.text.toPlainText() == 'Made with Flutter & Firebase', + ), + findsOneWidget, + ); + }); + + testWidgets( + 'tapping on "Flutter" opens the flutter website', + (tester) async { + when(() => urlLauncher.canLaunch(any())).thenAnswer((_) async => true); + when( + () => urlLauncher.launch( + any(), + useSafariVC: any(named: 'useSafariVC'), + useWebView: any(named: 'useWebView'), + enableJavaScript: any(named: 'enableJavaScript'), + enableDomStorage: any(named: 'enableDomStorage'), + universalLinksOnly: any(named: 'universalLinksOnly'), + headers: any(named: 'headers'), + ), + ).thenAnswer((_) async => true); + await tester.pumpApp(const Footer()); + final flutterTextFinder = find.byWidgetPredicate( + (widget) => widget is RichText && tapTextSpan(widget, 'Flutter'), + ); + await tester.tap(flutterTextFinder); + await tester.pumpAndSettle(); + verify( + () => urlLauncher.launch( + 'https://flutter.dev', + useSafariVC: any(named: 'useSafariVC'), + useWebView: any(named: 'useWebView'), + enableJavaScript: any(named: 'enableJavaScript'), + enableDomStorage: any(named: 'enableDomStorage'), + universalLinksOnly: any(named: 'universalLinksOnly'), + headers: any(named: 'headers'), + ), + ); + }, + ); + + testWidgets( + 'tapping on "Firebase" opens the firebase website', + (tester) async { + when(() => urlLauncher.canLaunch(any())).thenAnswer((_) async => true); + when( + () => urlLauncher.launch( + any(), + useSafariVC: any(named: 'useSafariVC'), + useWebView: any(named: 'useWebView'), + enableJavaScript: any(named: 'enableJavaScript'), + enableDomStorage: any(named: 'enableDomStorage'), + universalLinksOnly: any(named: 'universalLinksOnly'), + headers: any(named: 'headers'), + ), + ).thenAnswer((_) async => true); + await tester.pumpApp(const Footer()); + final firebaseTextFinder = find.byWidgetPredicate( + (widget) => widget is RichText && tapTextSpan(widget, 'Firebase'), + ); + await tester.tap(firebaseTextFinder); + await tester.pumpAndSettle(); + verify( + () => urlLauncher.launch( + 'https://firebase.google.com', + useSafariVC: any(named: 'useSafariVC'), + useWebView: any(named: 'useWebView'), + enableJavaScript: any(named: 'enableJavaScript'), + enableDomStorage: any(named: 'enableDomStorage'), + universalLinksOnly: any(named: 'universalLinksOnly'), + headers: any(named: 'headers'), + ), + ); + }, + ); + }); +} diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 37e14f73..3711105e 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -4,27 +4,69 @@ import 'package:pinball/game/game.dart'; void main() { group('GameBloc', () { - test('initial state has 3 balls and empty score', () { + test('initial state has 3 rounds and empty score', () { final gameBloc = GameBloc(); expect(gameBloc.state.score, equals(0)); - expect(gameBloc.state.balls, equals(3)); + expect(gameBloc.state.rounds, equals(3)); }); - group('LostBall', () { + group('RoundLost', () { blocTest( - 'decreases number of balls', + 'decreases number of rounds ' + 'when there are already available rounds', build: GameBloc.new, act: (bloc) { - bloc.add(const BallLost()); + bloc.add(const RoundLost()); }, expect: () => [ const GameState( score: 0, - balls: 2, + multiplier: 1, + rounds: 2, bonusHistory: [], ), ], ); + + blocTest( + 'apply multiplier to score ' + 'when round is lost', + build: GameBloc.new, + seed: () => const GameState( + score: 5, + multiplier: 3, + rounds: 2, + bonusHistory: [], + ), + act: (bloc) { + bloc.add(const RoundLost()); + }, + expect: () => [ + isA() + ..having((state) => state.score, 'score', 15) + ..having((state) => state.rounds, 'rounds', 1), + ], + ); + + blocTest( + 'resets multiplier ' + 'when round is lost', + build: GameBloc.new, + seed: () => const GameState( + score: 5, + multiplier: 3, + rounds: 2, + bonusHistory: [], + ), + act: (bloc) { + bloc.add(const RoundLost()); + }, + expect: () => [ + isA() + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.rounds, 'rounds', 1), + ], + ); }); group('Scored', () { @@ -36,16 +78,12 @@ void main() { ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ - const GameState( - score: 2, - balls: 3, - bonusHistory: [], - ), - const GameState( - score: 5, - balls: 3, - bonusHistory: [], - ), + isA() + ..having((state) => state.score, 'score', 2) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 5) + ..having((state) => state.isGameOver, 'isGameOver', false), ], ); @@ -54,27 +92,85 @@ void main() { 'when game is over', build: GameBloc.new, act: (bloc) { - for (var i = 0; i < bloc.state.balls; i++) { - bloc.add(const BallLost()); + for (var i = 0; i < bloc.state.rounds; i++) { + bloc.add(const RoundLost()); } bloc.add(const Scored(points: 2)); }, expect: () => [ - const GameState( - score: 0, - balls: 2, - bonusHistory: [], - ), - const GameState( - score: 0, - balls: 1, - bonusHistory: [], - ), - const GameState( - score: 0, - balls: 0, - bonusHistory: [], - ), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.rounds, 'rounds', 2) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.rounds, 'rounds', 1) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.rounds, 'rounds', 0) + ..having((state) => state.isGameOver, 'isGameOver', true), + ], + ); + }); + + group('MultiplierIncreased', () { + blocTest( + 'increases multiplier ' + 'when multiplier is below 6 and game is not over', + build: GameBloc.new, + act: (bloc) => bloc + ..add(const MultiplierIncreased()) + ..add(const MultiplierIncreased()), + expect: () => [ + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 2) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 3) + ..having((state) => state.isGameOver, 'isGameOver', false), + ], + ); + + blocTest( + "doesn't increase multiplier " + 'when multiplier is 6 and game is not over', + build: GameBloc.new, + seed: () => const GameState( + score: 0, + multiplier: 6, + rounds: 3, + bonusHistory: [], + ), + act: (bloc) => bloc..add(const MultiplierIncreased()), + expect: () => const [], + ); + + blocTest( + "doesn't increase multiplier " + 'when game is over', + build: GameBloc.new, + act: (bloc) { + for (var i = 0; i < bloc.state.rounds; i++) { + bloc.add(const RoundLost()); + } + bloc.add(const MultiplierIncreased()); + }, + expect: () => [ + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.isGameOver, 'isGameOver', true), ], ); }); @@ -88,17 +184,19 @@ void main() { act: (bloc) => bloc ..add(const BonusActivated(GameBonus.googleWord)) ..add(const BonusActivated(GameBonus.dashNest)), - expect: () => const [ - GameState( - score: 0, - balls: 3, - bonusHistory: [GameBonus.googleWord], - ), - GameState( - score: 0, - balls: 3, - bonusHistory: [GameBonus.googleWord, GameBonus.dashNest], - ), + expect: () => [ + isA() + ..having( + (state) => state.bonusHistory, + 'bonusHistory', + [GameBonus.googleWord], + ), + isA() + ..having( + (state) => state.bonusHistory, + 'bonusHistory', + [GameBonus.googleWord, GameBonus.dashNest], + ), ], ); }, @@ -109,12 +207,13 @@ void main() { 'adds game bonus', build: GameBloc.new, act: (bloc) => bloc..add(const SparkyTurboChargeActivated()), - expect: () => const [ - GameState( - score: 0, - balls: 3, - bonusHistory: [GameBonus.sparkyTurboCharge], - ), + expect: () => [ + isA() + ..having( + (state) => state.bonusHistory, + 'bonusHistory', + [GameBonus.sparkyTurboCharge], + ), ], ); }); diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index d7d587bd..6a39bd67 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -5,15 +5,15 @@ import 'package:pinball/game/game.dart'; void main() { group('GameEvent', () { - group('BallLost', () { + group('RoundLost', () { test('can be instantiated', () { - expect(const BallLost(), isNotNull); + expect(const RoundLost(), isNotNull); }); test('supports value equality', () { expect( - BallLost(), - equals(const BallLost()), + RoundLost(), + equals(const RoundLost()), ); }); }); @@ -41,6 +41,19 @@ void main() { }); }); + group('MultiplierIncreased', () { + test('can be instantiated', () { + expect(const MultiplierIncreased(), isNotNull); + }); + + test('supports value equality', () { + expect( + MultiplierIncreased(), + equals(const MultiplierIncreased()), + ); + }); + }); + group('BonusActivated', () { test('can be instantiated', () { expect(const BonusActivated(GameBonus.dashNest), isNotNull); diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 8170346f..add25e05 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -9,13 +9,15 @@ void main() { expect( GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: const [], ), equals( const GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ), ), @@ -27,7 +29,8 @@ void main() { expect( const GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ), isNotNull, @@ -37,12 +40,13 @@ void main() { test( 'throws AssertionError ' - 'when balls are negative', + 'when score is negative', () { expect( () => GameState( - balls: -1, - score: 0, + score: -1, + multiplier: 1, + rounds: 3, bonusHistory: const [], ), throwsAssertionError, @@ -52,12 +56,29 @@ void main() { test( 'throws AssertionError ' - 'when score is negative', + 'when multiplier is less than 1', () { expect( () => GameState( - balls: 0, - score: -1, + score: 1, + multiplier: 0, + rounds: 3, + bonusHistory: const [], + ), + throwsAssertionError, + ); + }, + ); + + test( + 'throws AssertionError ' + 'when rounds is negative', + () { + expect( + () => GameState( + score: 1, + multiplier: 1, + rounds: -1, bonusHistory: const [], ), throwsAssertionError, @@ -68,10 +89,11 @@ void main() { group('isGameOver', () { test( 'is true ' - 'when no balls are left', () { + 'when no rounds are left', () { const gameState = GameState( - balls: 0, score: 0, + multiplier: 1, + rounds: 0, bonusHistory: [], ); expect(gameState.isGameOver, isTrue); @@ -79,10 +101,11 @@ void main() { test( 'is false ' - 'when one 1 ball left', () { + 'when one 1 round left', () { const gameState = GameState( - balls: 1, score: 0, + multiplier: 1, + rounds: 1, bonusHistory: [], ); expect(gameState.isGameOver, isFalse); @@ -95,8 +118,9 @@ void main() { 'when scored is decreased', () { const gameState = GameState( - balls: 0, score: 2, + multiplier: 1, + rounds: 3, bonusHistory: [], ); expect( @@ -111,8 +135,9 @@ void main() { 'when no argument specified', () { const gameState = GameState( - balls: 0, score: 2, + multiplier: 1, + rounds: 3, bonusHistory: [], ); expect( @@ -128,12 +153,14 @@ void main() { () { const gameState = GameState( score: 2, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ); final otherGameState = GameState( score: gameState.score + 1, - balls: gameState.balls + 1, + multiplier: gameState.multiplier + 1, + rounds: gameState.rounds + 1, bonusHistory: const [GameBonus.googleWord], ); expect(gameState, isNot(equals(otherGameState))); @@ -141,7 +168,8 @@ void main() { expect( gameState.copyWith( score: otherGameState.score, - balls: otherGameState.balls, + multiplier: otherGameState.multiplier, + rounds: otherGameState.rounds, bonusHistory: otherGameState.bonusHistory, ), equals(otherGameState), diff --git a/test/game/components/alien_zone_test.dart b/test/game/components/alien_zone_test.dart deleted file mode 100644 index de4e58fc..00000000 --- a/test/game/components/alien_zone_test.dart +++ /dev/null @@ -1,104 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'dart:ui'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.alienBumper.a.active.keyName, - Assets.images.alienBumper.a.inactive.keyName, - Assets.images.alienBumper.b.active.keyName, - Assets.images.alienBumper.b.inactive.keyName, - ]; - final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); - - group('AlienZone', () { - flameTester.test( - 'loads correctly', - (game) async { - final alienZone = AlienZone(); - await game.ensureAdd(alienZone); - - expect(game.contains(alienZone), isTrue); - }, - ); - - group('loads', () { - flameTester.test( - 'two AlienBumper', - (game) async { - final alienZone = AlienZone(); - await game.ensureAdd(alienZone); - - expect( - alienZone.descendants().whereType().length, - equals(2), - ); - }, - ); - }); - - group('bumpers', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); - - flameTester.test('call animate on contact', (game) async { - final contactCallback = AlienBumperBallContactCallback(); - final bumper = MockAlienBumper(); - final ball = MockBall(); - - when(bumper.animate).thenAnswer((_) async {}); - - contactCallback.begin(bumper, ball, MockContact()); - - verify(bumper.animate).called(1); - }); - - flameBlocTester.testGameWidget( - 'add Scored event', - setUp: (game, tester) async { - final ball = Ball(baseColor: const Color(0xFF00FFFF)); - final alienZone = AlienZone(); - - await game.ensureAdd(alienZone); - await game.ensureAdd(ball); - game.addContactCallback(BallScorePointsCallback(game)); - - final bumpers = alienZone.descendants().whereType(); - - for (final bumper in bumpers) { - beginContact(game, bumper, ball); - verify( - () => gameBloc.add( - Scored(points: bumper.points), - ), - ).called(1); - } - }, - ); - }); - }); -} diff --git a/test/game/components/android_acres_test.dart b/test/game/components/android_acres_test.dart new file mode 100644 index 00000000..4c5e4cb7 --- /dev/null +++ b/test/game/components/android_acres_test.dart @@ -0,0 +1,92 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('AndroidAcres', () { + flameTester.test('loads correctly', (game) async { + final component = AndroidAcres(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + group('loads', () { + flameTester.test( + 'a Spaceship', + (game) async { + await game.ensureAdd(AndroidAcres()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'a SpaceshipRamp', + (game) async { + await game.ensureAdd(AndroidAcres()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'a SpaceshipRail', + (game) async { + await game.ensureAdd(AndroidAcres()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'three AndroidBumper', + (game) async { + await game.ensureAdd(AndroidAcres()); + expect( + game.descendants().whereType().length, + equals(3), + ); + }, + ); + }); + }); +} diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart deleted file mode 100644 index bc1c5c39..00000000 --- a/test/game/components/board_test.dart +++ /dev/null @@ -1,120 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, - Assets.images.dash.bumper.a.active.keyName, - Assets.images.dash.bumper.a.inactive.keyName, - Assets.images.dash.bumper.b.active.keyName, - Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.animatronic.keyName, - Assets.images.signpost.inactive.keyName, - Assets.images.signpost.active1.keyName, - Assets.images.signpost.active2.keyName, - Assets.images.signpost.active3.keyName, - Assets.images.baseboard.left.keyName, - Assets.images.baseboard.right.keyName, - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - ]; - final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); - - group('Board', () { - flameTester.test( - 'loads correctly', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - expect(game.contains(board), isTrue); - }, - ); - - group('loads', () { - flameTester.test( - 'one left flipper', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final leftFlippers = board.descendants().whereType().where( - (flipper) => flipper.side.isLeft, - ); - expect(leftFlippers.length, equals(1)); - }, - ); - - flameTester.test( - 'one right flipper', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - final rightFlippers = board.descendants().whereType().where( - (flipper) => flipper.side.isRight, - ); - expect(rightFlippers.length, equals(1)); - }, - ); - - flameTester.test( - 'two Baseboards', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final baseboards = board.descendants().whereType(); - expect(baseboards.length, equals(2)); - }, - ); - - flameTester.test( - 'two Kickers', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final kickers = board.descendants().whereType(); - expect(kickers.length, equals(2)); - }, - ); - - flameTester.test( - 'one FlutterForest', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final flutterForest = board.descendants().whereType(); - expect(flutterForest.length, equals(1)); - }, - ); - - flameTester.test( - 'one ChromeDino', - (game) async { - final board = Board(); - await game.ready(); - await game.ensureAdd(board); - - final chromeDino = board.descendants().whereType(); - expect(chromeDino.length, equals(1)); - }, - ); - }); - }); -} diff --git a/test/game/components/bottom_group_test.dart b/test/game/components/bottom_group_test.dart new file mode 100644 index 00000000..1d9e58ab --- /dev/null +++ b/test/game/components/bottom_group_test.dart @@ -0,0 +1,90 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.kicker.left.lit.keyName, + Assets.images.kicker.left.dimmed.keyName, + Assets.images.kicker.right.lit.keyName, + Assets.images.kicker.right.dimmed.keyName, + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('BottomGroup', () { + flameTester.test( + 'loads correctly', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + expect(game.contains(bottomGroup), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'one left flipper', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final leftFlippers = + bottomGroup.descendants().whereType().where( + (flipper) => flipper.side.isLeft, + ); + expect(leftFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'one right flipper', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final rightFlippers = + bottomGroup.descendants().whereType().where( + (flipper) => flipper.side.isRight, + ); + expect(rightFlippers.length, equals(1)); + }, + ); + + flameTester.test( + 'two Baseboards', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final basebottomGroups = + bottomGroup.descendants().whereType(); + expect(basebottomGroups.length, equals(2)); + }, + ); + + flameTester.test( + 'two Kickers', + (game) async { + final bottomGroup = BottomGroup(); + await game.ensureAdd(bottomGroup); + + final kickers = bottomGroup.descendants().whereType(); + expect(kickers.length, equals(2)); + }, + ); + }); + }); +} diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index e615d508..c84ddaa7 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -53,16 +53,39 @@ void main() { }); flameBlocTester.testGameWidget( - 'lost adds BallLost to GameBloc', + "lost doesn't adds RoundLost to GameBloc " + 'when there are balls left', + setUp: (game, tester) async { + final controller = BallController(ball); + await ball.add(controller); + await game.ensureAdd(ball); + + final otherBall = Ball(baseColor: const Color(0xFF00FFFF)); + final otherController = BallController(otherBall); + await otherBall.add(otherController); + await game.ensureAdd(otherBall); + + controller.lost(); + await game.ready(); + }, + verify: (game, tester) async { + verifyNever(() => gameBloc.add(const RoundLost())); + }, + ); + + flameBlocTester.testGameWidget( + 'lost adds RoundLost to GameBloc ' + 'when there are no balls left', setUp: (game, tester) async { final controller = BallController(ball); await ball.add(controller); await game.ensureAdd(ball); controller.lost(); + await game.ready(); }, verify: (game, tester) async { - verify(() => gameBloc.add(const BallLost())).called(1); + verify(() => gameBloc.add(const RoundLost())).called(1); }, ); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index 1b2a7e43..36a8161b 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -15,7 +15,9 @@ void main() { Assets.images.flipper.left.keyName, Assets.images.flipper.right.keyName, ]; - final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); final flameBlocTester = FlameBlocTester( gameBuilder: EmptyPinballTestGame.new, @@ -23,7 +25,8 @@ void main() { final bloc = MockGameBloc(); const state = GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: [], ); whenListen(bloc, Stream.value(state), initialState: state); diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index eee2bcb0..a39bdef6 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -20,7 +20,8 @@ void main() { final bloc = MockGameBloc(); const state = GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: [], ); whenListen(bloc, Stream.value(state), initialState: state); diff --git a/test/game/components/drain_test.dart b/test/game/components/drain_test.dart new file mode 100644 index 00000000..f1875a56 --- /dev/null +++ b/test/game/components/drain_test.dart @@ -0,0 +1,60 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + + group('Drain', () { + flameTester.test( + 'loads correctly', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(game.contains(drain), isTrue); + }, + ); + + flameTester.test( + 'body is static', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(drain.body.bodyType, equals(BodyType.static)); + }, + ); + + flameTester.test( + 'is sensor', + (game) async { + final drain = Drain(); + await game.ensureAdd(drain); + + expect(drain.body.fixtures.first.isSensor, isTrue); + }, + ); + + test( + 'calls lost on contact with ball', + () async { + final drain = Drain(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + + drain.beginContact(ball, MockContact()); + + verify(controller.lost).called(1); + }, + ); + }); +} diff --git a/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart new file mode 100644 index 00000000..c1834516 --- /dev/null +++ b/test/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior_test.dart @@ -0,0 +1,84 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.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/flutter_forest/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + group('FlutterForestBonusBehavior', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'adds GameBonus.dashNest to the game when all bumpers are active', + setUp: (game, tester) async { + final behavior = FlutterForestBonusBehavior(); + final parent = FlutterForest.test(); + final bumpers = [ + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + ]; + await parent.addAll(bumpers); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + for (final bumper in bumpers) { + bumper.bloc.onBallContacted(); + } + await tester.pump(); + + verify( + () => gameBloc.add(const BonusActivated(GameBonus.dashNest)), + ).called(1); + }, + ); + + flameBlocTester.testGameWidget( + 'adds a new ball to the game when all bumpers are active', + setUp: (game, tester) async { + final behavior = FlutterForestBonusBehavior(); + final parent = FlutterForest.test(); + final bumpers = [ + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + DashNestBumper.test(bloc: DashNestBumperCubit()), + ]; + await parent.addAll(bumpers); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + for (final bumper in bumpers) { + bumper.bloc.onBallContacted(); + } + await game.ready(); + + expect( + game.descendants().whereType().single, + isNotNull, + ); + }, + ); + }); +} diff --git a/test/game/components/flutter_forest/flutter_forest_test.dart b/test/game/components/flutter_forest/flutter_forest_test.dart new file mode 100644 index 00000000..4f32e0f4 --- /dev/null +++ b/test/game/components/flutter_forest/flutter_forest_test.dart @@ -0,0 +1,80 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.dash.bumper.a.active.keyName, + Assets.images.dash.bumper.a.inactive.keyName, + Assets.images.dash.bumper.b.active.keyName, + Assets.images.dash.bumper.b.inactive.keyName, + Assets.images.dash.animatronic.keyName, + Assets.images.signpost.inactive.keyName, + Assets.images.signpost.active1.keyName, + Assets.images.signpost.active2.keyName, + Assets.images.signpost.active3.keyName, + ]; + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('FlutterForest', () { + flameTester.test( + 'loads correctly', + (game) async { + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + expect(game.contains(flutterForest), isTrue); + }, + ); + + group('loads', () { + flameTester.test( + 'a Signpost', + (game) async { + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'a DashAnimatronic', + (game) async { + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.firstChild(), + isNotNull, + ); + }, + ); + + flameTester.test( + 'three DashNestBumper', + (game) async { + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(3), + ); + }, + ); + }); + }); +} diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart deleted file mode 100644 index 388410d5..00000000 --- a/test/game/components/flutter_forest_test.dart +++ /dev/null @@ -1,177 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, - Assets.images.dash.bumper.a.active.keyName, - Assets.images.dash.bumper.a.inactive.keyName, - Assets.images.dash.bumper.b.active.keyName, - Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.animatronic.keyName, - Assets.images.signpost.inactive.keyName, - Assets.images.signpost.active1.keyName, - Assets.images.signpost.active2.keyName, - Assets.images.signpost.active3.keyName, - ]; - final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); - - group('FlutterForest', () { - flameTester.test( - 'loads correctly', - (game) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - - expect(game.contains(flutterForest), isTrue); - }, - ); - - group('loads', () { - flameTester.test( - 'a Signpost', - (game) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - - expect( - flutterForest.descendants().whereType().length, - equals(1), - ); - }, - ); - - flameTester.test( - 'a DashAnimatronic', - (game) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - - expect( - flutterForest.firstChild(), - isNotNull, - ); - }, - ); - - flameTester.test( - 'three DashNestBumper', - (game) async { - final flutterForest = FlutterForest(); - await game.ensureAdd(flutterForest); - - expect( - flutterForest.descendants().whereType().length, - equals(3), - ); - }, - ); - }); - - group('bumpers', () { - late Ball ball; - late GameBloc gameBloc; - - setUp(() { - ball = Ball(baseColor: const Color(0xFF00FFFF)); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: () => EmptyPinballTestGame(assets), - blocBuilder: () { - gameBloc = MockGameBloc(); - const state = GameState.initial(); - whenListen(gameBloc, Stream.value(state), initialState: state); - return gameBloc; - }, - assets: assets, - ); - - flameBlocTester.testGameWidget( - 'add Scored event', - setUp: (game, tester) async { - final flutterForest = FlutterForest(); - await game.ensureAddAll([ - flutterForest, - ball, - ]); - game.addContactCallback(BallScorePointsCallback(game)); - - final bumpers = flutterForest.descendants().whereType(); - - for (final bumper in bumpers) { - beginContact(game, bumper, ball); - verify( - () => gameBloc.add( - Scored(points: bumper.points), - ), - ).called(1); - } - }, - ); - - flameBlocTester.testGameWidget( - 'adds GameBonus.dashNest to the game when 3 bumpers are activated', - setUp: (game, _) async { - final ball = Ball(baseColor: const Color(0xFFFF0000)); - final flutterForest = FlutterForest(); - await game.ensureAddAll([flutterForest, ball]); - - final bumpers = flutterForest.children.whereType(); - expect(bumpers.length, equals(3)); - for (final bumper in bumpers) { - beginContact(game, bumper, ball); - await game.ready(); - - if (bumper == bumpers.last) { - verify( - () => gameBloc.add(const BonusActivated(GameBonus.dashNest)), - ).called(1); - } else { - verifyNever( - () => gameBloc.add(const BonusActivated(GameBonus.dashNest)), - ); - } - } - }, - ); - - flameBlocTester.testGameWidget( - 'deactivates bumpers when 3 are active', - setUp: (game, _) async { - final ball = Ball(baseColor: const Color(0xFFFF0000)); - final flutterForest = FlutterForest(); - await game.ensureAddAll([flutterForest, ball]); - - final bumpers = [ - MockDashNestBumper(), - MockDashNestBumper(), - MockDashNestBumper(), - ]; - - for (final bumper in bumpers) { - flutterForest.controller.activateBumper(bumper); - await game.ready(); - - if (bumper == bumpers.last) { - for (final bumper in bumpers) { - verify(bumper.deactivate).called(1); - } - } - } - }, - ); - }); - }); -} diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index 4efc7174..ef93892c 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -15,7 +15,8 @@ void main() { test('is true when the game over state has changed', () { final state = GameState( score: 10, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: const [], ); @@ -57,8 +58,7 @@ void main() { when(game.firstChild).thenReturn(backboard); when(game.firstChild).thenReturn(cameraController); when(() => game.overlays).thenReturn(overlays); - when(() => game.theme) - .thenReturn(PinballTheme(characterTheme: DashTheme())); + when(() => game.characterTheme).thenReturn(DashTheme()); }); test( @@ -67,7 +67,8 @@ void main() { gameFlowController.onNewState( GameState( score: 10, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: const [], ), ); 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 new file mode 100644 index 00000000..deca61ee --- /dev/null +++ b/test/game/components/google_word/behaviors/google_word_bonus_behavior_test.dart @@ -0,0 +1,61 @@ +// ignore_for_file: cascade_invocations + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.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 '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('GoogleWordBonusBehaviors', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + ); + + flameBlocTester.testGameWidget( + 'adds GameBonus.googleWord to the game when all letters are activated', + setUp: (game, tester) async { + 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.ensureAdd(parent); + 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/components/google_word/google_word_test.dart b/test/game/components/google_word/google_word_test.dart new file mode 100644 index 00000000..2d7d04e5 --- /dev/null +++ b/test/game/components/google_word/google_word_test.dart @@ -0,0 +1,26 @@ +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/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(EmptyPinballTestGame.new); + + group('GoogleWord', () { + flameTester.test( + 'loads the letters correctly', + (game) async { + const word = 'Google'; + final googleWord = GoogleWord(position: Vector2.zero()); + await game.ensureAdd(googleWord); + + final letters = googleWord.children.whereType(); + expect(letters.length, equals(word.length)); + }, + ); + }); +} diff --git a/test/game/components/google_word_test.dart b/test/game/components/google_word_test.dart deleted file mode 100644 index fee7bdd0..00000000 --- a/test/game/components/google_word_test.dart +++ /dev/null @@ -1,73 +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/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('GoogleWord', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); - - final flameTester = FlameTester(EmptyPinballTestGame.new); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - ); - - flameTester.test( - 'loads the letters correctly', - (game) async { - const word = 'Google'; - final googleWord = GoogleWord(position: Vector2.zero()); - await game.ensureAdd(googleWord); - - final letters = googleWord.children.whereType(); - expect(letters.length, equals(word.length)); - }, - ); - - flameBlocTester.testGameWidget( - 'adds GameBonus.googleWord to the game when all letters are activated', - setUp: (game, _) async { - final ball = Ball(baseColor: const Color(0xFFFF0000)); - final googleWord = GoogleWord(position: Vector2.zero()); - await game.ensureAddAll([googleWord, ball]); - - final letters = googleWord.children.whereType(); - expect(letters, isNotEmpty); - for (final letter in letters) { - beginContact(game, letter, ball); - await game.ready(); - - if (letter == letters.last) { - verify( - () => gameBloc.add(const BonusActivated(GameBonus.googleWord)), - ).called(1); - } else { - verifyNever( - () => gameBloc.add(const BonusActivated(GameBonus.googleWord)), - ); - } - } - }, - ); - }); -} diff --git a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart new file mode 100644 index 00000000..a4f3502c --- /dev/null +++ b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart @@ -0,0 +1,133 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ]; + + group('MultipliersBehavior', () { + late GameBloc gameBloc; + + setUp(() { + registerFallbackValue(MockComponent()); + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + group('listenWhen', () { + test('is true when the multiplier has changed', () { + final state = GameState( + score: 10, + multiplier: 2, + rounds: 0, + bonusHistory: const [], + ); + + final previous = GameState.initial(); + expect( + MultipliersBehavior().listenWhen(previous, state), + isTrue, + ); + }); + + test('is false when the multiplier state is the same', () { + final state = GameState( + score: 10, + multiplier: 1, + rounds: 0, + bonusHistory: const [], + ); + + final previous = GameState.initial(); + expect( + MultipliersBehavior().listenWhen(previous, state), + isFalse, + ); + }); + }); + + group('onNewState', () { + flameBlocTester.testGameWidget( + "calls 'next' once per each multiplier when GameBloc emit state", + setUp: (game, tester) async { + final behavior = MultipliersBehavior(); + final parent = Multipliers.test(); + final multiplierX2Cubit = MockMultiplierCubit(); + final multiplierX3Cubit = MockMultiplierCubit(); + final multipliers = [ + Multiplier.test( + value: MultiplierValue.x2, + bloc: multiplierX2Cubit, + ), + Multiplier.test( + value: MultiplierValue.x3, + bloc: multiplierX3Cubit, + ), + ]; + + whenListen( + multiplierX2Cubit, + const Stream.empty(), + initialState: MultiplierState.initial(MultiplierValue.x2), + ); + when(() => multiplierX2Cubit.next(any())).thenAnswer((_) async {}); + + whenListen( + multiplierX3Cubit, + const Stream.empty(), + initialState: MultiplierState.initial(MultiplierValue.x2), + ); + when(() => multiplierX3Cubit.next(any())).thenAnswer((_) async {}); + + await parent.addAll(multipliers); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + await tester.pump(); + + behavior.onNewState( + GameState.initial().copyWith(multiplier: 2), + ); + + for (final multiplier in multipliers) { + verify( + () => multiplier.bloc.next(any()), + ).called(1); + } + }, + ); + }); + }); +} diff --git a/test/game/components/multipliers/multipliers_test.dart b/test/game/components/multipliers/multipliers_test.dart new file mode 100644 index 00000000..6b2d95a6 --- /dev/null +++ b/test/game/components/multipliers/multipliers_test.dart @@ -0,0 +1,63 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + ]; + + late GameBloc gameBloc; + + setUp(() { + gameBloc = GameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + group('Multipliers', () { + flameBlocTester.testGameWidget( + 'loads correctly', + setUp: (game, tester) async { + final multipliersGroup = Multipliers(); + await game.ensureAdd(multipliersGroup); + + expect(game.contains(multipliersGroup), isTrue); + }, + ); + + group('loads', () { + flameBlocTester.testGameWidget( + 'five Multiplier', + setUp: (game, tester) async { + final multipliersGroup = Multipliers(); + await game.ensureAdd(multipliersGroup); + + expect( + multipliersGroup.descendants().whereType().length, + equals(5), + ); + }, + ); + }); + }); +} diff --git a/test/game/components/score_points_test.dart b/test/game/components/score_points_test.dart deleted file mode 100644 index dcd0ad82..00000000 --- a/test/game/components/score_points_test.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_components/pinball_components.dart'; - -import '../../helpers/helpers.dart'; - -class FakeScorePoints extends BodyComponent with ScorePoints { - @override - Body createBody() { - throw UnimplementedError(); - } - - @override - int get points => 2; -} - -void main() { - group('BallScorePointsCallback', () { - late PinballGame game; - late GameBloc bloc; - late PinballAudio audio; - late Ball ball; - late FakeScorePoints fakeScorePoints; - - setUp(() { - game = MockPinballGame(); - bloc = MockGameBloc(); - audio = MockPinballAudio(); - fakeScorePoints = FakeScorePoints(); - - ball = MockBall(); - final ballBody = MockBody(); - when(() => ball.body).thenReturn(ballBody); - when(() => ballBody.position).thenReturn(Vector2.all(4)); - }); - - setUpAll(() { - registerFallbackValue(FakeGameEvent()); - }); - - group('begin', () { - test( - 'emits Scored event with points', - () { - when(game.read).thenReturn(bloc); - when(() => game.audio).thenReturn(audio); - - BallScorePointsCallback(game).begin( - ball, - fakeScorePoints, - FakeContact(), - ); - - verify( - () => bloc.add( - Scored(points: fakeScorePoints.points), - ), - ).called(1); - }, - ); - - test( - 'plays a Score sound', - () { - when(game.read).thenReturn(bloc); - when(() => game.audio).thenReturn(audio); - - BallScorePointsCallback(game).begin( - ball, - fakeScorePoints, - FakeContact(), - ); - - verify(audio.score).called(1); - }, - ); - - test( - "adds a ScoreText component at Ball's position", - () { - when(game.read).thenReturn(bloc); - when(() => game.audio).thenReturn(audio); - - BallScorePointsCallback(game).begin( - ball, - fakeScorePoints, - FakeContact(), - ); - - verify( - () => game.add( - ScoreText( - text: fakeScorePoints.points.toString(), - position: ball.body.position, - ), - ), - ).called(1); - }, - ); - }); - }); -} diff --git a/test/game/components/scoring_behavior_test.dart b/test/game/components/scoring_behavior_test.dart new file mode 100644 index 00000000..731bb481 --- /dev/null +++ b/test/game/components/scoring_behavior_test.dart @@ -0,0 +1,116 @@ +// 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/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +import '../../helpers/helpers.dart'; + +class _TestBodyComponent extends BodyComponent { + @override + Body createBody() => world.createBody(BodyDef()); +} + +void main() { + group('ScoringBehavior', () { + group('beginContact', () { + late GameBloc bloc; + late PinballAudio audio; + late Ball ball; + late BodyComponent parent; + + setUp(() { + audio = MockPinballAudio(); + + ball = MockBall(); + final ballBody = MockBody(); + when(() => ball.body).thenReturn(ballBody); + when(() => ballBody.position).thenReturn(Vector2.all(4)); + + parent = _TestBodyComponent(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: () => EmptyPinballTestGame( + audio: audio, + ), + blocBuilder: () { + bloc = MockGameBloc(); + const state = GameState( + score: 0, + multiplier: 1, + rounds: 3, + bonusHistory: [], + ); + whenListen(bloc, Stream.value(state), initialState: state); + return bloc; + }, + ); + + flameBlocTester.testGameWidget( + 'emits Scored event with points', + setUp: (game, tester) async { + const points = 20; + final scoringBehavior = ScoringBehavior(points: points); + await parent.add(scoringBehavior); + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); + + scoringBehavior.beginContact(ball, MockContact()); + + verify( + () => bloc.add( + const Scored(points: points), + ), + ).called(1); + }, + ); + + flameBlocTester.testGameWidget( + 'plays score sound', + setUp: (game, tester) async { + const points = 20; + final scoringBehavior = ScoringBehavior(points: points); + await parent.add(scoringBehavior); + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); + + scoringBehavior.beginContact(ball, MockContact()); + + verify(audio.score).called(1); + }, + ); + + flameBlocTester.testGameWidget( + "adds a ScoreText component at Ball's position with points", + setUp: (game, tester) async { + const points = 20; + final scoringBehavior = ScoringBehavior(points: points); + await parent.add(scoringBehavior); + final canvas = ZCanvasComponent(children: [parent]); + await game.ensureAdd(canvas); + + scoringBehavior.beginContact(ball, MockContact()); + await game.ready(); + + final scoreText = game.descendants().whereType(); + expect(scoreText.length, equals(1)); + expect( + scoreText.first.text, + equals(points.toString()), + ); + expect( + scoreText.first.position, + equals(ball.body.position), + ); + }, + ); + }); + }); +} diff --git a/test/game/components/sparky_fire_zone_test.dart b/test/game/components/sparky_fire_zone_test.dart deleted file mode 100644 index 0ad69dab..00000000 --- a/test/game/components/sparky_fire_zone_test.dart +++ /dev/null @@ -1,163 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'dart:ui'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, - Assets.images.sparky.animatronic.keyName, - ]; - final flameTester = FlameTester(() => EmptyPinballTestGame(assets)); - - group('SparkyFireZone', () { - flameTester.test('loads correctly', (game) async { - await game.addFromBlueprint(SparkyFireZone()); - await game.ready(); - }); - - group('loads', () { - flameTester.test( - 'a SparkyComputer', - (game) async { - expect( - SparkyFireZone().blueprints.whereType().single, - isNotNull, - ); - }, - ); - - flameTester.test( - 'a SparkyAnimatronic', - (game) async { - final sparkyFireZone = SparkyFireZone(); - await game.addFromBlueprint(sparkyFireZone); - await game.ready(); - - expect( - game.descendants().whereType().single, - isNotNull, - ); - }, - ); - - flameTester.test( - 'three SparkyBumper', - (game) async { - final sparkyFireZone = SparkyFireZone(); - await game.addFromBlueprint(sparkyFireZone); - await game.ready(); - - expect( - game.descendants().whereType().length, - equals(3), - ); - }, - ); - }); - - group('bumpers', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); - - flameTester.test('call animate on contact', (game) async { - final contactCallback = SparkyBumperBallContactCallback(); - final bumper = MockSparkyBumper(); - final ball = MockBall(); - - when(bumper.animate).thenAnswer((_) async {}); - - contactCallback.begin(bumper, ball, MockContact()); - - verify(bumper.animate).called(1); - }); - - flameBlocTester.testGameWidget( - 'add Scored event', - setUp: (game, tester) async { - final ball = Ball(baseColor: const Color(0xFF00FFFF)); - final sparkyFireZone = SparkyFireZone(); - await game.addFromBlueprint(sparkyFireZone); - await game.ensureAdd(ball); - game.addContactCallback(BallScorePointsCallback(game)); - - final bumpers = sparkyFireZone.components.whereType(); - - for (final bumper in bumpers) { - beginContact(game, bumper, ball); - verify( - () => gameBloc.add( - Scored(points: bumper.points), - ), - ).called(1); - } - }, - ); - }); - }); - - group('SparkyTurboChargeSensorBallContactCallback', () { - flameTester.test('calls turboCharge', (game) async { - final callback = SparkyComputerSensorBallContactCallback(); - final ball = MockControlledBall(); - final controller = MockBallController(); - when(() => ball.controller).thenReturn(controller); - when(() => ball.gameRef).thenReturn(game); - when(controller.turboCharge).thenAnswer((_) async {}); - - callback.begin(MockSparkyComputerSensor(), ball, MockContact()); - - verify(() => ball.controller.turboCharge()).called(1); - }); - - flameTester.test('plays SparkyAnimatronic', (game) async { - final callback = SparkyComputerSensorBallContactCallback(); - final ball = MockControlledBall(); - final controller = MockBallController(); - when(() => ball.controller).thenReturn(controller); - when(() => ball.gameRef).thenReturn(game); - when(controller.turboCharge).thenAnswer((_) async {}); - - final sparkyFireZone = SparkyFireZone(); - await game.addFromBlueprint(sparkyFireZone); - await game.ready(); - - final sparkyAnimatronic = - sparkyFireZone.components.whereType().single; - - expect(sparkyAnimatronic.playing, isFalse); - callback.begin(MockSparkyComputerSensor(), ball, MockContact()); - - expect(sparkyAnimatronic.playing, isTrue); - }); - }); -} diff --git a/test/game/components/sparky_scorch_test.dart b/test/game/components/sparky_scorch_test.dart new file mode 100644 index 00000000..d331e340 --- /dev/null +++ b/test/game/components/sparky_scorch_test.dart @@ -0,0 +1,108 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.glow.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, + ]; + + final flameTester = FlameTester( + () => EmptyPinballTestGame(assets: assets), + ); + + group('SparkyScorch', () { + flameTester.test('loads correctly', (game) async { + final component = SparkyScorch(); + await game.ensureAdd(component); + expect(game.contains(component), isTrue); + }); + + group('loads', () { + flameTester.test( + 'a SparkyComputer', + (game) async { + await game.ensureAdd(SparkyScorch()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'a SparkyAnimatronic', + (game) async { + await game.ensureAdd(SparkyScorch()); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'three SparkyBumper', + (game) async { + await game.ensureAdd(SparkyScorch()); + expect( + game.descendants().whereType().length, + equals(3), + ); + }, + ); + }); + }); + + group('SparkyComputerSensor', () { + flameTester.test('calls turboCharge', (game) async { + final sensor = SparkyComputerSensor(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + when(controller.turboCharge).thenAnswer((_) async {}); + + await game.ensureAddAll([ + sensor, + SparkyAnimatronic(), + ]); + + sensor.beginContact(ball, MockContact()); + + verify(() => ball.controller.turboCharge()).called(1); + }); + + flameTester.test('plays SparkyAnimatronic', (game) async { + final sensor = SparkyComputerSensor(); + final sparkyAnimatronic = SparkyAnimatronic(); + final ball = MockControlledBall(); + final controller = MockBallController(); + when(() => ball.controller).thenReturn(controller); + when(controller.turboCharge).thenAnswer((_) async {}); + await game.ensureAddAll([ + sensor, + sparkyAnimatronic, + ]); + + expect(sparkyAnimatronic.playing, isFalse); + sensor.beginContact(ball, MockContact()); + expect(sparkyAnimatronic.playing, isTrue); + }); + }); +} diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart deleted file mode 100644 index 63a39991..00000000 --- a/test/game/components/wall_test.dart +++ /dev/null @@ -1,164 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(EmptyPinballTestGame.new); - - group('Wall', () { - flameTester.test( - 'loads correctly', - (game) async { - await game.ready(); - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(game.contains(wall), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'positions correctly', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - game.contains(wall); - - expect(wall.body.position, Vector2.zero()); - }, - ); - - flameTester.test( - 'is static', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(wall.body.bodyType, equals(BodyType.static)); - }, - ); - }); - - group('fixture', () { - flameTester.test( - 'exists', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - expect(wall.body.fixtures[0], isA()); - }, - ); - - flameTester.test( - 'has restitution', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - final fixture = wall.body.fixtures[0]; - expect(fixture.restitution, greaterThan(0)); - }, - ); - - flameTester.test( - 'has no friction', - (game) async { - final wall = Wall( - start: Vector2.zero(), - end: Vector2(100, 0), - ); - await game.ensureAdd(wall); - - final fixture = wall.body.fixtures[0]; - expect(fixture.friction, equals(0)); - }, - ); - }); - }); - - group( - 'BottomWall', - () { - group('removes ball on contact', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = GameBloc(); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - ); - - flameBlocTester.testGameWidget( - 'when ball is launch', - setUp: (game, tester) async { - final ball = ControlledBall.launch(theme: game.theme); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - game.addContactCallback(BottomWallBallContactCallback()); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - - flameBlocTester.testGameWidget( - 'when ball is bonus', - setUp: (game, tester) async { - final ball = ControlledBall.bonus(theme: game.theme); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - game.addContactCallback(BottomWallBallContactCallback()); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - - flameTester.test( - 'when ball is debug', - (game) async { - final ball = ControlledBall.debug(); - final wall = BottomWall(); - await game.ensureAddAll([ball, wall]); - game.addContactCallback(BottomWallBallContactCallback()); - - beginContact(game, ball, wall); - await game.ready(); - - expect(game.contains(ball), isFalse); - }, - ); - }); - }, - ); -} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index aebb31f9..687280c0 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,8 +1,10 @@ // ignore_for_file: cascade_invocations +import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -13,115 +15,183 @@ import '../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, + Assets.images.backboard.backboardScores.keyName, + Assets.images.backboard.backboardGameOver.keyName, + Assets.images.backboard.display.keyName, + Assets.images.boardBackground.keyName, + Assets.images.ball.ball.keyName, + Assets.images.ball.flameEffect.keyName, + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.boundary.bottom.keyName, + Assets.images.boundary.outer.keyName, + Assets.images.boundary.outerBottom.keyName, + Assets.images.dino.animatronic.mouth.keyName, + Assets.images.dino.animatronic.head.keyName, + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, + Assets.images.dash.animatronic.keyName, Assets.images.dash.bumper.a.active.keyName, Assets.images.dash.bumper.a.inactive.keyName, Assets.images.dash.bumper.b.active.keyName, Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.animatronic.keyName, + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + Assets.images.googleWord.letter1.keyName, + Assets.images.googleWord.letter2.keyName, + Assets.images.googleWord.letter3.keyName, + Assets.images.googleWord.letter4.keyName, + Assets.images.googleWord.letter5.keyName, + Assets.images.googleWord.letter6.keyName, + Assets.images.kicker.left.lit.keyName, + Assets.images.kicker.left.dimmed.keyName, + Assets.images.kicker.right.lit.keyName, + Assets.images.kicker.right.dimmed.keyName, + Assets.images.launchRamp.ramp.keyName, + Assets.images.launchRamp.foregroundRailing.keyName, + Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.multiplier.x2.lit.keyName, + Assets.images.multiplier.x2.dimmed.keyName, + Assets.images.multiplier.x3.lit.keyName, + Assets.images.multiplier.x3.dimmed.keyName, + Assets.images.multiplier.x4.lit.keyName, + Assets.images.multiplier.x4.dimmed.keyName, + Assets.images.multiplier.x5.lit.keyName, + Assets.images.multiplier.x5.dimmed.keyName, + Assets.images.multiplier.x6.lit.keyName, + Assets.images.multiplier.x6.dimmed.keyName, + Assets.images.plunger.plunger.keyName, + Assets.images.plunger.rocket.keyName, Assets.images.signpost.inactive.keyName, Assets.images.signpost.active1.keyName, Assets.images.signpost.active2.keyName, Assets.images.signpost.active3.keyName, - Assets.images.alienBumper.a.active.keyName, - Assets.images.alienBumper.a.inactive.keyName, - Assets.images.alienBumper.b.active.keyName, - Assets.images.alienBumper.b.inactive.keyName, - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, - Assets.images.sparky.animatronic.keyName, - Assets.images.spaceship.ramp.boardOpening.keyName, - Assets.images.spaceship.ramp.railingForeground.keyName, - Assets.images.spaceship.ramp.railingBackground.keyName, - Assets.images.spaceship.ramp.main.keyName, - Assets.images.spaceship.ramp.arrow.inactive.keyName, - Assets.images.spaceship.ramp.arrow.active1.keyName, - Assets.images.spaceship.ramp.arrow.active2.keyName, - Assets.images.spaceship.ramp.arrow.active3.keyName, - Assets.images.spaceship.ramp.arrow.active4.keyName, - Assets.images.spaceship.ramp.arrow.active5.keyName, - Assets.images.baseboard.left.keyName, - Assets.images.baseboard.right.keyName, - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - Assets.images.boundary.outer.keyName, - Assets.images.boundary.outerBottom.keyName, - Assets.images.boundary.bottom.keyName, Assets.images.slingshot.upper.keyName, Assets.images.slingshot.lower.keyName, + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.computer.glow.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.bumper.a.lit.keyName, + Assets.images.sparky.bumper.a.dimmed.keyName, + Assets.images.sparky.bumper.b.lit.keyName, + Assets.images.sparky.bumper.b.dimmed.keyName, + Assets.images.sparky.bumper.c.lit.keyName, + Assets.images.sparky.bumper.c.dimmed.keyName, ]; - final flameTester = FlameTester(() => PinballTestGame(assets)); - final debugModeFlameTester = FlameTester(() => DebugPinballTestGame(assets)); + + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameTester = FlameTester( + () => PinballTestGame(assets: assets), + ); + final debugModeFlameTester = FlameTester( + () => DebugPinballTestGame(assets: assets), + ); + + final flameBlocTester = FlameBlocTester( + gameBuilder: () => PinballTestGame(assets: assets), + blocBuilder: () => gameBloc, + ); group('PinballGame', () { - // TODO(alestiago): test if [PinballGame] registers - // [BallScorePointsCallback] once the following issue is resolved: - // https://github.com/flame-engine/flame/issues/1416 group('components', () { - flameTester.test( - 'has only one BottomWall', + // TODO(alestiago): tests that Blueprints get added once the Blueprint + // class is removed. + flameBlocTester.test( + 'has only one Drain', (game) async { await game.ready(); + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameBlocTester.test( + 'has only one BottomGroup', + (game) async { + await game.ready(); expect( - game.children.whereType().length, + game.descendants().whereType().length, equals(1), ); }, ); - flameTester.test( - 'has only one Plunger', + flameBlocTester.test( + 'has only one Launcher', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.descendants().whereType().length, equals(1), ); }, ); - flameTester.test('has one Board', (game) async { + flameBlocTester.test('has one FlutterForest', (game) async { await game.ready(); expect( - game.children.whereType().length, + game.descendants().whereType().length, equals(1), ); }); - flameTester.test( - 'one AlienZone', + flameBlocTester.test( + 'one GoogleWord', (game) async { await game.ready(); - expect(game.children.whereType().length, equals(1)); + expect( + game.descendants().whereType().length, + equals(1), + ); }, ); group('controller', () { - // TODO(alestiago): Write test to be controller agnostic. group('listenWhen', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = GameBloc(); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - // assets: assets, - ); - - flameBlocTester.testGameWidget( - 'listens when all balls are lost and there are more than 0 balls', + flameTester.testGameWidget( + 'listens when all balls are lost and there are more than 0 rounds', setUp: (game, tester) async { + // TODO(ruimiguel): check why testGameWidget doesn't add any ball + // to the game. Test needs to have no balls, so fortunately works. final newState = MockGameState(); - when(() => newState.balls).thenReturn(2); + when(() => newState.isGameOver).thenReturn(false); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), ); @@ -138,10 +208,10 @@ void main() { "doesn't listen when some balls are left", (game) async { final newState = MockGameState(); - when(() => newState.balls).thenReturn(1); + when(() => newState.isGameOver).thenReturn(false); expect( - game.descendants().whereType().length, + game.descendants().whereType().length, greaterThan(0), ); expect( @@ -151,19 +221,20 @@ void main() { }, ); - flameBlocTester.test( - "doesn't listen when no balls left", - (game) async { + flameTester.testGameWidget( + "doesn't listen when game is over", + setUp: (game, tester) async { + // TODO(ruimiguel): check why testGameWidget doesn't add any ball + // to the game. Test needs to have no balls, so fortunately works. final newState = MockGameState(); - when(() => newState.balls).thenReturn(0); - + when(() => newState.isGameOver).thenReturn(true); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), ); await game.ready(); expect( - game.descendants().whereType().isEmpty, + game.descendants().whereType().isEmpty, isTrue, ); expect( @@ -180,14 +251,13 @@ void main() { flameTester.test( 'spawns a ball', (game) async { - await game.ready(); final previousBalls = - game.descendants().whereType().toList(); + game.descendants().whereType().toList(); game.controller.onNewState(MockGameState()); await game.ready(); final currentBalls = - game.descendants().whereType().toList(); + game.descendants().whereType().toList(); expect( currentBalls.length, @@ -199,60 +269,208 @@ void main() { ); }); }); - }); - group('DebugPinballGame', () { - debugModeFlameTester.test('adds a ball on tap up', (game) async { - await game.ready(); + group('flipper control', () { + flameTester.test('tap down moves left flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); - final eventPosition = MockEventPosition(); - when(() => eventPosition.game).thenReturn(Vector2.all(10)); + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - final tapUpEvent = MockTapUpInfo(); - when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); - final previousBalls = game.descendants().whereType().toList(); + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); - game.onTapUp(tapUpEvent); - await game.ready(); + game.onTapDown(tapDownEvent); - expect( - game.children.whereType().length, - equals(previousBalls.length + 1), - ); + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap down moves right flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(game.canvasSize); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.right, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap up moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + + flameTester.test('tap cancel moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + game.onTapCancel(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); }); - group('controller', () { - late GameBloc gameBloc; + group('plunger control', () { + flameTester.test('tap down moves plunger down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - setUp(() { - gameBloc = GameBloc(); + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); }); - final debugModeFlameBlocTester = - FlameBlocTester( - gameBuilder: DebugPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + flameTester.test('tap up releases plunger', (game) async { + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); - debugModeFlameBlocTester.testGameWidget( - 'ignores debug balls', - setUp: (game, tester) async { - final newState = MockGameState(); - when(() => newState.balls).thenReturn(1); + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); - await game.ready(); - game.children.removeWhere((component) => component is Ball); - await game.ready(); - await game.ensureAdd(ControlledBall.debug()); + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); - expect( - game.controller.listenWhen(MockGameState(), newState), - isTrue, - ); - }, - ); + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + + flameTester.test('tap cancel releases plunger', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + + expect(plunger.body.linearVelocity.y, equals(7)); + + game.onTapCancel(); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); }); }); + + group('DebugPinballGame', () { + debugModeFlameTester.test( + 'adds a ball on tap up', + (game) async { + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.all(10)); + + final raw = MockTapUpDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.mouse); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + when(() => tapUpEvent.raw).thenReturn(raw); + + final previousBalls = + game.descendants().whereType().toList(); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect( + game.descendants().whereType().length, + equals(previousBalls.length + 1), + ); + }, + ); + }); } diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart deleted file mode 100644 index cdc56832..00000000 --- a/test/game/view/game_hud_test.dart +++ /dev/null @@ -1,83 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; -import '../../helpers/helpers.dart'; - -void main() { - group('GameHud', () { - late GameBloc gameBloc; - const initialState = GameState( - score: 10, - balls: 2, - bonusHistory: [], - ); - - void _mockState(GameState state) { - whenListen( - gameBloc, - Stream.value(state), - initialState: state, - ); - } - - Future _pumpHud(WidgetTester tester) async { - await tester.pumpApp( - GameHud(), - gameBloc: gameBloc, - ); - } - - setUp(() { - gameBloc = MockGameBloc(); - _mockState(initialState); - }); - - testWidgets( - 'renders the current score', - (tester) async { - await _pumpHud(tester); - expect(find.text(initialState.score.toString()), findsOneWidget); - }, - ); - - testWidgets( - 'renders the current ball number', - (tester) async { - await _pumpHud(tester); - expect( - find.byType(CircleAvatar), - findsNWidgets(initialState.balls), - ); - }, - ); - - testWidgets('updates the score', (tester) async { - await _pumpHud(tester); - expect(find.text(initialState.score.toString()), findsOneWidget); - - _mockState(initialState.copyWith(score: 20)); - - await tester.pump(); - expect(find.text('20'), findsOneWidget); - }); - - testWidgets('updates the ball number', (tester) async { - await _pumpHud(tester); - expect( - find.byType(CircleAvatar), - findsNWidgets(initialState.balls), - ); - - _mockState(initialState.copyWith(balls: 1)); - - await tester.pump(); - expect( - find.byType(CircleAvatar), - findsNWidgets(1), - ); - }); - }); -} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index bbed2963..f8b62d05 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -5,7 +5,8 @@ import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import '../../helpers/helpers.dart'; @@ -13,18 +14,18 @@ void main() { final game = PinballTestGame(); group('PinballGamePage', () { - late ThemeCubit themeCubit; + late CharacterThemeCubit characterThemeCubit; late GameBloc gameBloc; setUp(() async { await Future.wait(game.preLoadAssets()); - themeCubit = MockThemeCubit(); + characterThemeCubit = MockCharacterThemeCubit(); gameBloc = MockGameBloc(); whenListen( - themeCubit, - const Stream.empty(), - initialState: const ThemeState.initial(), + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), ); whenListen( @@ -37,7 +38,7 @@ void main() { testWidgets('renders PinballGameView', (tester) async { await tester.pumpApp( PinballGamePage(), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); expect(find.byType(PinballGameView), findsOneWidget); @@ -62,7 +63,7 @@ void main() { game: game, ), assetsManagerCubit: assetsManagerCubit, - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); expect( @@ -79,6 +80,7 @@ void main() { 'renders PinballGameLoadedView after resources have been loaded', (tester) async { final assetsManagerCubit = MockAssetsManagerCubit(); + final startGameBloc = MockStartGameBloc(); final loadedAssetsState = AssetsManagerState( loadables: [Future.value()], @@ -89,14 +91,20 @@ void main() { Stream.value(loadedAssetsState), initialState: loadedAssetsState, ); + whenListen( + startGameBloc, + Stream.value(StartGameState.initial()), + initialState: StartGameState.initial(), + ); await tester.pumpApp( PinballGameView( game: game, ), assetsManagerCubit: assetsManagerCubit, - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, gameBloc: gameBloc, + startGameBloc: startGameBloc, ); await tester.pump(); @@ -126,7 +134,7 @@ void main() { }, ), ), - themeCubit: themeCubit, + characterThemeCubit: characterThemeCubit, ); await tester.tap(find.text('Tap me')); @@ -160,27 +168,61 @@ void main() { }); group('PinballGameView', () { + final gameBloc = MockGameBloc(); + final startGameBloc = MockStartGameBloc(); + setUp(() async { await Future.wait(game.preLoadAssets()); - }); - testWidgets('renders game and a hud', (tester) async { - final gameBloc = MockGameBloc(); whenListen( gameBloc, Stream.value(const GameState.initial()), initialState: const GameState.initial(), ); + whenListen( + startGameBloc, + Stream.value(StartGameState.initial()), + initialState: StartGameState.initial(), + ); + }); + + testWidgets('renders game', (tester) async { await tester.pumpApp( PinballGameView(game: game), gameBloc: gameBloc, + startGameBloc: startGameBloc, ); expect( find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, ); + // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc + // status + // expect( + // find.byType(GameHud), + // findsNothing, + // ); + }); + + testWidgets('renders a hud on play state', (tester) async { + final startGameState = StartGameState.initial().copyWith( + status: StartGameStatus.play, + ); + + whenListen( + startGameBloc, + Stream.value(startGameState), + initialState: startGameState, + ); + + await tester.pumpApp( + PinballGameView(game: game), + gameBloc: gameBloc, + startGameBloc: startGameBloc, + ); + expect( find.byType(GameHud), findsOneWidget, diff --git a/test/game/view/widgets/bonus_animation_test.dart b/test/game/view/widgets/bonus_animation_test.dart index 9c23ae0d..11e249c7 100644 --- a/test/game/view/widgets/bonus_animation_test.dart +++ b/test/game/view/widgets/bonus_animation_test.dart @@ -1,13 +1,16 @@ -import 'dart:async'; +// ignore_for_file: invalid_use_of_protected_member +import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flame/assets.dart'; -import 'package:flame/widgets.dart'; +import 'package:flame/flame.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball/game/view/widgets/bonus_animation.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import '../../../helpers/helpers.dart'; @@ -15,11 +18,21 @@ class MockImages extends Mock implements Images {} class MockImage extends Mock implements ui.Image {} +class MockCallback extends Mock { + void call(); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() async { - await BonusAnimation.loadAssets(); + // TODO(arturplaczek): need to find for a better solution for loading image + // or use original images from BonusAnimation.loadAssets() + final image = await decodeImageFromList(Uint8List.fromList(fakeImage)); + final images = MockImages(); + when(() => images.fromCache(any())).thenReturn(image); + when(() => images.load(any())).thenAnswer((_) => Future.value(image)); + Flame.images = images; }); group('loads SpriteAnimationWidget correctly for', () { @@ -32,9 +45,9 @@ void main() { expect(find.byType(SpriteAnimationWidget), findsOneWidget); }); - testWidgets('dino', (tester) async { + testWidgets('dinoChomp', (tester) async { await tester.pumpApp( - BonusAnimation.dino(), + BonusAnimation.dinoChomp(), ); await tester.pump(); @@ -50,18 +63,18 @@ void main() { expect(find.byType(SpriteAnimationWidget), findsOneWidget); }); - testWidgets('google', (tester) async { + testWidgets('googleWord', (tester) async { await tester.pumpApp( - BonusAnimation.google(), + BonusAnimation.googleWord(), ); await tester.pump(); expect(find.byType(SpriteAnimationWidget), findsOneWidget); }); - testWidgets('android', (tester) async { + testWidgets('androidSpaceship', (tester) async { await tester.pumpApp( - BonusAnimation.android(), + BonusAnimation.androidSpaceship(), ); await tester.pump(); @@ -74,14 +87,14 @@ void main() { // https://github.com/flame-engine/flame/issues/1543 testWidgets('called onCompleted callback at the end of animation ', (tester) async { - final completer = Completer(); + final callback = MockCallback(); await tester.runAsync(() async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: BonusAnimation.dashNest( - onCompleted: completer.complete, + onCompleted: callback.call, ), ), ), @@ -93,7 +106,38 @@ void main() { await tester.pump(); - expect(completer.isCompleted, isTrue); + verify(callback.call).called(1); + }); + }); + + testWidgets('called onCompleted once when animation changed', (tester) async { + final callback = MockCallback(); + final secondAnimation = BonusAnimation.sparkyTurboCharge( + onCompleted: callback.call, + ); + + await tester.runAsync(() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BonusAnimation.dashNest( + onCompleted: callback.call, + ), + ), + ), + ); + + await tester.pump(); + + tester + .state(find.byType(BonusAnimation)) + .didUpdateWidget(secondAnimation); + + await Future.delayed(const Duration(seconds: 4)); + + await tester.pump(); + + verify(callback.call).called(1); }); }); } diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart new file mode 100644 index 00000000..79cc4f33 --- /dev/null +++ b/test/game/view/widgets/game_hud_test.dart @@ -0,0 +1,157 @@ +// ignore_for_file: prefer_const_constructors + +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/assets.dart'; +import 'package:flame/flame.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart' hide Assets; +import 'package:pinball_ui/pinball_ui.dart'; + +import '../../../helpers/helpers.dart'; + +class MockImages extends Mock implements Images {} + +class MockImage extends Mock implements ui.Image {} + +void main() { + group('GameHud', () { + late GameBloc gameBloc; + + const initialState = GameState( + score: 1000, + multiplier: 1, + rounds: 1, + bonusHistory: [], + ); + + setUp(() async { + gameBloc = MockGameBloc(); + + // TODO(arturplaczek): need to find for a better solution for loading + // image or use original images from BonusAnimation.loadAssets() + final image = await decodeImageFromList(Uint8List.fromList(fakeImage)); + final images = MockImages(); + when(() => images.fromCache(any())).thenReturn(image); + when(() => images.load(any())).thenAnswer((_) => Future.value(image)); + Flame.images = images; + + whenListen( + gameBloc, + Stream.value(initialState), + initialState: initialState, + ); + }); + + // We cannot use pumpApp when we are testing animation because + // animation tests needs to be run and check in tester.runAsync + Future _pumpAppWithWidget(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + theme: PinballTheme.standard, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: BlocProvider.value( + value: gameBloc, + child: GameHud(), + ), + ), + ), + ); + } + + group('renders ScoreView widget', () { + testWidgets( + 'with the score', + (tester) async { + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + + expect(find.text(initialState.score.formatScore()), findsOneWidget); + }, + ); + + testWidgets( + 'on game over', + (tester) async { + final state = initialState.copyWith( + bonusHistory: [GameBonus.dashNest], + balls: 0, + ); + + whenListen( + gameBloc, + Stream.value(state), + initialState: initialState, + ); + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + + expect(find.byType(ScoreView), findsOneWidget); + expect(find.byType(BonusAnimation), findsNothing); + }, + ); + }); + + for (final gameBonus in GameBonus.values) { + testWidgets('renders BonusAnimation for $gameBonus', (tester) async { + await tester.runAsync(() async { + final state = initialState.copyWith( + bonusHistory: [gameBonus], + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: initialState, + ); + + await _pumpAppWithWidget(tester); + await tester.pump(); + + expect(find.byType(BonusAnimation), findsOneWidget); + }); + }); + } + + testWidgets( + 'goes back to ScoreView after the animation', + (tester) async { + await tester.runAsync(() async { + final state = initialState.copyWith( + bonusHistory: [GameBonus.dashNest], + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: initialState, + ); + + await _pumpAppWithWidget(tester); + await tester.pump(); + // TODO(arturplaczek): remove magic number once this is merged: + // https://github.com/flame-engine/flame/pull/1564 + await Future.delayed(const Duration(seconds: 4)); + + await expectLater(find.byType(ScoreView), findsOneWidget); + }); + }, + ); + }); +} diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 210cc347..2229f4b5 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,7 +1,8 @@ +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; import '../../../helpers/helpers.dart'; @@ -9,37 +10,46 @@ void main() { group('PlayButtonOverlay', () { late PinballGame game; late GameFlowController gameFlowController; + late CharacterThemeCubit characterThemeCubit; setUp(() { game = MockPinballGame(); gameFlowController = MockGameFlowController(); - + characterThemeCubit = MockCharacterThemeCubit(); + whenListen( + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), + ); + when(() => characterThemeCubit.state) + .thenReturn(const CharacterThemeState.initial()); when(() => game.gameFlowController).thenReturn(gameFlowController); when(gameFlowController.start).thenAnswer((_) {}); }); testWidgets('renders correctly', (tester) async { await tester.pumpApp(PlayButtonOverlay(game: game)); - expect(find.text('Play'), findsOneWidget); }); - testWidgets('calls gameFlowController.start when taped', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - + testWidgets('calls gameFlowController.start when tapped', (tester) async { + await tester.pumpApp( + PlayButtonOverlay(game: game), + characterThemeCubit: characterThemeCubit, + ); await tester.tap(find.text('Play')); await tester.pump(); - verify(gameFlowController.start).called(1); }); testWidgets('displays CharacterSelectionDialog when tapped', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - + await tester.pumpApp( + PlayButtonOverlay(game: game), + characterThemeCubit: characterThemeCubit, + ); await tester.tap(find.text('Play')); - await tester.pump(); - + await tester.pumpAndSettle(); expect(find.byType(CharacterSelectionDialog), findsOneWidget); }); }); diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart new file mode 100644 index 00000000..335a1c32 --- /dev/null +++ b/test/game/view/widgets/round_count_display_test.dart @@ -0,0 +1,133 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + group('RoundCountDisplay renders', () { + late GameBloc gameBloc; + const initialState = GameState( + score: 0, + multiplier: 1, + rounds: 3, + bonusHistory: [], + ); + + setUp(() { + gameBloc = MockGameBloc(); + + whenListen( + gameBloc, + Stream.value(initialState), + initialState: initialState, + ); + }); + + testWidgets('three active round indicator', (tester) async { + await tester.pumpApp( + const RoundCountDisplay(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.byType(RoundIndicator), findsNWidgets(3)); + }); + + testWidgets('two active round indicator', (tester) async { + final state = initialState.copyWith( + rounds: 2, + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp( + const RoundCountDisplay(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && widget.isActive, + ), + findsNWidgets(2), + ); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && !widget.isActive, + ), + findsOneWidget, + ); + }); + + testWidgets('one active round indicator', (tester) async { + final state = initialState.copyWith( + rounds: 1, + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp( + const RoundCountDisplay(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && widget.isActive, + ), + findsOneWidget, + ); + + expect( + find.byWidgetPredicate( + (widget) => widget is RoundIndicator && !widget.isActive, + ), + findsNWidgets(2), + ); + }); + }); + + testWidgets('active round indicator is displaying with proper color', + (tester) async { + await tester.pumpApp( + const RoundIndicator(isActive: true), + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is Container && widget.color == PinballColors.yellow, + ), + findsOneWidget, + ); + }); + + testWidgets('inactive round indicator is displaying with proper color', + (tester) async { + await tester.pumpApp( + const RoundIndicator(isActive: false), + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is Container && + widget.color == PinballColors.yellow.withAlpha(128), + ), + findsOneWidget, + ); + }); +} diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart new file mode 100644 index 00000000..63f7d1c5 --- /dev/null +++ b/test/game/view/widgets/score_view_test.dart @@ -0,0 +1,82 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + late GameBloc gameBloc; + late StreamController stateController; + const score = 123456789; + const initialState = GameState( + score: score, + multiplier: 1, + rounds: 1, + bonusHistory: [], + ); + + setUp(() { + gameBloc = MockGameBloc(); + stateController = StreamController()..add(initialState); + + whenListen( + gameBloc, + stateController.stream, + initialState: initialState, + ); + }); + + group('ScoreView', () { + testWidgets('renders score', (tester) async { + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.text(score.formatScore()), findsOneWidget); + }); + + testWidgets('renders game over', (tester) async { + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + stateController.add( + initialState.copyWith( + rounds: 0, + ), + ); + + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.text(l10n.gameOver), findsOneWidget); + }); + + testWidgets('updates the score', (tester) async { + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + + expect(find.text(score.formatScore()), findsOneWidget); + + final newState = initialState.copyWith( + score: 987654321, + ); + + stateController.add(newState); + + await tester.pump(); + + expect(find.text(newState.score.formatScore()), findsOneWidget); + }); + }); +} diff --git a/test/helpers/fakes.dart b/test/helpers/fakes.dart index 706733a1..d782ede4 100644 --- a/test/helpers/fakes.dart +++ b/test/helpers/fakes.dart @@ -5,3 +5,70 @@ import 'package:pinball/game/game.dart'; class FakeContact extends Fake implements Contact {} class FakeGameEvent extends Fake implements GameEvent {} + +const fakeImage = [ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x0D, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, + 0x1F, + 0x15, + 0xC4, + 0x89, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x49, + 0x44, + 0x41, + 0x54, + 0x78, + 0x9C, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, + 0x0D, + 0x0A, + 0x2D, + 0xB4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4E, + 0x44, + 0xAE, +]; diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 8732035a..58b4b126 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -12,3 +12,4 @@ export 'mocks.dart'; export 'navigator.dart'; export 'pump_app.dart'; export 'test_games.dart'; +export 'text_span.dart'; diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 9b0f67c9..1d3ad3c7 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -1,22 +1,23 @@ +import 'package:authentication_repository/authentication_repository.dart'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_ui/pinball_ui.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; class MockPinballGame extends Mock implements PinballGame {} -class MockWall extends Mock implements Wall {} - -class MockBottomWall extends Mock implements BottomWall {} +class MockDrain extends Mock implements Drain {} class MockBody extends Mock implements Body {} @@ -28,16 +29,16 @@ class MockBallController extends Mock implements BallController {} class MockContact extends Mock implements Contact {} -class MockContactCallback extends Mock - implements ContactCallback {} - class MockGameBloc extends Mock implements GameBloc {} +class MockStartGameBloc extends Mock implements StartGameBloc {} + class MockGameState extends Mock implements GameState {} -class MockThemeCubit extends Mock implements ThemeCubit {} +class MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} -class MockLeaderboardBloc extends Mock implements LeaderboardBloc {} +class MockAuthenticationRepository extends Mock + implements AuthenticationRepository {} class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} @@ -55,14 +56,22 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { } } +class MockTapDownInfo extends Mock implements TapDownInfo {} + +class MockTapDownDetails extends Mock implements TapDownDetails {} + class MockTapUpInfo extends Mock implements TapUpInfo {} +class MockTapUpDetails extends Mock implements TapUpDetails {} + class MockEventPosition extends Mock implements EventPosition {} class MockFilter extends Mock implements Filter {} class MockFixture extends Mock implements Fixture {} +class MockComponent extends Mock implements Component {} + class MockComponentSet extends Mock implements ComponentSet {} class MockDashNestBumper extends Mock implements DashNestBumper {} @@ -82,6 +91,16 @@ class MockActiveOverlaysNotifier extends Mock class MockGameFlowController extends Mock implements GameFlowController {} -class MockAlienBumper extends Mock implements AlienBumper {} +class MockAndroidBumper extends Mock implements AndroidBumper {} class MockSparkyBumper extends Mock implements SparkyBumper {} + +class MockMultiplier extends Mock implements Multiplier {} + +class MockMultipliersGroup extends Mock implements Multipliers {} + +class MockMultiplierCubit extends Mock implements MultiplierCubit {} + +class MockUrlLauncher extends Mock + with MockPlatformInterfaceMixin + implements UrlLauncherPlatform {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 92e2c042..be67d4d0 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -14,8 +14,10 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_ui/pinball_ui.dart'; import 'helpers.dart'; @@ -51,8 +53,9 @@ extension PumpApp on WidgetTester { Widget widget, { MockNavigator? navigator, GameBloc? gameBloc, + StartGameBloc? startGameBloc, AssetsManagerCubit? assetsManagerCubit, - ThemeCubit? themeCubit, + CharacterThemeCubit? characterThemeCubit, LeaderboardRepository? leaderboardRepository, PinballAudio? pinballAudio, }) { @@ -70,16 +73,20 @@ extension PumpApp on WidgetTester { child: MultiBlocProvider( providers: [ BlocProvider.value( - value: themeCubit ?? MockThemeCubit(), + value: characterThemeCubit ?? MockCharacterThemeCubit(), ), BlocProvider.value( value: gameBloc ?? MockGameBloc(), ), + BlocProvider.value( + value: startGameBloc ?? MockStartGameBloc(), + ), BlocProvider.value( value: assetsManagerCubit ?? _buildDefaultAssetsManagerCubit(), ), ], child: MaterialApp( + theme: PinballTheme.standard, localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, diff --git a/test/helpers/test_games.dart b/test/helpers/test_games.dart index 10caa768..baa466b8 100644 --- a/test/helpers/test_games.dart +++ b/test/helpers/test_games.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'helpers.dart'; @@ -16,13 +17,14 @@ class TestGame extends Forge2DGame with FlameBloc { } class PinballTestGame extends PinballGame { - PinballTestGame([List? assets]) - : _assets = assets, + PinballTestGame({ + List? assets, + PinballAudio? audio, + CharacterTheme? theme, + }) : _assets = assets, super( - audio: MockPinballAudio(), - theme: const PinballTheme( - characterTheme: DashTheme(), - ), + audio: audio ?? MockPinballAudio(), + characterTheme: theme ?? const DashTheme(), ); final List? _assets; @@ -36,13 +38,14 @@ class PinballTestGame extends PinballGame { } class DebugPinballTestGame extends DebugPinballGame { - DebugPinballTestGame([List? assets]) - : _assets = assets, + DebugPinballTestGame({ + List? assets, + PinballAudio? audio, + CharacterTheme? theme, + }) : _assets = assets, super( - audio: MockPinballAudio(), - theme: const PinballTheme( - characterTheme: DashTheme(), - ), + audio: audio ?? MockPinballAudio(), + characterTheme: theme ?? const DashTheme(), ); final List? _assets; @@ -57,7 +60,15 @@ class DebugPinballTestGame extends DebugPinballGame { } class EmptyPinballTestGame extends PinballTestGame { - EmptyPinballTestGame([List? assets]) : super(assets); + EmptyPinballTestGame({ + List? assets, + PinballAudio? audio, + CharacterTheme? theme, + }) : super( + assets: assets, + audio: audio, + theme: theme, + ); @override Future onLoad() async { diff --git a/test/helpers/text_span.dart b/test/helpers/text_span.dart new file mode 100644 index 00000000..c98d33d9 --- /dev/null +++ b/test/helpers/text_span.dart @@ -0,0 +1,17 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +bool tapTextSpan(RichText richText, String text) { + final isTapped = !richText.text.visitChildren( + (visitor) => _findTextAndTap(visitor, text), + ); + return isTapped; +} + +bool _findTextAndTap(InlineSpan visitor, String text) { + if (visitor is TextSpan && visitor.text == text) { + (visitor.recognizer as TapGestureRecognizer?)?.onTap?.call(); + return false; + } + return true; +} diff --git a/test/how_to_play/how_to_play_dialog_test.dart b/test/how_to_play/how_to_play_dialog_test.dart new file mode 100644 index 00000000..24c683a4 --- /dev/null +++ b/test/how_to_play/how_to_play_dialog_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:platform_helper/platform_helper.dart'; + +import '../helpers/helpers.dart'; + +class MockPlatformHelper extends Mock implements PlatformHelper {} + +void main() { + group('HowToPlayDialog', () { + late AppLocalizations l10n; + late PlatformHelper platformHelper; + + setUp(() async { + l10n = await AppLocalizations.delegate.load(const Locale('en')); + platformHelper = MockPlatformHelper(); + }); + + testWidgets( + 'can be instantiated without passing in a platform helper', + (tester) async { + await tester.pumpApp(HowToPlayDialog()); + expect(find.byType(HowToPlayDialog), findsOneWidget); + }, + ); + + testWidgets('displays content for desktop', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => false); + await tester.pumpApp( + HowToPlayDialog( + platformHelper: platformHelper, + ), + ); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.launchControls), findsOneWidget); + expect(find.text(l10n.flipperControls), findsOneWidget); + expect(find.byType(RotatedBox), findsNWidgets(7)); // controls + }); + + testWidgets('displays content for mobile', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => true); + await tester.pumpApp( + HowToPlayDialog( + platformHelper: platformHelper, + ), + ); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.tapAndHoldRocket), findsOneWidget); + expect(find.text(l10n.tapLeftRightScreen), findsOneWidget); + }); + + testWidgets('disappears after 3 seconds', (tester) async { + await tester.pumpApp( + Builder( + builder: (context) { + return TextButton( + onPressed: () => showHowToPlayDialog(context), + child: const Text('test'), + ); + }, + ), + ); + expect(find.byType(HowToPlayDialog), findsNothing); + await tester.tap(find.text('test')); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsOneWidget); + await tester.pump(const Duration(seconds: 4)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }); + }); +} diff --git a/test/leaderboard/bloc/leaderboard_bloc_test.dart b/test/leaderboard/bloc/leaderboard_bloc_test.dart deleted file mode 100644 index 2b217704..00000000 --- a/test/leaderboard/bloc/leaderboard_bloc_test.dart +++ /dev/null @@ -1,203 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('LeaderboardBloc', () { - late LeaderboardRepository leaderboardRepository; - - setUp(() { - leaderboardRepository = MockLeaderboardRepository(); - }); - - test('initial state has state loading no ranking and empty leaderboard', - () { - final bloc = LeaderboardBloc(leaderboardRepository); - expect(bloc.state.status, equals(LeaderboardStatus.loading)); - expect(bloc.state.ranking.ranking, equals(0)); - expect(bloc.state.ranking.outOf, equals(0)); - expect(bloc.state.leaderboard.isEmpty, isTrue); - }); - - group('Top10Fetched', () { - const top10Scores = [ - 2500, - 2200, - 2200, - 2000, - 1800, - 1400, - 1300, - 1000, - 600, - 300, - 100, - ]; - - final top10Leaderboard = top10Scores - .map( - (score) => LeaderboardEntryData( - playerInitials: 'user$score', - score: score, - character: CharacterType.dash, - ), - ) - .toList(); - - blocTest( - 'emits [loading, success] statuses ' - 'when fetchTop10Leaderboard succeeds', - setUp: () { - when(() => leaderboardRepository.fetchTop10Leaderboard()).thenAnswer( - (_) async => top10Leaderboard, - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(Top10Fetched()), - expect: () => [ - LeaderboardState.initial(), - isA() - ..having( - (element) => element.status, - 'status', - equals(LeaderboardStatus.success), - ) - ..having( - (element) => element.leaderboard.length, - 'leaderboard', - equals(top10Leaderboard.length), - ) - ], - verify: (_) => - verify(() => leaderboardRepository.fetchTop10Leaderboard()) - .called(1), - ); - - blocTest( - 'emits [loading, error] statuses ' - 'when fetchTop10Leaderboard fails', - setUp: () { - when(() => leaderboardRepository.fetchTop10Leaderboard()).thenThrow( - Exception(), - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(Top10Fetched()), - expect: () => [ - LeaderboardState.initial(), - LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), - ], - verify: (_) => - verify(() => leaderboardRepository.fetchTop10Leaderboard()) - .called(1), - errors: () => [isA()], - ); - }); - - group('LeaderboardEntryAdded', () { - final leaderboardEntry = LeaderboardEntryData( - playerInitials: 'ABC', - score: 1500, - character: CharacterType.dash, - ); - - final ranking = LeaderboardRanking(ranking: 3, outOf: 4); - - blocTest( - 'emits [loading, success] statuses ' - 'when addLeaderboardEntry succeeds', - setUp: () { - when( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).thenAnswer( - (_) async => ranking, - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(LeaderboardEntryAdded(entry: leaderboardEntry)), - expect: () => [ - LeaderboardState.initial(), - isA() - ..having( - (element) => element.status, - 'status', - equals(LeaderboardStatus.success), - ) - ..having( - (element) => element.ranking, - 'ranking', - equals(ranking), - ) - ], - verify: (_) => verify( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).called(1), - ); - - blocTest( - 'emits [loading, error] statuses ' - 'when addLeaderboardEntry fails', - setUp: () { - when( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).thenThrow( - Exception(), - ); - }, - build: () => LeaderboardBloc(leaderboardRepository), - act: (bloc) => bloc.add(LeaderboardEntryAdded(entry: leaderboardEntry)), - expect: () => [ - LeaderboardState.initial(), - LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), - ], - verify: (_) => verify( - () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), - ).called(1), - errors: () => [isA()], - ); - }); - }); - - group('CharacterTypeX', () { - test('converts CharacterType.android to AndroidTheme', () { - expect(CharacterType.android.toTheme, equals(AndroidTheme())); - }); - - test('converts CharacterType.dash to DashTheme', () { - expect(CharacterType.dash.toTheme, equals(DashTheme())); - }); - - test('converts CharacterType.dino to DinoTheme', () { - expect(CharacterType.dino.toTheme, equals(DinoTheme())); - }); - - test('converts CharacterType.sparky to SparkyTheme', () { - expect(CharacterType.sparky.toTheme, equals(SparkyTheme())); - }); - }); - - group('CharacterThemeX', () { - test('converts AndroidTheme to CharacterType.android', () { - expect(AndroidTheme().toType, equals(CharacterType.android)); - }); - - test('converts DashTheme to CharacterType.dash', () { - expect(DashTheme().toType, equals(CharacterType.dash)); - }); - - test('converts DinoTheme to CharacterType.dino', () { - expect(DinoTheme().toType, equals(CharacterType.dino)); - }); - - test('converts SparkyTheme to CharacterType.sparky', () { - expect(SparkyTheme().toType, equals(CharacterType.sparky)); - }); - }); -} diff --git a/test/leaderboard/bloc/leaderboard_event_test.dart b/test/leaderboard/bloc/leaderboard_event_test.dart deleted file mode 100644 index 33199ca1..00000000 --- a/test/leaderboard/bloc/leaderboard_event_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; - -void main() { - group('GameEvent', () { - group('Top10Fetched', () { - test('can be instantiated', () { - expect(const Top10Fetched(), isNotNull); - }); - - test('supports value equality', () { - expect( - Top10Fetched(), - equals(const Top10Fetched()), - ); - }); - }); - - group('LeaderboardEntryAdded', () { - const leaderboardEntry = LeaderboardEntryData( - playerInitials: 'ABC', - score: 1500, - character: CharacterType.dash, - ); - - test('can be instantiated', () { - expect(const LeaderboardEntryAdded(entry: leaderboardEntry), isNotNull); - }); - - test('supports value equality', () { - expect( - LeaderboardEntryAdded(entry: leaderboardEntry), - equals(const LeaderboardEntryAdded(entry: leaderboardEntry)), - ); - }); - }); - }); -} diff --git a/test/leaderboard/bloc/leaderboard_state_test.dart b/test/leaderboard/bloc/leaderboard_state_test.dart deleted file mode 100644 index 1b5d41d9..00000000 --- a/test/leaderboard/bloc/leaderboard_state_test.dart +++ /dev/null @@ -1,72 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -void main() { - group('LeaderboardState', () { - test('supports value equality', () { - expect( - LeaderboardState.initial(), - equals( - LeaderboardState.initial(), - ), - ); - }); - - group('constructor', () { - test('can be instantiated', () { - expect( - LeaderboardState.initial(), - isNotNull, - ); - }); - }); - - group('copyWith', () { - final leaderboardEntry = LeaderboardEntry( - rank: '1', - playerInitials: 'ABC', - score: 1500, - character: DashTheme().leaderboardIcon, - ); - - test( - 'copies correctly ' - 'when no argument specified', - () { - const leaderboardState = LeaderboardState.initial(); - expect( - leaderboardState.copyWith(), - equals(leaderboardState), - ); - }, - ); - - test( - 'copies correctly ' - 'when all arguments specified', - () { - const leaderboardState = LeaderboardState.initial(); - final otherLeaderboardState = LeaderboardState( - status: LeaderboardStatus.success, - ranking: LeaderboardRanking(ranking: 0, outOf: 0), - leaderboard: [leaderboardEntry], - ); - expect(leaderboardState, isNot(equals(otherLeaderboardState))); - - expect( - leaderboardState.copyWith( - status: otherLeaderboardState.status, - ranking: otherLeaderboardState.ranking, - leaderboard: otherLeaderboardState.leaderboard, - ), - equals(otherLeaderboardState), - ); - }, - ); - }); - }); -} diff --git a/test/leaderboard/view/leaderboard_page_test.dart b/test/leaderboard/view/leaderboard_page_test.dart deleted file mode 100644 index daacb4a7..00000000 --- a/test/leaderboard/view/leaderboard_page_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('LeaderboardPage', () { - testWidgets('renders LeaderboardView', (tester) async { - await tester.pumpApp( - LeaderboardPage( - theme: DashTheme(), - ), - ); - - expect(find.byType(LeaderboardView), findsOneWidget); - }); - - testWidgets('route returns a valid navigation route', (tester) async { - await expectNavigatesToRoute( - tester, - LeaderboardPage.route( - theme: DashTheme(), - ), - ); - }); - }); - - group('LeaderboardView', () { - late LeaderboardBloc leaderboardBloc; - - setUp(() { - leaderboardBloc = MockLeaderboardBloc(); - }); - - testWidgets('renders correctly', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial(), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.text(l10n.leaderboard), findsOneWidget); - expect(find.text(l10n.retry), findsOneWidget); - }); - - testWidgets('renders loading view when bloc emits [loading]', - (tester) async { - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial(), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - expect(find.text('There was en error loading data!'), findsNothing); - expect(find.byType(ListView), findsNothing); - }); - - testWidgets('renders error view when bloc emits [error]', (tester) async { - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial().copyWith( - status: LeaderboardStatus.error, - ), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.text('There was en error loading data!'), findsOneWidget); - expect(find.byType(ListView), findsNothing); - }); - - testWidgets('renders success view when bloc emits [success]', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState( - status: LeaderboardStatus.success, - ranking: LeaderboardRanking(ranking: 0, outOf: 0), - leaderboard: [ - LeaderboardEntry( - rank: '1', - playerInitials: 'ABC', - score: 10000, - character: DashTheme().leaderboardIcon, - ), - ], - ), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: LeaderboardView( - theme: DashTheme(), - ), - ), - ); - - expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.text('There was en error loading data!'), findsNothing); - expect(find.text(l10n.rank), findsOneWidget); - expect(find.text(l10n.character), findsOneWidget); - expect(find.text(l10n.username), findsOneWidget); - expect(find.text(l10n.score), findsOneWidget); - expect(find.byType(ListView), findsOneWidget); - }); - - testWidgets('navigates to CharacterSelectionPage when retry is tapped', - (tester) async { - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); - - await tester.pumpApp( - LeaderboardPage( - theme: DashTheme(), - ), - navigator: navigator, - ); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - - verify(() => navigator.push(any())).called(1); - }); - }); -} diff --git a/test/select_character/cubit/character_theme_cubit_test.dart b/test/select_character/cubit/character_theme_cubit_test.dart new file mode 100644 index 00000000..967eb1e1 --- /dev/null +++ b/test/select_character/cubit/character_theme_cubit_test.dart @@ -0,0 +1,25 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +void main() { + group('CharacterThemeCubit', () { + test('initial state has Dash character theme', () { + final characterThemeCubit = CharacterThemeCubit(); + expect( + characterThemeCubit.state.characterTheme, + equals(const DashTheme()), + ); + }); + + blocTest( + 'charcterSelected emits selected character theme', + build: CharacterThemeCubit.new, + act: (bloc) => bloc.characterSelected(const SparkyTheme()), + expect: () => [ + const CharacterThemeState(SparkyTheme()), + ], + ); + }); +} diff --git a/test/theme/cubit/theme_state_test.dart b/test/select_character/cubit/character_theme_state_test.dart similarity index 54% rename from test/theme/cubit/theme_state_test.dart rename to test/select_character/cubit/character_theme_state_test.dart index 49a2a387..c0d584e2 100644 --- a/test/theme/cubit/theme_state_test.dart +++ b/test/select_character/cubit/character_theme_state_test.dart @@ -1,18 +1,18 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/theme/theme.dart'; +import 'package:pinball/select_character/select_character.dart'; void main() { group('ThemeState', () { test('can be instantiated', () { - expect(const ThemeState.initial(), isNotNull); + expect(const CharacterThemeState.initial(), isNotNull); }); test('supports value equality', () { expect( - ThemeState.initial(), - equals(const ThemeState.initial()), + CharacterThemeState.initial(), + equals(const CharacterThemeState.initial()), ); }); }); diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart new file mode 100644 index 00000000..b9c95f7f --- /dev/null +++ b/test/select_character/view/character_selection_page_test.dart @@ -0,0 +1,72 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_theme/pinball_theme.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + late CharacterThemeCubit characterThemeCubit; + + setUp(() { + characterThemeCubit = MockCharacterThemeCubit(); + whenListen( + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), + ); + when(() => characterThemeCubit.state) + .thenReturn(const CharacterThemeState.initial()); + }); + + group('CharacterSelectionDialog', () { + group('showCharacterSelectionDialog', () { + testWidgets('inflates the dialog', (tester) async { + await tester.pumpApp( + Builder( + builder: (context) { + return TextButton( + onPressed: () => showCharacterSelectionDialog(context), + child: const Text('test'), + ); + }, + ), + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.text('test')); + await tester.pumpAndSettle(); + expect(find.byType(CharacterSelectionDialog), findsOneWidget); + }); + }); + + testWidgets('selecting a new character calls characterSelected on cubit', + (tester) async { + await tester.pumpApp( + const CharacterSelectionDialog(), + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.byKey(const Key('sparky_character_selection'))); + await tester.pumpAndSettle(); + verify( + () => characterThemeCubit.characterSelected(const SparkyTheme()), + ).called(1); + }); + + testWidgets( + 'tapping the select button dismisses the character ' + 'dialog and shows the how to play dialog', (tester) async { + await tester.pumpApp( + const CharacterSelectionDialog(), + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.byType(PinballButton)); + await tester.pumpAndSettle(); + expect(find.byType(CharacterSelectionDialog), findsNothing); + expect(find.byType(HowToPlayDialog), findsOneWidget); + }); + }); +} diff --git a/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart deleted file mode 100644 index 082f102e..00000000 --- a/test/start_game/widgets/how_to_play_dialog_test.dart +++ /dev/null @@ -1,39 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/start_game/start_game.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('HowToPlayDialog', () { - testWidgets('displays dialog', (tester) async { - await tester.pumpApp(HowToPlayDialog()); - - expect(find.byType(Dialog), findsOneWidget); - }); - }); - - group('KeyIndicator', () { - testWidgets('fromKeyName renders correctly', (tester) async { - const keyName = 'A'; - - await tester.pumpApp( - KeyIndicator.fromKeyName(keyName: keyName), - ); - - expect(find.text(keyName), findsOneWidget); - }); - - testWidgets('fromIcon renders correctly', (tester) async { - const keyIcon = Icons.keyboard_arrow_down; - - await tester.pumpApp( - KeyIndicator.fromIcon(keyIcon: keyIcon), - ); - - expect(find.byIcon(keyIcon), findsOneWidget); - }); - }); -} diff --git a/test/theme/cubit/theme_cubit_test.dart b/test/theme/cubit/theme_cubit_test.dart deleted file mode 100644 index 1f2d24e0..00000000 --- a/test/theme/cubit/theme_cubit_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/theme/theme.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -void main() { - group('ThemeCubit', () { - test('initial state has Dash character theme', () { - final themeCubit = ThemeCubit(); - expect(themeCubit.state.theme.characterTheme, equals(const DashTheme())); - }); - - blocTest( - 'charcterSelected emits selected character theme', - build: ThemeCubit.new, - act: (bloc) => bloc.characterSelected(const SparkyTheme()), - expect: () => [ - const ThemeState(PinballTheme(characterTheme: SparkyTheme())), - ], - ); - }); -} diff --git a/test/theme/view/character_selection_page_test.dart b/test/theme/view/character_selection_page_test.dart deleted file mode 100644 index dcf54a13..00000000 --- a/test/theme/view/character_selection_page_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/start_game/start_game.dart'; -import 'package:pinball/theme/theme.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - late ThemeCubit themeCubit; - - setUp(() { - themeCubit = MockThemeCubit(); - whenListen( - themeCubit, - const Stream.empty(), - initialState: const ThemeState.initial(), - ); - }); - - group('CharacterSelectionPage', () { - testWidgets('renders CharacterSelectionView', (tester) async { - await tester.pumpApp( - CharacterSelectionDialog(), - themeCubit: themeCubit, - ); - expect(find.byType(CharacterSelectionView), findsOneWidget); - }); - - testWidgets('route returns a valid navigation route', (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context) - .push(CharacterSelectionDialog.route()); - }, - child: Text('Tap me'), - ); - }, - ), - ), - themeCubit: themeCubit, - ); - - await tester.tap(find.text('Tap me')); - await tester.pumpAndSettle(); - - expect(find.byType(CharacterSelectionDialog), findsOneWidget); - }); - }); - - group('CharacterSelectionView', () { - testWidgets('renders correctly', (tester) async { - const titleText = 'Choose your character!'; - await tester.pumpApp( - CharacterSelectionView(), - themeCubit: themeCubit, - ); - - expect(find.text(titleText), findsOneWidget); - expect(find.byType(CharacterImageButton), findsNWidgets(4)); - expect(find.byType(TextButton), findsOneWidget); - }); - - testWidgets('calls characterSelected when a character image is tapped', - (tester) async { - const sparkyButtonKey = Key('characterSelectionPage_sparkyButton'); - - await tester.pumpApp( - CharacterSelectionView(), - themeCubit: themeCubit, - ); - - await tester.tap(find.byKey(sparkyButtonKey)); - - verify(() => themeCubit.characterSelected(SparkyTheme())).called(1); - }); - - testWidgets('displays how to play dialog when start is tapped', - (tester) async { - await tester.pumpApp( - CharacterSelectionView(), - themeCubit: themeCubit, - ); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - - expect(find.byType(HowToPlayDialog), findsOneWidget); - }); - }); - - testWidgets('CharacterImageButton renders correctly', (tester) async { - await tester.pumpApp( - CharacterImageButton(DashTheme()), - themeCubit: themeCubit, - ); - - expect(find.byType(Image), findsOneWidget); - }); -} diff --git a/web/__/firebase/8.10.1/firebase-app.js b/web/__/firebase/8.10.1/firebase-app.js new file mode 100644 index 00000000..c688d1c4 --- /dev/null +++ b/web/__/firebase/8.10.1/firebase-app.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).firebase=t()}(this,function(){"use strict";var r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)};var n=function(){return(n=Object.assign||function(e){for(var t,n=1,r=arguments.length;na[0]&&t[1]=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function f(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||0"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new c(o,e,i)},v);function v(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var m=/\{\$([^}]+)}/g;function y(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function g(e,t){t=new b(e,t);return t.subscribe.bind(t)}var b=(I.prototype.next=function(t){this.forEachObserver(function(e){e.next(t)})},I.prototype.error=function(t){this.forEachObserver(function(e){e.error(t)}),this.close(t)},I.prototype.complete=function(){this.forEachObserver(function(e){e.complete()}),this.close()},I.prototype.subscribe=function(e,t,n){var r,i=this;if(void 0===e&&void 0===t&&void 0===n)throw new Error("Missing Observer.");void 0===(r=function(e,t){if("object"!=typeof e||null===e)return!1;for(var n=0,r=t;n=(null!=o?o:e.logLevel)&&a({level:R[t].toLowerCase(),message:i,args:n,type:e.name})}}(n[e])}var H=((H={})["no-app"]="No Firebase App '{$appName}' has been created - call Firebase App.initializeApp()",H["bad-app-name"]="Illegal App name: '{$appName}",H["duplicate-app"]="Firebase App named '{$appName}' already exists",H["app-deleted"]="Firebase App named '{$appName}' already deleted",H["invalid-app-argument"]="firebase.{$appName}() takes either no argument or a Firebase App instance.",H["invalid-log-argument"]="First argument to `onLog` must be null or a function.",H),V=new d("app","Firebase",H),B="@firebase/app",M="[DEFAULT]",U=((H={})[B]="fire-core",H["@firebase/analytics"]="fire-analytics",H["@firebase/app-check"]="fire-app-check",H["@firebase/auth"]="fire-auth",H["@firebase/database"]="fire-rtdb",H["@firebase/functions"]="fire-fn",H["@firebase/installations"]="fire-iid",H["@firebase/messaging"]="fire-fcm",H["@firebase/performance"]="fire-perf",H["@firebase/remote-config"]="fire-rc",H["@firebase/storage"]="fire-gcs",H["@firebase/firestore"]="fire-fst",H["fire-js"]="fire-js",H["firebase-wrapper"]="fire-js-all",H),W=new z("@firebase/app"),G=(Object.defineProperty($.prototype,"automaticDataCollectionEnabled",{get:function(){return this.checkDestroyed_(),this.automaticDataCollectionEnabled_},set:function(e){this.checkDestroyed_(),this.automaticDataCollectionEnabled_=e},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"name",{get:function(){return this.checkDestroyed_(),this.name_},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"options",{get:function(){return this.checkDestroyed_(),this.options_},enumerable:!1,configurable:!0}),$.prototype.delete=function(){var t=this;return new Promise(function(e){t.checkDestroyed_(),e()}).then(function(){return t.firebase_.INTERNAL.removeApp(t.name_),Promise.all(t.container.getProviders().map(function(e){return e.delete()}))}).then(function(){t.isDeleted_=!0})},$.prototype._getService=function(e,t){void 0===t&&(t=M),this.checkDestroyed_();var n=this.container.getProvider(e);return n.isInitialized()||"EXPLICIT"!==(null===(e=n.getComponent())||void 0===e?void 0:e.instantiationMode)||n.initialize(),n.getImmediate({identifier:t})},$.prototype._removeServiceInstance=function(e,t){void 0===t&&(t=M),this.container.getProvider(e).clearInstance(t)},$.prototype._addComponent=function(t){try{this.container.addComponent(t)}catch(e){W.debug("Component "+t.name+" failed to register with FirebaseApp "+this.name,e)}},$.prototype._addOrOverwriteComponent=function(e){this.container.addOrOverwriteComponent(e)},$.prototype.toJSON=function(){return{name:this.name,automaticDataCollectionEnabled:this.automaticDataCollectionEnabled,options:this.options}},$.prototype.checkDestroyed_=function(){if(this.isDeleted_)throw V.create("app-deleted",{appName:this.name_})},$);function $(e,t,n){var r=this;this.firebase_=n,this.isDeleted_=!1,this.name_=t.name,this.automaticDataCollectionEnabled_=t.automaticDataCollectionEnabled||!1,this.options_=h(void 0,e),this.container=new S(t.name),this._addComponent(new O("app",function(){return r},"PUBLIC")),this.firebase_.INTERNAL.components.forEach(function(e){return r._addComponent(e)})}G.prototype.name&&G.prototype.options||G.prototype.delete||console.log("dc");var K="8.10.1";function Y(a){var s={},l=new Map,c={__esModule:!0,initializeApp:function(e,t){void 0===t&&(t={});"object"==typeof t&&null!==t||(t={name:t});var n=t;void 0===n.name&&(n.name=M);t=n.name;if("string"!=typeof t||!t)throw V.create("bad-app-name",{appName:String(t)});if(y(s,t))throw V.create("duplicate-app",{appName:t});n=new a(e,n,c);return s[t]=n},app:u,registerVersion:function(e,t,n){var r=null!==(i=U[e])&&void 0!==i?i:e;n&&(r+="-"+n);var i=r.match(/\s|\//),e=t.match(/\s|\//);i||e?(n=['Unable to register library "'+r+'" with version "'+t+'":'],i&&n.push('library name "'+r+'" contains illegal characters (whitespace or "/")'),i&&e&&n.push("and"),e&&n.push('version name "'+t+'" contains illegal characters (whitespace or "/")'),W.warn(n.join(" "))):o(new O(r+"-version",function(){return{library:r,version:t}},"VERSION"))},setLogLevel:T,onLog:function(e,t){if(null!==e&&"function"!=typeof e)throw V.create("invalid-log-argument");x(e,t)},apps:null,SDK_VERSION:K,INTERNAL:{registerComponent:o,removeApp:function(e){delete s[e]},components:l,useAsService:function(e,t){return"serverAuth"!==t?t:null}}};function u(e){if(!y(s,e=e||M))throw V.create("no-app",{appName:e});return s[e]}function o(n){var e,r=n.name;if(l.has(r))return W.debug("There were multiple attempts to register component "+r+"."),"PUBLIC"===n.type?c[r]:null;l.set(r,n),"PUBLIC"===n.type&&(e=function(e){if("function"!=typeof(e=void 0===e?u():e)[r])throw V.create("invalid-app-argument",{appName:r});return e[r]()},void 0!==n.serviceProps&&h(e,n.serviceProps),c[r]=e,a.prototype[r]=function(){for(var e=[],t=0;t>>0),i=0;function r(t,e,n){return t.call.apply(t.bind,arguments)}function g(e,n,t){if(!e)throw Error();if(2/g,Q=/"/g,tt=/'/g,et=/\x00/g,nt=/[\x00&<>"']/;function it(t,e){return-1!=t.indexOf(e)}function rt(t,e){return t"}else o=void 0===t?"undefined":null===t?"null":typeof t;D("Argument is not a %s (or a non-Element, non-Location mock); got: %s",e,o)}}function dt(t,e){this.a=t===gt&&e||"",this.b=mt}function pt(t){return t instanceof dt&&t.constructor===dt&&t.b===mt?t.a:(D("expected object of type Const, got '"+t+"'"),"type_error:Const")}dt.prototype.ta=!0,dt.prototype.sa=function(){return this.a},dt.prototype.toString=function(){return"Const{"+this.a+"}"};var vt,mt={},gt={};function bt(){if(void 0===vt){var t=null,e=l.trustedTypes;if(e&&e.createPolicy){try{t=e.createPolicy("goog#html",{createHTML:I,createScript:I,createScriptURL:I})}catch(t){l.console&&l.console.error(t.message)}vt=t}else vt=t}return vt}function yt(t,e){this.a=e===At?t:""}function wt(t){return t instanceof yt&&t.constructor===yt?t.a:(D("expected object of type TrustedResourceUrl, got '"+t+"' of type "+d(t)),"type_error:TrustedResourceUrl")}function It(t,n){var e,i=pt(t);if(!Et.test(i))throw Error("Invalid TrustedResourceUrl format: "+i);return t=i.replace(Tt,function(t,e){if(!Object.prototype.hasOwnProperty.call(n,e))throw Error('Found marker, "'+e+'", in format string, "'+i+'", but no valid label mapping found in args: '+JSON.stringify(n));return(t=n[e])instanceof dt?pt(t):encodeURIComponent(String(t))}),e=t,t=bt(),new yt(e=t?t.createScriptURL(e):e,At)}yt.prototype.ta=!0,yt.prototype.sa=function(){return this.a.toString()},yt.prototype.toString=function(){return"TrustedResourceUrl{"+this.a+"}"};var Tt=/%{(\w+)}/g,Et=/^((https:)?\/\/[0-9a-z.:[\]-]+\/|\/[^/\\]|[^:/\\%]+\/|[^:/\\%]*[?#]|about:blank#)/i,At={};function kt(t,e){this.a=e===Dt?t:""}function St(t){return t instanceof kt&&t.constructor===kt?t.a:(D("expected object of type SafeUrl, got '"+t+"' of type "+d(t)),"type_error:SafeUrl")}kt.prototype.ta=!0,kt.prototype.sa=function(){return this.a.toString()},kt.prototype.toString=function(){return"SafeUrl{"+this.a+"}"};var Nt=/^(?:audio\/(?:3gpp2|3gpp|aac|L16|midi|mp3|mp4|mpeg|oga|ogg|opus|x-m4a|x-matroska|x-wav|wav|webm)|font\/\w+|image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp|x-icon)|text\/csv|video\/(?:mpeg|mp4|ogg|webm|quicktime|x-matroska))(?:;\w+=(?:\w+|"[\w;,= ]+"))*$/i,_t=/^data:(.*);base64,[a-z0-9+\/]+=*$/i,Ot=/^(?:(?:https?|mailto|ftp):|[^:/?#]*(?:[/?#]|$))/i;function Ct(t){return t instanceof kt?t:(t="object"==typeof t&&t.ta?t.sa():String(t),t=Ot.test(t)||(e=(t=(t=String(t)).replace(/(%0A|%0D)/g,"")).match(_t))&&Nt.test(e[1])?new kt(t,Dt):null);var e}function Rt(t){return t instanceof kt?t:(t="object"==typeof t&&t.ta?t.sa():String(t),new kt(t=!Ot.test(t)?"about:invalid#zClosurez":t,Dt))}var Dt={},Pt=new kt("about:invalid#zClosurez",Dt);function Lt(t,e,n){this.a=n===xt?t:""}Lt.prototype.ta=!0,Lt.prototype.sa=function(){return this.a.toString()},Lt.prototype.toString=function(){return"SafeHtml{"+this.a+"}"};var xt={};function Mt(t,e,n,i){return t=t instanceof kt?t:Rt(t),e=e||l,n=n instanceof dt?pt(n):n||"",e.open(St(t),n,i,void 0)}function jt(t){for(var e=t.split("%s"),n="",i=Array.prototype.slice.call(arguments,1);i.length&&1")?t.replace(Z,">"):t).indexOf('"')?t.replace(Q,"""):t).indexOf("'")?t.replace(tt,"'"):t).indexOf("\0")&&(t=t.replace(et,"�"))),t}function Vt(t){return Vt[" "](t),t}Vt[" "]=a;var Ft,qt=at("Opera"),Ht=at("Trident")||at("MSIE"),Kt=at("Edge"),Gt=Kt||Ht,Bt=at("Gecko")&&!(it(J.toLowerCase(),"webkit")&&!at("Edge"))&&!(at("Trident")||at("MSIE"))&&!at("Edge"),Wt=it(J.toLowerCase(),"webkit")&&!at("Edge");function Xt(){var t=l.document;return t?t.documentMode:void 0}t:{var Jt="",Yt=(Yt=J,Bt?/rv:([^\);]+)(\)|;)/.exec(Yt):Kt?/Edge\/([\d\.]+)/.exec(Yt):Ht?/\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/.exec(Yt):Wt?/WebKit\/(\S+)/.exec(Yt):qt?/(?:Version)[ \/]?(\S+)/.exec(Yt):void 0);if(Yt&&(Jt=Yt?Yt[1]:""),Ht){Yt=Xt();if(null!=Yt&&Yt>parseFloat(Jt)){Ft=String(Yt);break t}}Ft=Jt}var zt={};function $t(s){return t=s,e=function(){for(var t=0,e=Y(String(Ft)).split("."),n=Y(String(s)).split("."),i=Math.max(e.length,n.length),r=0;0==t&&r"),i=i.join("")),i=ae(n,i),r&&("string"==typeof r?i.className=r:Array.isArray(r)?i.className=r.join(" "):ee(i,r)),2>>0);function ln(e){return v(e)?e:(e[hn]||(e[hn]=function(t){return e.handleEvent(t)}),e[hn])}function fn(){Pe.call(this),this.v=new Je(this),(this.bc=this).hb=null}function dn(t,e,n,i,r){t.v.add(String(e),n,!1,i,r)}function pn(t,e,n,i,r){t.v.add(String(e),n,!0,i,r)}function vn(t,e,n,i){if(!(e=t.v.a[String(e)]))return!0;e=e.concat();for(var r=!0,o=0;o>4&15).toString(16)+(15&t).toString(16)}An.prototype.toString=function(){var t=[],e=this.c;e&&t.push(Pn(e,xn,!0),":");var n=this.a;return!n&&"file"!=e||(t.push("//"),(e=this.l)&&t.push(Pn(e,xn,!0),"@"),t.push(encodeURIComponent(String(n)).replace(/%25([0-9a-fA-F]{2})/g,"%$1")),null!=(n=this.g)&&t.push(":",String(n))),(n=this.f)&&(this.a&&"/"!=n.charAt(0)&&t.push("/"),t.push(Pn(n,"/"==n.charAt(0)?jn:Mn,!0))),(n=this.b.toString())&&t.push("?",n),(n=this.h)&&t.push("#",Pn(n,Vn)),t.join("")},An.prototype.resolve=function(t){var e=new An(this),n=!!t.c;n?kn(e,t.c):n=!!t.l,n?e.l=t.l:n=!!t.a,n?e.a=t.a:n=null!=t.g;var i=t.f;if(n)Sn(e,t.g);else if(n=!!t.f)if("/"!=i.charAt(0)&&(this.a&&!this.f?i="/"+i:-1!=(r=e.f.lastIndexOf("/"))&&(i=e.f.substr(0,r+1)+i)),".."==(r=i)||"."==r)i="";else if(it(r,"./")||it(r,"/.")){for(var i=0==r.lastIndexOf("/",0),r=r.split("/"),o=[],a=0;a2*t.c&&In(t)))}function Gn(t,e){return qn(t),e=Xn(t,e),Tn(t.a.b,e)}function Bn(t,e,n){Kn(t,e),0',t=new Lt(t=(i=bt())?i.createHTML(t):t,0,xt),i=a.document)&&(i.write((o=t)instanceof Lt&&o.constructor===Lt?o.a:(D("expected object of type SafeHtml, got '"+o+"' of type "+d(o)),"type_error:SafeHtml")),i.close())):(a=Mt(e,i,n,a))&&t.noopener&&(a.opener=null),a)try{a.focus()}catch(t){}return a}var oi=/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,ai=/^[^@]+@[^@]+$/;function si(){var e=null;return new fe(function(t){"complete"==l.document.readyState?t():(e=function(){t()},en(window,"load",e))}).o(function(t){throw nn(window,"load",e),t})}function ui(t){return t=t||bi(),!("file:"!==Ei()&&"ionic:"!==Ei()||!t.toLowerCase().match(/iphone|ipad|ipod|android/))}function ci(){var t=l.window;try{return t&&t!=t.top}catch(t){return}}function hi(){return void 0!==l.WorkerGlobalScope&&"function"==typeof l.importScripts}function li(){return Zl.default.INTERNAL.hasOwnProperty("reactNative")?"ReactNative":Zl.default.INTERNAL.hasOwnProperty("node")?"Node":hi()?"Worker":"Browser"}function fi(){var t=li();return"ReactNative"===t||"Node"===t}var di="Firefox",pi="Chrome";function vi(t){var e=t.toLowerCase();return it(e,"opera/")||it(e,"opr/")||it(e,"opios/")?"Opera":it(e,"iemobile")?"IEMobile":it(e,"msie")||it(e,"trident/")?"IE":it(e,"edge/")?"Edge":it(e,"firefox/")?di:it(e,"silk/")?"Silk":it(e,"blackberry")?"Blackberry":it(e,"webos")?"Webos":!it(e,"safari/")||it(e,"chrome/")||it(e,"crios/")||it(e,"android")?!it(e,"chrome/")&&!it(e,"crios/")||it(e,"edge/")?it(e,"android")?"Android":(t=t.match(/([a-zA-Z\d\.]+)\/[a-zA-Z\d\.]*$/))&&2==t.length?t[1]:"Other":pi:"Safari"}var mi={md:"FirebaseCore-web",od:"FirebaseUI-web"};function gi(t,e){e=e||[];var n,i=[],r={};for(n in mi)r[mi[n]]=!0;for(n=0;n>4),64!=a&&(t(o<<4&240|a>>2),64!=s&&t(a<<6&192|s))}}(t,function(t){e.push(t)}),e}function Pr(t){var e=xr(t);if(!(e&&e.sub&&e.iss&&e.aud&&e.exp))throw Error("Invalid JWT");this.h=t,this.a=e.exp,this.i=e.sub,t=Date.now()/1e3,this.g=e.iat||(t>this.a?this.a:t),this.b=e.provider_id||e.firebase&&e.firebase.sign_in_provider||null,this.f=e.firebase&&e.firebase.tenant||null,this.c=!!e.is_anonymous||"anonymous"==this.b}function Lr(t){try{return new Pr(t)}catch(t){return null}}function xr(t){if(!t)return null;if(3!=(t=t.split(".")).length)return null;for(var e=(4-(t=t[1]).length%4)%4,n=0;n>10)),t[n++]=String.fromCharCode(56320+(1023&a))):(r=i[e++],o=i[e++],t[n++]=String.fromCharCode((15&s)<<12|(63&r)<<6|63&o))}return JSON.parse(t.join(""))}catch(t){}return null}Pr.prototype.T=function(){return this.f},Pr.prototype.l=function(){return this.c},Pr.prototype.toString=function(){return this.h};var Mr="oauth_consumer_key oauth_nonce oauth_signature oauth_signature_method oauth_timestamp oauth_token oauth_version".split(" "),jr=["client_id","response_type","scope","redirect_uri","state"],Ur={nd:{Ja:"locale",va:700,ua:600,fa:"facebook.com",Ya:jr},pd:{Ja:null,va:500,ua:750,fa:"github.com",Ya:jr},qd:{Ja:"hl",va:515,ua:680,fa:"google.com",Ya:jr},wd:{Ja:"lang",va:485,ua:705,fa:"twitter.com",Ya:Mr},kd:{Ja:"locale",va:640,ua:600,fa:"apple.com",Ya:[]}};function Vr(t){for(var e in Ur)if(Ur[e].fa==t)return Ur[e];return null}function Fr(t){var e={};e["facebook.com"]=Br,e["google.com"]=Xr,e["github.com"]=Wr,e["twitter.com"]=Jr;var n=t&&t[Hr];try{if(n)return new(e[n]||Gr)(t);if(void 0!==t[qr])return new Kr(t)}catch(t){}return null}var qr="idToken",Hr="providerId";function Kr(t){var e,n=t[Hr];if(n||!t[qr]||(e=Lr(t[qr]))&&e.b&&(n=e.b),!n)throw Error("Invalid additional user info!");e=!1,void 0!==t.isNewUser?e=!!t.isNewUser:"identitytoolkit#SignupNewUserResponse"===t.kind&&(e=!0),Fi(this,"providerId",n="anonymous"==n||"custom"==n?null:n),Fi(this,"isNewUser",e)}function Gr(t){Kr.call(this,t),Fi(this,"profile",Ki((t=Ni(t.rawUserInfo||"{}"))||{}))}function Br(t){if(Gr.call(this,t),"facebook.com"!=this.providerId)throw Error("Invalid provider ID!")}function Wr(t){if(Gr.call(this,t),"github.com"!=this.providerId)throw Error("Invalid provider ID!");Fi(this,"username",this.profile&&this.profile.login||null)}function Xr(t){if(Gr.call(this,t),"google.com"!=this.providerId)throw Error("Invalid provider ID!")}function Jr(t){if(Gr.call(this,t),"twitter.com"!=this.providerId)throw Error("Invalid provider ID!");Fi(this,"username",t.screenName||null)}function Yr(t){var e=On(i=Cn(t),"link"),n=On(Cn(e),"link"),i=On(i,"deep_link_id");return On(Cn(i),"link")||i||n||e||t}function zr(t,e){if(!t&&!e)throw new T("internal-error","Internal assert: no raw session string available");if(t&&e)throw new T("internal-error","Internal assert: unable to determine the session type");this.a=t||null,this.b=e||null,this.type=this.a?$r:Zr}w(Gr,Kr),w(Br,Gr),w(Wr,Gr),w(Xr,Gr),w(Jr,Gr);var $r="enroll",Zr="signin";function Qr(){}function to(t,n){return t.then(function(t){if(t[Ka]){var e=Lr(t[Ka]);if(!e||n!=e.i)throw new T("user-mismatch");return t}throw new T("user-mismatch")}).o(function(t){throw t&&t.code&&t.code==k+"user-not-found"?new T("user-mismatch"):t})}function eo(t,e){if(!e)throw new T("internal-error","failed to construct a credential");this.a=e,Fi(this,"providerId",t),Fi(this,"signInMethod",t)}function no(t){return{pendingToken:t.a,requestUri:"http://localhost"}}function io(t){if(t&&t.providerId&&t.signInMethod&&0==t.providerId.indexOf("saml.")&&t.pendingToken)try{return new eo(t.providerId,t.pendingToken)}catch(t){}return null}function ro(t,e,n){if(this.a=null,e.idToken||e.accessToken)e.idToken&&Fi(this,"idToken",e.idToken),e.accessToken&&Fi(this,"accessToken",e.accessToken),e.nonce&&!e.pendingToken&&Fi(this,"nonce",e.nonce),e.pendingToken&&(this.a=e.pendingToken);else{if(!e.oauthToken||!e.oauthTokenSecret)throw new T("internal-error","failed to construct a credential");Fi(this,"accessToken",e.oauthToken),Fi(this,"secret",e.oauthTokenSecret)}Fi(this,"providerId",t),Fi(this,"signInMethod",n)}function oo(t){var e={};return t.idToken&&(e.id_token=t.idToken),t.accessToken&&(e.access_token=t.accessToken),t.secret&&(e.oauth_token_secret=t.secret),e.providerId=t.providerId,t.nonce&&!t.a&&(e.nonce=t.nonce),e={postBody:Hn(e).toString(),requestUri:"http://localhost"},t.a&&(delete e.postBody,e.pendingToken=t.a),e}function ao(t){if(t&&t.providerId&&t.signInMethod){var e={idToken:t.oauthIdToken,accessToken:t.oauthTokenSecret?null:t.oauthAccessToken,oauthTokenSecret:t.oauthTokenSecret,oauthToken:t.oauthTokenSecret&&t.oauthAccessToken,nonce:t.nonce,pendingToken:t.pendingToken};try{return new ro(t.providerId,e,t.signInMethod)}catch(t){}}return null}function so(t,e){this.Qc=e||[],qi(this,{providerId:t,isOAuthProvider:!0}),this.Jb={},this.qb=(Vr(t)||{}).Ja||null,this.pb=null}function uo(t){if("string"!=typeof t||0!=t.indexOf("saml."))throw new T("argument-error",'SAML provider IDs must be prefixed with "saml."');so.call(this,t,[])}function co(t){so.call(this,t,jr),this.a=[]}function ho(){co.call(this,"facebook.com")}function lo(t){if(!t)throw new T("argument-error","credential failed: expected 1 argument (the OAuth access token).");var e=t;return m(t)&&(e=t.accessToken),(new ho).credential({accessToken:e})}function fo(){co.call(this,"github.com")}function po(t){if(!t)throw new T("argument-error","credential failed: expected 1 argument (the OAuth access token).");var e=t;return m(t)&&(e=t.accessToken),(new fo).credential({accessToken:e})}function vo(){co.call(this,"google.com"),this.Ca("profile")}function mo(t,e){var n=t;return m(t)&&(n=t.idToken,e=t.accessToken),(new vo).credential({idToken:n,accessToken:e})}function go(){so.call(this,"twitter.com",Mr)}function bo(t,e){var n=t;if(!(n=!m(n)?{oauthToken:t,oauthTokenSecret:e}:n).oauthToken||!n.oauthTokenSecret)throw new T("argument-error","credential failed: expected 2 arguments (the OAuth access token and secret).");return new ro("twitter.com",n,"twitter.com")}function yo(t,e,n){this.a=t,this.f=e,Fi(this,"providerId","password"),Fi(this,"signInMethod",n===Io.EMAIL_LINK_SIGN_IN_METHOD?Io.EMAIL_LINK_SIGN_IN_METHOD:Io.EMAIL_PASSWORD_SIGN_IN_METHOD)}function wo(t){return t&&t.email&&t.password?new yo(t.email,t.password,t.signInMethod):null}function Io(){qi(this,{providerId:"password",isOAuthProvider:!1})}function To(t,e){if(!(e=Eo(e)))throw new T("argument-error","Invalid email link!");return new yo(t,e.code,Io.EMAIL_LINK_SIGN_IN_METHOD)}function Eo(t){return(t=yr(t=Yr(t)))&&t.operation===Qi?t:null}function Ao(t){if(!(t.fb&&t.eb||t.La&&t.ea))throw new T("internal-error");this.a=t,Fi(this,"providerId","phone"),this.fa="phone",Fi(this,"signInMethod","phone")}function ko(e){if(e&&"phone"===e.providerId&&(e.verificationId&&e.verificationCode||e.temporaryProof&&e.phoneNumber)){var n={};return V(["verificationId","verificationCode","temporaryProof","phoneNumber"],function(t){e[t]&&(n[t]=e[t])}),new Ao(n)}return null}function So(t){return t.a.La&&t.a.ea?{temporaryProof:t.a.La,phoneNumber:t.a.ea}:{sessionInfo:t.a.fb,code:t.a.eb}}function No(t){try{this.a=t||Zl.default.auth()}catch(t){throw new T("argument-error","Either an instance of firebase.auth.Auth must be passed as an argument to the firebase.auth.PhoneAuthProvider constructor, or the default firebase App instance must be initialized via firebase.initializeApp().")}qi(this,{providerId:"phone",isOAuthProvider:!1})}function _o(t,e){if(!t)throw new T("missing-verification-id");if(!e)throw new T("missing-verification-code");return new Ao({fb:t,eb:e})}function Oo(t){if(t.temporaryProof&&t.phoneNumber)return new Ao({La:t.temporaryProof,ea:t.phoneNumber});var e=t&&t.providerId;if(!e||"password"===e)return null;var n=t&&t.oauthAccessToken,i=t&&t.oauthTokenSecret,r=t&&t.nonce,o=t&&t.oauthIdToken,a=t&&t.pendingToken;try{switch(e){case"google.com":return mo(o,n);case"facebook.com":return lo(n);case"github.com":return po(n);case"twitter.com":return bo(n,i);default:return n||i||o||a?a?0==e.indexOf("saml.")?new eo(e,a):new ro(e,{pendingToken:a,idToken:t.oauthIdToken,accessToken:t.oauthAccessToken},e):new co(e).credential({idToken:o,accessToken:n,rawNonce:r}):null}}catch(t){return null}}function Co(t){if(!t.isOAuthProvider)throw new T("invalid-oauth-provider")}function Ro(t,e,n,i,r,o,a){if(this.c=t,this.b=e||null,this.g=n||null,this.f=i||null,this.i=o||null,this.h=a||null,this.a=r||null,!this.g&&!this.a)throw new T("invalid-auth-event");if(this.g&&this.a)throw new T("invalid-auth-event");if(this.g&&!this.f)throw new T("invalid-auth-event")}function Do(t){return(t=t||{}).type?new Ro(t.type,t.eventId,t.urlResponse,t.sessionId,t.error&&E(t.error),t.postBody,t.tenantId):null}function Po(){this.b=null,this.a=[]}zr.prototype.Ha=function(){return this.a?ye(this.a):ye(this.b)},zr.prototype.w=function(){return this.type==$r?{multiFactorSession:{idToken:this.a}}:{multiFactorSession:{pendingCredential:this.b}}},Qr.prototype.ka=function(){},Qr.prototype.b=function(){},Qr.prototype.c=function(){},Qr.prototype.w=function(){},eo.prototype.ka=function(t){return ls(t,no(this))},eo.prototype.b=function(t,e){var n=no(this);return n.idToken=e,fs(t,n)},eo.prototype.c=function(t,e){return to(ds(t,no(this)),e)},eo.prototype.w=function(){return{providerId:this.providerId,signInMethod:this.signInMethod,pendingToken:this.a}},ro.prototype.ka=function(t){return ls(t,oo(this))},ro.prototype.b=function(t,e){var n=oo(this);return n.idToken=e,fs(t,n)},ro.prototype.c=function(t,e){return to(ds(t,oo(this)),e)},ro.prototype.w=function(){var t={providerId:this.providerId,signInMethod:this.signInMethod};return this.idToken&&(t.oauthIdToken=this.idToken),this.accessToken&&(t.oauthAccessToken=this.accessToken),this.secret&&(t.oauthTokenSecret=this.secret),this.nonce&&(t.nonce=this.nonce),this.a&&(t.pendingToken=this.a),t},so.prototype.Ka=function(t){return this.Jb=ct(t),this},w(uo,so),w(co,so),co.prototype.Ca=function(t){return K(this.a,t)||this.a.push(t),this},co.prototype.Rb=function(){return X(this.a)},co.prototype.credential=function(t,e){e=m(t)?{idToken:t.idToken||null,accessToken:t.accessToken||null,nonce:t.rawNonce||null}:{idToken:t||null,accessToken:e||null};if(!e.idToken&&!e.accessToken)throw new T("argument-error","credential failed: must provide the ID token and/or the access token.");return new ro(this.providerId,e,this.providerId)},w(ho,co),Fi(ho,"PROVIDER_ID","facebook.com"),Fi(ho,"FACEBOOK_SIGN_IN_METHOD","facebook.com"),w(fo,co),Fi(fo,"PROVIDER_ID","github.com"),Fi(fo,"GITHUB_SIGN_IN_METHOD","github.com"),w(vo,co),Fi(vo,"PROVIDER_ID","google.com"),Fi(vo,"GOOGLE_SIGN_IN_METHOD","google.com"),w(go,so),Fi(go,"PROVIDER_ID","twitter.com"),Fi(go,"TWITTER_SIGN_IN_METHOD","twitter.com"),yo.prototype.ka=function(t){return this.signInMethod==Io.EMAIL_LINK_SIGN_IN_METHOD?Js(t,Is,{email:this.a,oobCode:this.f}):Js(t,Ks,{email:this.a,password:this.f})},yo.prototype.b=function(t,e){return this.signInMethod==Io.EMAIL_LINK_SIGN_IN_METHOD?Js(t,Ts,{idToken:e,email:this.a,oobCode:this.f}):Js(t,xs,{idToken:e,email:this.a,password:this.f})},yo.prototype.c=function(t,e){return to(this.ka(t),e)},yo.prototype.w=function(){return{email:this.a,password:this.f,signInMethod:this.signInMethod}},qi(Io,{PROVIDER_ID:"password"}),qi(Io,{EMAIL_LINK_SIGN_IN_METHOD:"emailLink"}),qi(Io,{EMAIL_PASSWORD_SIGN_IN_METHOD:"password"}),Ao.prototype.ka=function(t){return t.gb(So(this))},Ao.prototype.b=function(t,e){var n=So(this);return n.idToken=e,Js(t,Bs,n)},Ao.prototype.c=function(t,e){var n=So(this);return n.operation="REAUTH",to(t=Js(t,Ws,n),e)},Ao.prototype.w=function(){var t={providerId:"phone"};return this.a.fb&&(t.verificationId=this.a.fb),this.a.eb&&(t.verificationCode=this.a.eb),this.a.La&&(t.temporaryProof=this.a.La),this.a.ea&&(t.phoneNumber=this.a.ea),t},No.prototype.gb=function(i,r){var o=this.a.a;return ye(r.verify()).then(function(e){if("string"!=typeof e)throw new T("argument-error","An implementation of firebase.auth.ApplicationVerifier.prototype.verify() must return a firebase.Promise that resolves with a string.");if("recaptcha"!==r.type)throw new T("argument-error",'Only firebase.auth.ApplicationVerifiers with type="recaptcha" are currently supported.');var t=m(i)?i.session:null,n=m(i)?i.phoneNumber:i,t=t&&t.type==$r?t.Ha().then(function(t){return Js(o,js,{idToken:t,phoneEnrollmentInfo:{phoneNumber:n,recaptchaToken:e}}).then(function(t){return t.phoneSessionInfo.sessionInfo})}):t&&t.type==Zr?t.Ha().then(function(t){return t={mfaPendingCredential:t,mfaEnrollmentId:i.multiFactorHint&&i.multiFactorHint.uid||i.multiFactorUid,phoneSignInInfo:{recaptchaToken:e}},Js(o,Us,t).then(function(t){return t.phoneResponseInfo.sessionInfo})}):Js(o,Ps,{phoneNumber:n,recaptchaToken:e});return t.then(function(t){return"function"==typeof r.reset&&r.reset(),t},function(t){throw"function"==typeof r.reset&&r.reset(),t})})},qi(No,{PROVIDER_ID:"phone"}),qi(No,{PHONE_SIGN_IN_METHOD:"phone"}),Ro.prototype.getUid=function(){var t=[];return t.push(this.c),this.b&&t.push(this.b),this.f&&t.push(this.f),this.h&&t.push(this.h),t.join("-")},Ro.prototype.T=function(){return this.h},Ro.prototype.w=function(){return{type:this.c,eventId:this.b,urlResponse:this.g,sessionId:this.f,postBody:this.i,tenantId:this.h,error:this.a&&this.a.w()}};var Lo,xo=null;function Mo(t){var e="unauthorized-domain",n=void 0,i=Cn(t);t=i.a,"chrome-extension"==(i=i.c)?n=jt("This chrome extension ID (chrome-extension://%s) is not authorized to run this operation. Add it to the OAuth redirect domains list in the Firebase console -> Auth section -> Sign in method tab.",t):"http"==i||"https"==i?n=jt("This domain (%s) is not authorized to run this operation. Add it to the OAuth redirect domains list in the Firebase console -> Auth section -> Sign in method tab.",t):e="operation-not-supported-in-this-environment",T.call(this,e,n)}function jo(t,e,n){T.call(this,t,n),(t=e||{}).Kb&&Fi(this,"email",t.Kb),t.ea&&Fi(this,"phoneNumber",t.ea),t.credential&&Fi(this,"credential",t.credential),t.$b&&Fi(this,"tenantId",t.$b)}function Uo(t){if(t.code){var e=t.code||"";0==e.indexOf(k)&&(e=e.substring(k.length));var n={credential:Oo(t),$b:t.tenantId};if(t.email)n.Kb=t.email;else if(t.phoneNumber)n.ea=t.phoneNumber;else if(!n.credential)return new T(e,t.message||void 0);return new jo(e,n,t.message)}return null}function Vo(){}function Fo(t){return t.c||(t.c=t.b())}function qo(){}function Ho(t){if(t.f||"undefined"!=typeof XMLHttpRequest||"undefined"==typeof ActiveXObject)return t.f;for(var e=["MSXML2.XMLHTTP.6.0","MSXML2.XMLHTTP.3.0","MSXML2.XMLHTTP","Microsoft.XMLHTTP"],n=0;n=function t(e){return e.c||(e.a?t(e.a):(D("Root logger has no level set."),null))}(this).value)for(v(e)&&(e=e()),t=new Wo(t,String(e),this.f),n&&(t.a=n),n=this;n;)n=n.a};var Qo,ta={},ea=null;function na(t){var e,n,i;return ea||(ea=new Xo(""),(ta[""]=ea).c=$o),(e=ta[t])||(e=new Xo(t),i=t.lastIndexOf("."),n=t.substr(i+1),(i=na(t.substr(0,i))).b||(i.b={}),(i.b[n]=e).a=i,ta[t]=e),e}function ia(t,e){t&&t.log(Zo,e,void 0)}function ra(t){this.f=t}function oa(t){fn.call(this),this.u=t,this.h=void 0,this.readyState=aa,this.status=0,this.responseType=this.responseText=this.response=this.statusText="",this.onreadystatechange=null,this.l=new Headers,this.b=null,this.s="GET",this.f="",this.a=!1,this.i=na("goog.net.FetchXmlHttp"),this.m=this.c=this.g=null}w(ra,Vo),ra.prototype.a=function(){return new oa(this.f)},ra.prototype.b=(Qo={},function(){return Qo}),w(oa,fn);var aa=0;function sa(t){t.c.read().then(t.pc.bind(t)).catch(t.Va.bind(t))}function ua(t){t.readyState=4,t.g=null,t.c=null,t.m=null,ca(t)}function ca(t){t.onreadystatechange&&t.onreadystatechange.call(t)}function ha(t){fn.call(this),this.headers=new wn,this.D=t||null,this.c=!1,this.C=this.a=null,this.h=this.P=this.l="",this.f=this.N=this.i=this.J=!1,this.g=0,this.s=null,this.m=la,this.u=this.S=!1}(t=oa.prototype).open=function(t,e){if(this.readyState!=aa)throw this.abort(),Error("Error reopening a connection");this.s=t,this.f=e,this.readyState=1,ca(this)},t.send=function(t){if(1!=this.readyState)throw this.abort(),Error("need to call open() first. ");this.a=!0;var e={headers:this.l,method:this.s,credentials:this.h,cache:void 0};t&&(e.body=t),this.u.fetch(new Request(this.f,e)).then(this.uc.bind(this),this.Va.bind(this))},t.abort=function(){this.response=this.responseText="",this.l=new Headers,this.status=0,this.c&&this.c.cancel("Request was aborted."),1<=this.readyState&&this.a&&4!=this.readyState&&(this.a=!1,ua(this)),this.readyState=aa},t.uc=function(t){this.a&&(this.g=t,this.b||(this.status=this.g.status,this.statusText=this.g.statusText,this.b=t.headers,this.readyState=2,ca(this)),this.a&&(this.readyState=3,ca(this),this.a&&("arraybuffer"===this.responseType?t.arrayBuffer().then(this.sc.bind(this),this.Va.bind(this)):void 0!==l.ReadableStream&&"body"in t?(this.response=this.responseText="",this.c=t.body.getReader(),this.m=new TextDecoder,sa(this)):t.text().then(this.tc.bind(this),this.Va.bind(this)))))},t.pc=function(t){var e;this.a&&((e=this.m.decode(t.value||new Uint8Array(0),{stream:!t.done}))&&(this.response=this.responseText+=e),(t.done?ua:ca)(this),3==this.readyState&&sa(this))},t.tc=function(t){this.a&&(this.response=this.responseText=t,ua(this))},t.sc=function(t){this.a&&(this.response=t,ua(this))},t.Va=function(t){var e=this.i;e&&e.log(zo,"Failed to fetch url "+this.f,t instanceof Error?t:Error(t)),this.a&&ua(this)},t.setRequestHeader=function(t,e){this.l.append(t,e)},t.getResponseHeader=function(t){return this.b?this.b.get(t.toLowerCase())||"":((t=this.i)&&t.log(zo,"Attempting to get response header but no headers have been received for url: "+this.f,void 0),"")},t.getAllResponseHeaders=function(){if(!this.b){var t=this.i;return t&&t.log(zo,"Attempting to get all response headers but no headers have been received for url: "+this.f,void 0),""}for(var t=[],e=this.b.entries(),n=e.next();!n.done;)n=n.value,t.push(n[0]+": "+n[1]),n=e.next();return t.join("\r\n")},Object.defineProperty(oa.prototype,"withCredentials",{get:function(){return"include"===this.h},set:function(t){this.h=t?"include":"same-origin"}}),w(ha,fn);var la="";ha.prototype.b=na("goog.net.XhrIo");var fa=/^https?$/i,da=["POST","PUT"];function pa(e,t,n,i,r){if(e.a)throw Error("[goog.net.XhrIo] Object is active with another request="+e.l+"; newUri="+t);n=n?n.toUpperCase():"GET",e.l=t,e.h="",e.P=n,e.J=!1,e.c=!0,e.a=(e.D||Lo).a(),e.C=e.D?Fo(e.D):Fo(Lo),e.a.onreadystatechange=b(e.Wb,e);try{ia(e.b,Ea(e,"Opening Xhr")),e.N=!0,e.a.open(n,String(t),!0),e.N=!1}catch(t){return ia(e.b,Ea(e,"Error opening Xhr: "+t.message)),void ma(e,t)}t=i||"";var o,a=new wn(e.headers);r&&function(t,e){if(t.forEach&&"function"==typeof t.forEach)t.forEach(e,void 0);else if(p(t)||"string"==typeof t)V(t,e,void 0);else for(var n=yn(t),i=bn(t),r=i.length,o=0;o>>7|r<<25)^(r>>>18|r<<14)^r>>>3)|0,a=(0|n[e-7])+((i>>>17|i<<15)^(i>>>19|i<<13)^i>>>10)|0;n[e]=o+a|0}i=0|t.a[0],r=0|t.a[1];var s=0|t.a[2],u=0|t.a[3],c=0|t.a[4],h=0|t.a[5],l=0|t.a[6];for(o=0|t.a[7],e=0;e<64;e++){var f=((i>>>2|i<<30)^(i>>>13|i<<19)^(i>>>22|i<<10))+(i&r^i&s^r&s)|0;a=(o=o+((c>>>6|c<<26)^(c>>>11|c<<21)^(c>>>25|c<<7))|0)+((a=(a=c&h^~c&l)+(0|Zu[e])|0)+(0|n[e])|0)|0,o=l,l=h,h=c,c=u+a|0,u=s,s=r,r=i,i=a+f|0}t.a[0]=t.a[0]+i|0,t.a[1]=t.a[1]+r|0,t.a[2]=t.a[2]+s|0,t.a[3]=t.a[3]+u|0,t.a[4]=t.a[4]+c|0,t.a[5]=t.a[5]+h|0,t.a[6]=t.a[6]+l|0,t.a[7]=t.a[7]+o|0}function uc(t,e,n){void 0===n&&(n=e.length);var i=0,r=t.c;if("string"==typeof e)for(;i>r&255;return q(t,function(t){return 1<(t=t.toString(16)).length?t:"0"+t}).join("")}function vc(t,e){for(var n=0;nt.f&&(t.a=t.f),e)}function oh(t){this.f=t,this.b=this.a=null,this.c=Date.now()}function ah(t,e){void 0===e&&(e=t.b?(e=t.b).a-e.g:0),t.c=Date.now()+1e3*e}function sh(t,e){t.b=Lr(e[Ka]||""),t.a=e.refreshToken,ah(t,void 0!==(e=e.expiresIn)?Number(e):void 0)}function uh(e,t){return i=e.f,r=t,new fe(function(e,n){"refresh_token"==r.grant_type&&r.refresh_token||"authorization_code"==r.grant_type&&r.code?Za(i,i.l+"?key="+encodeURIComponent(i.c),function(t){t?t.error?n(zs(t)):t.access_token&&t.refresh_token?e(t):n(new T("internal-error")):n(new T("network-request-failed"))},"POST",Hn(r).toString(),i.g,i.m.get()):n(new T("internal-error"))}).then(function(t){return e.b=Lr(t.access_token),e.a=t.refresh_token,ah(e,t.expires_in),{accessToken:e.b.toString(),refreshToken:e.a}}).o(function(t){throw"auth/user-token-expired"==t.code&&(e.a=null),t});var i,r}function ch(t,e){this.a=t||null,this.b=e||null,qi(this,{lastSignInTime:Li(e||null),creationTime:Li(t||null)})}function hh(t,e,n,i,r,o){qi(this,{uid:t,displayName:i||null,photoURL:r||null,email:n||null,phoneNumber:o||null,providerId:e})}function lh(t,e,n){this.N=[],this.l=t.apiKey,this.m=t.appName,this.s=t.authDomain||null;var i,r=Zl.default.SDK_VERSION?gi(Zl.default.SDK_VERSION):null;this.a=new qa(this.l,_(A),r),(this.u=t.emulatorConfig||null)&&Ya(this.a,this.u),this.h=new oh(this.a),wh(this,e[Ka]),sh(this.h,e),Fi(this,"refreshToken",this.h.a),Eh(this,n||{}),fn.call(this),this.P=!1,this.s&&Ii()&&(this.b=xc(this.s,this.l,this.m,this.u)),this.W=[],this.i=null,this.D=(i=this,new ih(function(){return i.I(!0)},function(t){return!(!t||"auth/network-request-failed"!=t.code)},function(){var t=i.h.c-Date.now()-3e5;return 0this.c-3e4?this.a?uh(this,{grant_type:"refresh_token",refresh_token:this.a}):ye(null):ye({accessToken:this.b.toString(),refreshToken:this.a})},ch.prototype.w=function(){return{lastLoginAt:this.b,createdAt:this.a}},w(lh,fn),lh.prototype.xa=function(t){this.za=t,Ja(this.a,t)},lh.prototype.la=function(){return this.za},lh.prototype.Ga=function(){return X(this.aa)},lh.prototype.ib=function(){this.D.b&&(this.D.stop(),this.D.start())},Fi(lh.prototype,"providerId","firebase"),(t=lh.prototype).reload=function(){var t=this;return Vh(this,kh(this).then(function(){return Rh(t).then(function(){return Ih(t)}).then(Ah)}))},t.oc=function(t){return this.I(t).then(function(t){return new Bc(t)})},t.I=function(t){var e=this;return Vh(this,kh(this).then(function(){return e.h.getToken(t)}).then(function(t){if(!t)throw new T("internal-error");return t.accessToken!=e.Aa&&(wh(e,t.accessToken),e.dispatchEvent(new th("tokenChanged"))),Oh(e,"refreshToken",t.refreshToken),t.accessToken}))},t.Kc=function(t){if(!(t=t.users)||!t.length)throw new T("internal-error");Eh(this,{uid:(t=t[0]).localId,displayName:t.displayName,photoURL:t.photoUrl,email:t.email,emailVerified:!!t.emailVerified,phoneNumber:t.phoneNumber,lastLoginAt:t.lastLoginAt,createdAt:t.createdAt,tenantId:t.tenantId});for(var e,n=(e=(e=t).providerUserInfo)&&e.length?q(e,function(t){return new hh(t.rawId,t.providerId,t.email,t.displayName,t.photoUrl,t.phoneNumber)}):[],i=0;i=xl.length)throw new T("internal-error","Argument validator received an unsupported number of arguments.");n=xl[r],i=(i?"":n+" argument ")+(e.name?'"'+e.name+'" ':"")+"must be "+e.K+".";break t}i=null}}if(i)throw new T("argument-error",t+" failed: "+i)}(t=kl.prototype).Ia=function(){var e=this;return this.f||(this.f=Rl(this,ye().then(function(){if(Ti()&&!hi())return si();throw new T("operation-not-supported-in-this-environment","RecaptchaVerifier is only supported in a browser HTTP/HTTPS environment.")}).then(function(){return e.m.g(e.u())}).then(function(t){return e.g=t,Js(e.s,Rs,{})}).then(function(t){e.a[_l]=t.recaptchaSiteKey}).o(function(t){throw e.f=null,t})))},t.render=function(){Dl(this);var n=this;return Rl(this,this.Ia().then(function(){var t,e;return null===n.c&&(e=n.v,n.i||(t=te(e),e=oe("DIV"),t.appendChild(e)),n.c=n.g.render(e,n.a)),n.c}))},t.verify=function(){Dl(this);var r=this;return Rl(this,this.render().then(function(e){return new fe(function(n){var i,t=r.g.getResponse(e);t?n(t):(r.l.push(i=function(t){var e;t&&(e=i,B(r.l,function(t){return t==e}),n(t))}),r.i&&r.g.execute(r.c))})}))},t.reset=function(){Dl(this),null!==this.c&&this.g.reset(this.c)},t.clear=function(){Dl(this),this.J=!0,this.m.c();for(var t,e=0;es[0]&&e[1]>6|192:(55296==(64512&i)&&r+1>18|240,e[n++]=i>>12&63|128):e[n++]=i>>12|224,e[n++]=i>>6&63|128),e[n++]=63&i|128)}return e}function u(t){return function(t){t=i(t);return o.encodeByteArray(t,!0)}(t).replace(/\./g,"")}var o={byteToCharMap_:null,charToByteMap_:null,byteToCharMapWebSafe_:null,charToByteMapWebSafe_:null,ENCODED_VALS_BASE:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",get ENCODED_VALS(){return this.ENCODED_VALS_BASE+"+/="},get ENCODED_VALS_WEBSAFE(){return this.ENCODED_VALS_BASE+"-_."},HAS_NATIVE_SUPPORT:"function"==typeof atob,encodeByteArray:function(t,e){if(!Array.isArray(t))throw Error("encodeByteArray takes an array as a parameter");this.init_();for(var n=e?this.byteToCharMapWebSafe_:this.byteToCharMap_,r=[],i=0;i>6,c=63&c;u||(c=64,s||(h=64)),r.push(n[o>>2],n[(3&o)<<4|a>>4],n[h],n[c])}return r.join("")},encodeString:function(t,e){return this.HAS_NATIVE_SUPPORT&&!e?btoa(t):this.encodeByteArray(i(t),e)},decodeString:function(t,e){return this.HAS_NATIVE_SUPPORT&&!e?atob(t):function(t){for(var e=[],n=0,r=0;n>10)),e[r++]=String.fromCharCode(56320+(1023&i))):(o=t[n++],s=t[n++],e[r++]=String.fromCharCode((15&a)<<12|(63&o)<<6|63&s))}return e.join("")}(this.decodeStringToByteArray(t,e))},decodeStringToByteArray:function(t,e){this.init_();for(var n=e?this.charToByteMapWebSafe_:this.charToByteMap_,r=[],i=0;i>4),64!==a&&(r.push(s<<4&240|a>>2),64!==u&&r.push(a<<6&192|u))}return r},init_:function(){if(!this.byteToCharMap_){this.byteToCharMap_={},this.charToByteMap_={},this.byteToCharMapWebSafe_={},this.charToByteMapWebSafe_={};for(var t=0;t=this.ENCODED_VALS_BASE.length&&(this.charToByteMap_[this.ENCODED_VALS_WEBSAFE.charAt(t)]=t,this.charToByteMapWebSafe_[this.ENCODED_VALS.charAt(t)]=t)}}};function h(){return"undefined"!=typeof navigator&&"string"==typeof navigator.userAgent?navigator.userAgent:""}function c(){return!function(){try{return"[object process]"===Object.prototype.toString.call(global.process)}catch(t){return}}()&&navigator.userAgent.includes("Safari")&&!navigator.userAgent.includes("Chrome")}var l,f="FirebaseError",d=(n(p,l=Error),p);function p(t,e,n){e=l.call(this,e)||this;return e.code=t,e.customData=n,e.name=f,Object.setPrototypeOf(e,p.prototype),Error.captureStackTrace&&Error.captureStackTrace(e,m.prototype.create),e}var m=(v.prototype.create=function(t){for(var e=[],n=1;n"})):"Error",t=this.serviceName+": "+t+" ("+o+").";return new d(o,t,i)},v);function v(t,e,n){this.service=t,this.serviceName=e,this.errors=n}var w,b=/\{\$([^}]+)}/g;function E(t){return t&&t._delegate?t._delegate:t}(k=w=w||{})[k.DEBUG=0]="DEBUG",k[k.VERBOSE=1]="VERBOSE",k[k.INFO=2]="INFO",k[k.WARN=3]="WARN",k[k.ERROR=4]="ERROR",k[k.SILENT=5]="SILENT";function T(t,e){for(var n=[],r=2;r=t.length?void 0:t)&&t[r++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")}var k,R="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},x={},O=R||self;function L(){}function P(t){var e=typeof t;return"array"==(e="object"!=e?e:t?Array.isArray(t)?"array":e:"null")||"object"==e&&"number"==typeof t.length}function M(t){var e=typeof t;return"object"==e&&null!=t||"function"==e}var F="closure_uid_"+(1e9*Math.random()>>>0),V=0;function U(t,e,n){return t.call.apply(t.bind,arguments)}function q(e,n,t){if(!e)throw Error();if(2parseFloat(yt)){at=String(gt);break t}}at=yt}var mt={};function vt(){return t=function(){for(var t=0,e=J(String(at)).split("."),n=J("9").split("."),r=Math.max(e.length,n.length),i=0;0==t&&i>>0);function qt(e){return"function"==typeof e?e:(e[Ut]||(e[Ut]=function(t){return e.handleEvent(t)}),e[Ut])}function Bt(){G.call(this),this.i=new Nt(this),(this.P=this).I=null}function jt(t,e){var n,r=t.I;if(r)for(n=[];r;r=r.I)n.push(r);if(t=t.P,r=e.type||e,"string"==typeof e?e=new Et(e,t):e instanceof Et?e.target=e.target||t:(s=e,ot(e=new Et(r,t),s)),s=!0,n)for(var i=n.length-1;0<=i;i--)var o=e.g=n[i],s=Kt(o,r,!0,e)&&s;if(s=Kt(o=e.g=t,r,!0,e)&&s,s=Kt(o,r,!1,e)&&s,n)for(i=0;io.length?Pe:(o=o.substr(a,s),i.C=a+s,o)))==Pe){4==e&&(t.o=4,be(14),u=!1),de(t.j,t.m,null,"[Incomplete Response]");break}if(r==Le){t.o=4,be(15),de(t.j,t.m,n,"[Invalid Chunk]"),u=!1;break}de(t.j,t.m,r,null),Qe(t,r)}Ve(t)&&r!=Pe&&r!=Le&&(t.h.g="",t.C=0),4!=e||0!=n.length||t.h.h||(t.o=1,be(16),u=!1),t.i=t.i&&u,u?0>4&15).toString(16)+(15&t).toString(16)}$e.prototype.toString=function(){var t=[],e=this.j;e&&t.push(an(e,cn,!0),":");var n=this.i;return!n&&"file"!=e||(t.push("//"),(e=this.s)&&t.push(an(e,cn,!0),"@"),t.push(encodeURIComponent(String(n)).replace(/%25([0-9a-fA-F]{2})/g,"%$1")),null!=(n=this.m)&&t.push(":",String(n))),(n=this.l)&&(this.i&&"/"!=n.charAt(0)&&t.push("/"),t.push(an(n,"/"==n.charAt(0)?ln:hn,!0))),(n=this.h.toString())&&t.push("?",n),(n=this.o)&&t.push("#",an(n,dn)),t.join("")};var cn=/[#\/\?@]/g,hn=/[#\?:]/g,ln=/[#\?]/g,fn=/[#\?@]/g,dn=/#/g;function pn(t,e){this.h=this.g=null,this.i=t||null,this.j=!!e}function yn(n){n.g||(n.g=new ze,n.h=0,n.i&&function(t,e){if(t){t=t.split("&");for(var n=0;n2*t.i&&We(t)))}function mn(t,e){return yn(t),e=wn(t,e),Ye(t.g.h,e)}function vn(t,e,n){gn(t,e),0=t.j}function Sn(t){return t.h?1:t.g?t.g.size:0}function An(t,e){return t.h?t.h==e:t.g&&t.g.has(e)}function Dn(t,e){t.g?t.g.add(e):t.h=e}function Nn(t,e){t.h&&t.h==e?t.h=null:t.g&&t.g.has(e)&&t.g.delete(e)}function Cn(t){var e,n;if(null!=t.h)return t.i.concat(t.h.D);if(null==t.g||0===t.g.size)return Y(t.i);var r=t.i;try{for(var i=C(t.g.values()),o=i.next();!o.done;o=i.next())var s=o.value,r=r.concat(s.D)}catch(t){e={error:t}}finally{try{o&&!o.done&&(n=i.return)&&n.call(i)}finally{if(e)throw e.error}}return r}function kn(){}function Rn(){this.g=new kn}function xn(t,e,n,r,i){try{e.onload=null,e.onerror=null,e.onabort=null,e.ontimeout=null,i(r)}catch(t){}}function On(t){this.l=t.$b||null,this.j=t.ib||!1}function Ln(t,e){Bt.call(this),this.D=t,this.u=e,this.m=void 0,this.readyState=Pn,this.status=0,this.responseType=this.responseText=this.response=this.statusText="",this.onreadystatechange=null,this.v=new Headers,this.h=null,this.C="GET",this.B="",this.g=!1,this.A=this.j=this.l=null}En.prototype.cancel=function(){var e,t;if(this.i=Cn(this),this.h)this.h.cancel(),this.h=null;else if(this.g&&0!==this.g.size){try{for(var n=C(this.g.values()),r=n.next();!r.done;r=n.next())r.value.cancel()}catch(t){e={error:t}}finally{try{r&&!r.done&&(t=n.return)&&t.call(n)}finally{if(e)throw e.error}}this.g.clear()}},kn.prototype.stringify=function(t){return O.JSON.stringify(t,void 0)},kn.prototype.parse=function(t){return O.JSON.parse(t,void 0)},K(On,_e),On.prototype.g=function(){return new Ln(this.l,this.j)},On.prototype.i=(Tn={},function(){return Tn}),K(Ln,Bt);var Pn=0;function Mn(t){t.j.read().then(t.Sa.bind(t)).catch(t.ha.bind(t))}function Fn(t){t.readyState=4,t.l=null,t.j=null,t.A=null,Vn(t)}function Vn(t){t.onreadystatechange&&t.onreadystatechange.call(t)}(k=Ln.prototype).open=function(t,e){if(this.readyState!=Pn)throw this.abort(),Error("Error reopening a connection");this.C=t,this.B=e,this.readyState=1,Vn(this)},k.send=function(t){if(1!=this.readyState)throw this.abort(),Error("need to call open() first. ");this.g=!0;var e={headers:this.v,method:this.C,credentials:this.m,cache:void 0};t&&(e.body=t),(this.D||O).fetch(new Request(this.B,e)).then(this.Va.bind(this),this.ha.bind(this))},k.abort=function(){this.response=this.responseText="",this.v=new Headers,this.status=0,this.j&&this.j.cancel("Request was aborted."),1<=this.readyState&&this.g&&4!=this.readyState&&(this.g=!1,Fn(this)),this.readyState=Pn},k.Va=function(t){if(this.g&&(this.l=t,this.h||(this.status=this.l.status,this.statusText=this.l.statusText,this.h=t.headers,this.readyState=2,Vn(this)),this.g&&(this.readyState=3,Vn(this),this.g)))if("arraybuffer"===this.responseType)t.arrayBuffer().then(this.Ta.bind(this),this.ha.bind(this));else if(void 0!==O.ReadableStream&&"body"in t){if(this.j=t.body.getReader(),this.u){if(this.responseType)throw Error('responseType must be empty for "streamBinaryChunks" mode responses.');this.response=[]}else this.response=this.responseText="",this.A=new TextDecoder;Mn(this)}else t.text().then(this.Ua.bind(this),this.ha.bind(this))},k.Sa=function(t){var e;this.g&&(this.u&&t.value?this.response.push(t.value):this.u||(e=t.value||new Uint8Array(0),(e=this.A.decode(e,{stream:!t.done}))&&(this.response=this.responseText+=e)),(t.done?Fn:Vn)(this),3==this.readyState&&Mn(this))},k.Ua=function(t){this.g&&(this.response=this.responseText=t,Fn(this))},k.Ta=function(t){this.g&&(this.response=t,Fn(this))},k.ha=function(){this.g&&Fn(this)},k.setRequestHeader=function(t,e){this.v.append(t,e)},k.getResponseHeader=function(t){return this.h&&this.h.get(t.toLowerCase())||""},k.getAllResponseHeaders=function(){if(!this.h)return"";for(var t=[],e=this.h.entries(),n=e.next();!n.done;)n=n.value,t.push(n[0]+": "+n[1]),n=e.next();return t.join("\r\n")},Object.defineProperty(Ln.prototype,"withCredentials",{get:function(){return"include"===this.m},set:function(t){this.m=t?"include":"same-origin"}});var Un=O.JSON.parse;function qn(t){Bt.call(this),this.headers=new ze,this.u=t||null,this.h=!1,this.C=this.g=null,this.H="",this.m=0,this.j="",this.l=this.F=this.v=this.D=!1,this.B=0,this.A=null,this.J=Bn,this.K=this.L=!1}K(qn,Bt);var Bn="",jn=/^https?$/i,Kn=["POST","PUT"];function Gn(t){return"content-type"==t.toLowerCase()}function Qn(t,e){t.h=!1,t.g&&(t.l=!0,t.g.abort(),t.l=!1),t.j=e,t.m=5,Hn(t),Wn(t)}function Hn(t){t.D||(t.D=!0,jt(t,"complete"),jt(t,"error"))}function zn(t){if(t.h&&void 0!==x&&(!t.C[1]||4!=Xn(t)||2!=t.ba()))if(t.v&&4==Xn(t))ie(t.Fa,0,t);else if(jt(t,"readystatechange"),4==Xn(t)){t.h=!1;try{var e,n,r,i,o=t.ba();t:switch(o){case 200:case 201:case 202:case 204:case 206:case 304:case 1223:var s=!0;break t;default:s=!1}if((e=s)||((n=0===o)&&(!(i=String(t.H).match(Xe)[1]||null)&&O.self&&O.self.location&&(i=(r=O.self.location.protocol).substr(0,r.length-1)),n=!jn.test(i?i.toLowerCase():"")),e=n),e)jt(t,"complete"),jt(t,"success");else{t.m=6;try{var a=2=r.i.j-(r.m?1:0)||(r.m?(r.l=i.D.concat(r.l),0):1==r.G||2==r.G||r.C>=(r.Xa?0:r.Ya)||(r.m=Te(B(r.Ha,r,i),yr(r,r.C)),r.C++,0))))&&(2!=s||!hr(t)))switch(o&&0e.length?1:0},vi),ci=(n(mi,ui=R),mi.prototype.construct=function(t,e,n){return new mi(t,e,n)},mi.prototype.canonicalString=function(){return this.toArray().join("/")},mi.prototype.toString=function(){return this.canonicalString()},mi.fromString=function(){for(var t=[],e=0;et.length&&zr(),void 0===n?n=t.length-e:n>t.length-e&&zr(),this.segments=t,this.offset=e,this.len=n}di.EMPTY_BYTE_STRING=new di("");var wi=new RegExp(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.(\d+))?Z$/);function bi(t){if(Wr(!!t),"string"!=typeof t)return{seconds:Ei(t.seconds),nanos:Ei(t.nanos)};var e=0,n=wi.exec(t);Wr(!!n),n[1]&&(n=((n=n[1])+"000000000").substr(0,9),e=Number(n));t=new Date(t);return{seconds:Math.floor(t.getTime()/1e3),nanos:e}}function Ei(t){return"number"==typeof t?t:"string"==typeof t?Number(t):0}function Ti(t){return"string"==typeof t?di.fromBase64String(t):di.fromUint8Array(t)}function Ii(t){return"server_timestamp"===(null===(t=((null===(t=null==t?void 0:t.mapValue)||void 0===t?void 0:t.fields)||{}).__type__)||void 0===t?void 0:t.stringValue)}function _i(t){t=bi(t.mapValue.fields.__local_write_time__.timestampValue);return new ti(t.seconds,t.nanos)}function Si(t){return null==t}function Ai(t){return 0===t&&1/t==-1/0}function Di(t){return"number"==typeof t&&Number.isInteger(t)&&!Ai(t)&&t<=Number.MAX_SAFE_INTEGER&&t>=Number.MIN_SAFE_INTEGER}var Ni=(Ci.fromPath=function(t){return new Ci(ci.fromString(t))},Ci.fromName=function(t){return new Ci(ci.fromString(t).popFirst(5))},Ci.prototype.hasCollectionId=function(t){return 2<=this.path.length&&this.path.get(this.path.length-2)===t},Ci.prototype.isEqual=function(t){return null!==t&&0===ci.comparator(this.path,t.path)},Ci.prototype.toString=function(){return this.path.toString()},Ci.comparator=function(t,e){return ci.comparator(t.path,e.path)},Ci.isDocumentKey=function(t){return t.length%2==0},Ci.fromSegments=function(t){return new Ci(new ci(t.slice()))},Ci);function Ci(t){this.path=t}function ki(t){return"nullValue"in t?0:"booleanValue"in t?1:"integerValue"in t||"doubleValue"in t?2:"timestampValue"in t?3:"stringValue"in t?5:"bytesValue"in t?6:"referenceValue"in t?7:"geoPointValue"in t?8:"arrayValue"in t?9:"mapValue"in t?Ii(t)?4:10:zr()}function Ri(r,i){var t,e,n=ki(r);if(n!==ki(i))return!1;switch(n){case 0:return!0;case 1:return r.booleanValue===i.booleanValue;case 4:return _i(r).isEqual(_i(i));case 3:return function(t){if("string"==typeof r.timestampValue&&"string"==typeof t.timestampValue&&r.timestampValue.length===t.timestampValue.length)return r.timestampValue===t.timestampValue;var e=bi(r.timestampValue),t=bi(t.timestampValue);return e.seconds===t.seconds&&e.nanos===t.nanos}(i);case 5:return r.stringValue===i.stringValue;case 6:return e=i,Ti(r.bytesValue).isEqual(Ti(e.bytesValue));case 7:return r.referenceValue===i.referenceValue;case 8:return t=i,Ei((e=r).geoPointValue.latitude)===Ei(t.geoPointValue.latitude)&&Ei(e.geoPointValue.longitude)===Ei(t.geoPointValue.longitude);case 2:return function(t,e){if("integerValue"in t&&"integerValue"in e)return Ei(t.integerValue)===Ei(e.integerValue);if("doubleValue"in t&&"doubleValue"in e){t=Ei(t.doubleValue),e=Ei(e.doubleValue);return t===e?Ai(t)===Ai(e):isNaN(t)&&isNaN(e)}return!1}(r,i);case 9:return Jr(r.arrayValue.values||[],i.arrayValue.values||[],Ri);case 10:return function(){var t,e=r.mapValue.fields||{},n=i.mapValue.fields||{};if(ii(e)!==ii(n))return!1;for(t in e)if(e.hasOwnProperty(t)&&(void 0===n[t]||!Ri(e[t],n[t])))return!1;return!0}();default:return zr()}}function xi(t,e){return void 0!==(t.values||[]).find(function(t){return Ri(t,e)})}function Oi(t,e){var n,r,i,o=ki(t),s=ki(e);if(o!==s)return $r(o,s);switch(o){case 0:return 0;case 1:return $r(t.booleanValue,e.booleanValue);case 2:return r=e,i=Ei(t.integerValue||t.doubleValue),r=Ei(r.integerValue||r.doubleValue),i":return 0=":return 0<=t;default:return zr()}},to.prototype.g=function(){return 0<=["<","<=",">",">=","!=","not-in"].indexOf(this.op)},to);function to(t,e,n){var r=this;return(r=Ji.call(this)||this).field=t,r.op=e,r.value=n,r}var eo,no,ro,io=(n(co,ro=Zi),co.prototype.matches=function(t){t=Ni.comparator(t.key,this.key);return this.m(t)},co),oo=(n(uo,no=Zi),uo.prototype.matches=function(e){return this.keys.some(function(t){return t.isEqual(e.key)})},uo),so=(n(ao,eo=Zi),ao.prototype.matches=function(e){return!this.keys.some(function(t){return t.isEqual(e.key)})},ao);function ao(t,e){var n=this;return(n=eo.call(this,t,"not-in",e)||this).keys=ho(0,e),n}function uo(t,e){var n=this;return(n=no.call(this,t,"in",e)||this).keys=ho(0,e),n}function co(t,e,n){var r=this;return(r=ro.call(this,t,e,n)||this).key=Ni.fromName(n.referenceValue),r}function ho(t,e){return((null===(e=e.arrayValue)||void 0===e?void 0:e.values)||[]).map(function(t){return Ni.fromName(t.referenceValue)})}var lo,fo,po,yo,go=(n(_o,yo=Zi),_o.prototype.matches=function(t){t=t.data.field(this.field);return Vi(t)&&xi(t.arrayValue,this.value)},_o),mo=(n(Io,po=Zi),Io.prototype.matches=function(t){t=t.data.field(this.field);return null!==t&&xi(this.value.arrayValue,t)},Io),vo=(n(To,fo=Zi),To.prototype.matches=function(t){if(xi(this.value.arrayValue,{nullValue:"NULL_VALUE"}))return!1;t=t.data.field(this.field);return null!==t&&!xi(this.value.arrayValue,t)},To),wo=(n(Eo,lo=Zi),Eo.prototype.matches=function(t){var e=this,t=t.data.field(this.field);return!(!Vi(t)||!t.arrayValue.values)&&t.arrayValue.values.some(function(t){return xi(e.value.arrayValue,t)})},Eo),bo=function(t,e){this.position=t,this.before=e};function Eo(t,e){return lo.call(this,t,"array-contains-any",e)||this}function To(t,e){return fo.call(this,t,"not-in",e)||this}function Io(t,e){return po.call(this,t,"in",e)||this}function _o(t,e){return yo.call(this,t,"array-contains",e)||this}function So(t){return(t.before?"b":"a")+":"+t.position.map(Pi).join(",")}var Ao=function(t,e){void 0===e&&(e="asc"),this.field=t,this.dir=e};function Do(t,e,n){for(var r=0,i=0;i":"GREATER_THAN",">=":"GREATER_THAN_OR_EQUAL","==":"EQUAL","!=":"NOT_EQUAL","array-contains":"ARRAY_CONTAINS",in:"IN","not-in":"NOT_IN","array-contains-any":"ARRAY_CONTAINS_ANY"},ga=function(t,e){this.databaseId=t,this.I=e};function ma(t,e){return t.I?new Date(1e3*e.seconds).toISOString().replace(/\.\d*/,"").replace("Z","")+"."+("000000000"+e.nanoseconds).slice(-9)+"Z":{seconds:""+e.seconds,nanos:e.nanoseconds}}function va(t,e){return t.I?e.toBase64():e.toUint8Array()}function wa(t){return Wr(!!t),ei.fromTimestamp((t=bi(t),new ti(t.seconds,t.nanos)))}function ba(t,e){return new ci(["projects",t.projectId,"databases",t.database]).child("documents").child(e).canonicalString()}function Ea(t){t=ci.fromString(t);return Wr(Ba(t)),t}function Ta(t,e){return ba(t.databaseId,e.path)}function Ia(t,e){e=Ea(e);if(e.get(1)!==t.databaseId.projectId)throw new Ur(Vr.INVALID_ARGUMENT,"Tried to deserialize key from different project: "+e.get(1)+" vs "+t.databaseId.projectId);if(e.get(3)!==t.databaseId.database)throw new Ur(Vr.INVALID_ARGUMENT,"Tried to deserialize key from different database: "+e.get(3)+" vs "+t.databaseId.database);return new Ni(Da(e))}function _a(t,e){return ba(t.databaseId,e)}function Sa(t){t=Ea(t);return 4===t.length?ci.emptyPath():Da(t)}function Aa(t){return new ci(["projects",t.databaseId.projectId,"databases",t.databaseId.database]).canonicalString()}function Da(t){return Wr(4";case"GREATER_THAN_OR_EQUAL":return">=";case"LESS_THAN":return"<";case"LESS_THAN_OR_EQUAL":return"<=";case"ARRAY_CONTAINS":return"array-contains";case"IN":return"in";case"NOT_IN":return"not-in";case"ARRAY_CONTAINS_ANY":return"array-contains-any";case"OPERATOR_UNSPECIFIED":default:return zr()}}(),t.fieldFilter.value)}function qa(t){switch(t.unaryFilter.op){case"IS_NAN":var e=Va(t.unaryFilter.field);return Zi.create(e,"==",{doubleValue:NaN});case"IS_NULL":e=Va(t.unaryFilter.field);return Zi.create(e,"==",{nullValue:"NULL_VALUE"});case"IS_NOT_NAN":var n=Va(t.unaryFilter.field);return Zi.create(n,"!=",{doubleValue:NaN});case"IS_NOT_NULL":n=Va(t.unaryFilter.field);return Zi.create(n,"!=",{nullValue:"NULL_VALUE"});case"OPERATOR_UNSPECIFIED":default:return zr()}}function Ba(t){return 4<=t.length&&"projects"===t.get(0)&&"databases"===t.get(2)}function ja(t){for(var e="",n=0;n",t),this.store.put(t));return Au(t)},Su.prototype.add=function(t){return Kr("SimpleDb","ADD",this.store.name,t,t),Au(this.store.add(t))},Su.prototype.get=function(e){var n=this;return Au(this.store.get(e)).next(function(t){return Kr("SimpleDb","GET",n.store.name,e,t=void 0===t?null:t),t})},Su.prototype.delete=function(t){return Kr("SimpleDb","DELETE",this.store.name,t),Au(this.store.delete(t))},Su.prototype.count=function(){return Kr("SimpleDb","COUNT",this.store.name),Au(this.store.count())},Su.prototype.Nt=function(t,e){var e=this.cursor(this.options(t,e)),n=[];return this.xt(e,function(t,e){n.push(e)}).next(function(){return n})},Su.prototype.kt=function(t,e){Kr("SimpleDb","DELETE ALL",this.store.name);e=this.options(t,e);e.Ft=!1;e=this.cursor(e);return this.xt(e,function(t,e,n){return n.delete()})},Su.prototype.$t=function(t,e){e?n=t:(n={},e=t);var n=this.cursor(n);return this.xt(n,e)},Su.prototype.Ot=function(r){var t=this.cursor({});return new fu(function(n,e){t.onerror=function(t){t=Nu(t.target.error);e(t)},t.onsuccess=function(t){var e=t.target.result;e?r(e.primaryKey,e.value).next(function(t){t?e.continue():n()}):n()}})},Su.prototype.xt=function(t,i){var o=[];return new fu(function(r,e){t.onerror=function(t){e(t.target.error)},t.onsuccess=function(t){var e,n=t.target.result;n?(e=new yu(n),(t=i(n.primaryKey,n.value,e))instanceof fu&&(t=t.catch(function(t){return e.done(),fu.reject(t)}),o.push(t)),e.isDone?r():null===e.Dt?n.continue():n.continue(e.Dt)):r()}}).next(function(){return fu.waitFor(o)})},Su.prototype.options=function(t,e){var n;return void 0!==t&&("string"==typeof t?n=t:e=t),{index:n,range:e}},Su.prototype.cursor=function(t){var e="next";if(t.reverse&&(e="prev"),t.index){var n=this.store.index(t.index);return t.Ft?n.openKeyCursor(t.range,e):n.openCursor(t.range,e)}return this.store.openCursor(t.range,e)},Su);function Su(t){this.store=t}function Au(t){return new fu(function(e,n){t.onsuccess=function(t){t=t.target.result;e(t)},t.onerror=function(t){t=Nu(t.target.error);n(t)}})}var Du=!1;function Nu(t){var e=pu._t(h());if(12.2<=e&&e<13){e="An internal error was encountered in the Indexed Database server";if(0<=t.message.indexOf(e)){var n=new Ur("internal","IOS_INDEXEDDB_BUG1: IndexedDb has thrown '"+e+"'. This is likely due to an unavoidable bug in iOS. See https://stackoverflow.com/q/56496296/110915 for details and a potential workaround.");return Du||(Du=!0,setTimeout(function(){throw n},0)),n}}return t}var Cu,ku=(n(Ru,Cu=R),Ru);function Ru(t,e){var n=this;return(n=Cu.call(this)||this).Mt=t,n.currentSequenceNumber=e,n}function xu(t,e){return pu.It(t.Mt,e)}var Ou=(Uu.prototype.applyToRemoteDocument=function(t,e){for(var n,r,i,o,s,a,u=e.mutationResults,c=0;c=i),o=Hu(r.R,e)),n.done()}).next(function(){return o})},dc.prototype.getHighestUnacknowledgedBatchId=function(t){var e=IDBKeyRange.upperBound([this.userId,Number.POSITIVE_INFINITY]),r=-1;return yc(t).$t({index:Wa.userMutationsIndex,range:e,reverse:!0},function(t,e,n){r=e.batchId,n.done()}).next(function(){return r})},dc.prototype.getAllMutationBatches=function(t){var e=this,n=IDBKeyRange.bound([this.userId,-1],[this.userId,Number.POSITIVE_INFINITY]);return yc(t).Nt(Wa.userMutationsIndex,n).next(function(t){return t.map(function(t){return Hu(e.R,t)})})},dc.prototype.getAllMutationBatchesAffectingDocumentKey=function(o,s){var a=this,t=Ya.prefixForPath(this.userId,s.path),t=IDBKeyRange.lowerBound(t),u=[];return gc(o).$t({range:t},function(t,e,n){var r=t[0],i=t[1],t=t[2],i=Ga(i);if(r===a.userId&&s.path.isEqual(i))return yc(o).get(t).next(function(t){if(!t)throw zr();Wr(t.userId===a.userId),u.push(Hu(a.R,t))});n.done()}).next(function(){return u})},dc.prototype.getAllMutationBatchesAffectingDocumentKeys=function(e,t){var s=this,a=new Qs($r),n=[];return t.forEach(function(o){var t=Ya.prefixForPath(s.userId,o.path),t=IDBKeyRange.lowerBound(t),t=gc(e).$t({range:t},function(t,e,n){var r=t[0],i=t[1],t=t[2],i=Ga(i);r===s.userId&&o.path.isEqual(i)?a=a.add(t):n.done()});n.push(t)}),fu.waitFor(n).next(function(){return s.Wt(e,a)})},dc.prototype.getAllMutationBatchesAffectingQuery=function(t,e){var o=this,s=e.path,a=s.length+1,e=Ya.prefixForPath(this.userId,s),e=IDBKeyRange.lowerBound(e),u=new Qs($r);return gc(t).$t({range:e},function(t,e,n){var r=t[0],i=t[1],t=t[2],i=Ga(i);r===o.userId&&s.isPrefixOf(i)?i.length===a&&(u=u.add(t)):n.done()}).next(function(){return o.Wt(t,u)})},dc.prototype.Wt=function(e,t){var n=this,r=[],i=[];return t.forEach(function(t){i.push(yc(e).get(t).next(function(t){if(null===t)throw zr();Wr(t.userId===n.userId),r.push(Hu(n.R,t))}))}),fu.waitFor(i).next(function(){return r})},dc.prototype.removeMutationBatch=function(e,n){var r=this;return hc(e.Mt,this.userId,n).next(function(t){return e.addOnCommittedListener(function(){r.Gt(n.batchId)}),fu.forEach(t,function(t){return r.referenceDelegate.markPotentiallyOrphaned(e,t)})})},dc.prototype.Gt=function(t){delete this.Kt[t]},dc.prototype.performConsistencyCheck=function(e){var i=this;return this.checkEmpty(e).next(function(t){if(!t)return fu.resolve();var t=IDBKeyRange.lowerBound(Ya.prefixForUser(i.userId)),r=[];return gc(e).$t({range:t},function(t,e,n){t[0]===i.userId?(t=Ga(t[1]),r.push(t)):n.done()}).next(function(){Wr(0===r.length)})})},dc.prototype.containsKey=function(t,e){return pc(t,this.userId,e)},dc.prototype.zt=function(t){var e=this;return mc(t).get(this.userId).next(function(t){return t||new za(e.userId,-1,"")})},dc);function dc(t,e,n,r){this.userId=t,this.R=e,this.Ut=n,this.referenceDelegate=r,this.Kt={}}function pc(t,o,e){var e=Ya.prefixForPath(o,e.path),s=e[1],e=IDBKeyRange.lowerBound(e),a=!1;return gc(t).$t({range:e,Ft:!0},function(t,e,n){var r=t[0],i=t[1];t[2],r===o&&i===s&&(a=!0),n.done()}).next(function(){return a})}function yc(t){return xu(t,Wa.store)}function gc(t){return xu(t,Ya.store)}function mc(t){return xu(t,za.store)}var vc=(Ec.prototype.next=function(){return this.Ht+=2,this.Ht},Ec.Jt=function(){return new Ec(0)},Ec.Yt=function(){return new Ec(-1)},Ec),wc=(bc.prototype.allocateTargetId=function(n){var r=this;return this.Xt(n).next(function(t){var e=new vc(t.highestTargetId);return t.highestTargetId=e.next(),r.Zt(n,t).next(function(){return t.highestTargetId})})},bc.prototype.getLastRemoteSnapshotVersion=function(t){return this.Xt(t).next(function(t){return ei.fromTimestamp(new ti(t.lastRemoteSnapshotVersion.seconds,t.lastRemoteSnapshotVersion.nanoseconds))})},bc.prototype.getHighestSequenceNumber=function(t){return this.Xt(t).next(function(t){return t.highestListenSequenceNumber})},bc.prototype.setTargetsMetadata=function(e,n,r){var i=this;return this.Xt(e).next(function(t){return t.highestListenSequenceNumber=n,r&&(t.lastRemoteSnapshotVersion=r.toTimestamp()),n>t.highestListenSequenceNumber&&(t.highestListenSequenceNumber=n),i.Zt(e,t)})},bc.prototype.addTargetData=function(e,n){var r=this;return this.te(e,n).next(function(){return r.Xt(e).next(function(t){return t.targetCount+=1,r.ee(n,t),r.Zt(e,t)})})},bc.prototype.updateTargetData=function(t,e){return this.te(t,e)},bc.prototype.removeTargetData=function(e,t){var n=this;return this.removeMatchingKeysForTargetId(e,t.targetId).next(function(){return Tc(e).delete(t.targetId)}).next(function(){return n.Xt(e)}).next(function(t){return Wr(0e.highestTargetId&&(e.highestTargetId=t.targetId,n=!0),t.sequenceNumber>e.highestListenSequenceNumber&&(e.highestListenSequenceNumber=t.sequenceNumber,n=!0),n},bc.prototype.getTargetCount=function(t){return this.Xt(t).next(function(t){return t.targetCount})},bc.prototype.getTargetData=function(t,r){var e=Yi(r),e=IDBKeyRange.bound([e,Number.NEGATIVE_INFINITY],[e,Number.POSITIVE_INFINITY]),i=null;return Tc(t).$t({range:e,index:eu.queryTargetsIndexName},function(t,e,n){e=zu(e);Xi(r,e.target)&&(i=e,n.done())}).next(function(){return i})},bc.prototype.addMatchingKeys=function(n,t,r){var i=this,o=[],s=_c(n);return t.forEach(function(t){var e=ja(t.path);o.push(s.put(new nu(r,e))),o.push(i.referenceDelegate.addReference(n,r,t))}),fu.waitFor(o)},bc.prototype.removeMatchingKeys=function(n,t,r){var i=this,o=_c(n);return fu.forEach(t,function(t){var e=ja(t.path);return fu.waitFor([o.delete([r,e]),i.referenceDelegate.removeReference(n,r,t)])})},bc.prototype.removeMatchingKeysForTargetId=function(t,e){t=_c(t),e=IDBKeyRange.bound([e],[e+1],!1,!0);return t.delete(e)},bc.prototype.getMatchingKeysForTargetId=function(t,e){var e=IDBKeyRange.bound([e],[e+1],!1,!0),t=_c(t),r=Zs();return t.$t({range:e,Ft:!0},function(t,e,n){t=Ga(t[1]),t=new Ni(t);r=r.add(t)}).next(function(){return r})},bc.prototype.containsKey=function(t,e){var e=ja(e.path),e=IDBKeyRange.bound([e],[Zr(e)],!1,!0),i=0;return _c(t).$t({index:nu.documentTargetsIndex,Ft:!0,range:e},function(t,e,n){var r=t[0];t[1],0!==r&&(i++,n.done())}).next(function(){return 0h.params.maximumSequenceNumbersToCollect?(Kr("LruGarbageCollector","Capping sequence numbers to collect down to the maximum of "+h.params.maximumSequenceNumbersToCollect+" from "+t),h.params.maximumSequenceNumbersToCollect):t,s=Date.now(),h.nthSequenceNumber(e,i)}).next(function(t){return r=t,a=Date.now(),h.removeTargets(e,r,n)}).next(function(t){return o=t,u=Date.now(),h.removeOrphanedDocuments(e,r)}).next(function(t){return c=Date.now(),jr()<=w.DEBUG&&Kr("LruGarbageCollector","LRU Garbage Collection\n\tCounted targets in "+(s-l)+"ms\n\tDetermined least recently used "+i+" in "+(a-s)+"ms\n\tRemoved "+o+" targets in "+(u-a)+"ms\n\tRemoved "+t+" documents in "+(c-u)+"ms\nTotal Duration: "+(c-l)+"ms"),fu.resolve({didRun:!0,sequenceNumbersCollected:i,targetsRemoved:o,documentsRemoved:t})})},xc),kc=(Rc.prototype.he=function(t){var n=this.de(t);return this.db.getTargetCache().getTargetCount(t).next(function(e){return n.next(function(t){return e+t})})},Rc.prototype.de=function(t){var e=0;return this.le(t,function(t){e++}).next(function(){return e})},Rc.prototype.forEachTarget=function(t,e){return this.db.getTargetCache().forEachTarget(t,e)},Rc.prototype.le=function(t,n){return this.we(t,function(t,e){return n(e)})},Rc.prototype.addReference=function(t,e,n){return Pc(t,n)},Rc.prototype.removeReference=function(t,e,n){return Pc(t,n)},Rc.prototype.removeTargets=function(t,e,n){return this.db.getTargetCache().removeTargets(t,e,n)},Rc.prototype.markPotentiallyOrphaned=Pc,Rc.prototype._e=function(t,e){return r=e,i=!1,mc(n=t).Ot(function(t){return pc(n,t,r).next(function(t){return t&&(i=!0),fu.resolve(!t)})}).next(function(){return i});var n,r,i},Rc.prototype.removeOrphanedDocuments=function(n,r){var i=this,o=this.db.getRemoteDocumentCache().newChangeBuffer(),s=[],a=0;return this.we(n,function(e,t){t<=r&&(t=i._e(n,e).next(function(t){if(!t)return a++,o.getEntry(n,e).next(function(){return o.removeEntry(e),_c(n).delete([0,ja(e.path)])})}),s.push(t))}).next(function(){return fu.waitFor(s)}).next(function(){return o.apply(n)}).next(function(){return a})},Rc.prototype.removeTarget=function(t,e){e=e.withSequenceNumber(t.currentSequenceNumber);return this.db.getTargetCache().updateTargetData(t,e)},Rc.prototype.updateLimboDocument=Pc,Rc.prototype.we=function(t,r){var i,t=_c(t),o=Pr.o;return t.$t({index:nu.documentTargetsIndex},function(t,e){var n=t[0];t[1];t=e.path,e=e.sequenceNumber;0===n?(o!==Pr.o&&r(new Ni(Ga(i)),o),o=e,i=t):o=Pr.o}).next(function(){o!==Pr.o&&r(new Ni(Ga(i)),o)})},Rc.prototype.getCacheSize=function(t){return this.db.getRemoteDocumentCache().getSize(t)},Rc);function Rc(t,e){this.db=t,this.garbageCollector=new Cc(this,e)}function xc(t,e){this.ae=t,this.params=e}function Oc(t,e){this.garbageCollector=t,this.asyncQueue=e,this.oe=!1,this.ce=null}function Lc(t){this.ne=t,this.buffer=new Qs(Ac),this.se=0}function Pc(t,e){return _c(t).put((t=t.currentSequenceNumber,new nu(0,ja(e.path),t)))}var Mc,Fc=(Kc.prototype.get=function(t){var e=this.mapKeyFn(t),e=this.inner[e];if(void 0!==e)for(var n=0,r=e;n "+n),1))},Jc.prototype.We=function(){var t=this;null!==this.document&&"function"==typeof this.document.addEventListener&&(this.Fe=function(){t.Se.enqueueAndForget(function(){return t.inForeground="visible"===t.document.visibilityState,t.je()})},this.document.addEventListener("visibilitychange",this.Fe),this.inForeground="visible"===this.document.visibilityState)},Jc.prototype.an=function(){this.Fe&&(this.document.removeEventListener("visibilitychange",this.Fe),this.Fe=null)},Jc.prototype.Ge=function(){var t,e=this;"function"==typeof(null===(t=this.window)||void 0===t?void 0:t.addEventListener)&&(this.ke=function(){e.un(),c()&&navigator.appVersion.match("Version/14")&&e.Se.enterRestrictedMode(!0),e.Se.enqueueAndForget(function(){return e.shutdown()})},this.window.addEventListener("pagehide",this.ke))},Jc.prototype.hn=function(){this.ke&&(this.window.removeEventListener("pagehide",this.ke),this.ke=null)},Jc.prototype.cn=function(t){var e;try{var n=null!==(null===(e=this.Qe)||void 0===e?void 0:e.getItem(this.on(t)));return Kr("IndexedDbPersistence","Client '"+t+"' "+(n?"is":"is not")+" zombied in LocalStorage"),n}catch(t){return Gr("IndexedDbPersistence","Failed to get zombied client id.",t),!1}},Jc.prototype.un=function(){if(this.Qe)try{this.Qe.setItem(this.on(this.clientId),String(Date.now()))}catch(t){Gr("Failed to set zombie client id.",t)}},Jc.prototype.ln=function(){if(this.Qe)try{this.Qe.removeItem(this.on(this.clientId))}catch(t){}},Jc.prototype.on=function(t){return"firestore_zombie_"+this.persistenceKey+"_"+t},Jc);function Jc(t,e,n,r,i,o,s,a,u,c){if(this.allowTabSynchronization=t,this.persistenceKey=e,this.clientId=n,this.Se=i,this.window=o,this.document=s,this.De=u,this.Ce=c,this.Ne=null,this.xe=!1,this.isPrimary=!1,this.networkEnabled=!0,this.ke=null,this.inForeground=!1,this.Fe=null,this.$e=null,this.Oe=Number.NEGATIVE_INFINITY,this.Me=function(t){return Promise.resolve()},!Jc.yt())throw new Ur(Vr.UNIMPLEMENTED,"This platform is either missing IndexedDB or is known to have an incomplete implementation. Offline persistence has been disabled.");this.referenceDelegate=new kc(this,r),this.Le=e+"main",this.R=new Mu(a),this.Be=new pu(this.Le,11,new zc(this.R)),this.qe=new wc(this.referenceDelegate,this.R),this.Ut=new nc,this.Ue=(e=this.R,a=this.Ut,new Vc(e,a)),this.Ke=new Xu,this.window&&this.window.localStorage?this.Qe=this.window.localStorage:(this.Qe=null,!1===c&&Gr("IndexedDbPersistence","LocalStorage is unavailable. As a result, persistence may not work reliably. In particular enablePersistence() could fail immediately after refreshing the page."))}function Zc(t){return xu(t,Qa.store)}function th(t){return xu(t,ou.store)}function eh(t,e){var n=t.projectId;return t.isDefaultDatabase||(n+="."+t.database),"firestore/"+e+"/"+n+"/"}function nh(t,e){this.progress=t,this.wn=e}var rh=(hh.prototype.mn=function(e,n){var r=this;return this._n.getAllMutationBatchesAffectingDocumentKey(e,n).next(function(t){return r.yn(e,n,t)})},hh.prototype.yn=function(t,e,r){return this.Ue.getEntry(t,e).next(function(t){for(var e=0,n=r;ee?this._n[e]:null)},Kh.prototype.getHighestUnacknowledgedBatchId=function(){return fu.resolve(0===this._n.length?-1:this.ss-1)},Kh.prototype.getAllMutationBatches=function(t){return fu.resolve(this._n.slice())},Kh.prototype.getAllMutationBatchesAffectingDocumentKey=function(t,e){var n=this,r=new Dh(e,0),e=new Dh(e,Number.POSITIVE_INFINITY),i=[];return this.rs.forEachInRange([r,e],function(t){t=n.os(t.ns);i.push(t)}),fu.resolve(i)},Kh.prototype.getAllMutationBatchesAffectingDocumentKeys=function(t,e){var n=this,r=new Qs($r);return e.forEach(function(t){var e=new Dh(t,0),t=new Dh(t,Number.POSITIVE_INFINITY);n.rs.forEachInRange([e,t],function(t){r=r.add(t.ns)})}),fu.resolve(this.us(r))},Kh.prototype.getAllMutationBatchesAffectingQuery=function(t,e){var n=e.path,r=n.length+1,e=n;Ni.isDocumentKey(e)||(e=e.child(""));var e=new Dh(new Ni(e),0),i=new Qs($r);return this.rs.forEachWhile(function(t){var e=t.key.path;return!!n.isPrefixOf(e)&&(e.length===r&&(i=i.add(t.ns)),!0)},e),fu.resolve(this.us(i))},Kh.prototype.us=function(t){var e=this,n=[];return t.forEach(function(t){t=e.os(t);null!==t&&n.push(t)}),n},Kh.prototype.removeMutationBatch=function(n,r){var i=this;Wr(0===this.hs(r.batchId,"removed")),this._n.shift();var o=this.rs;return fu.forEach(r.mutations,function(t){var e=new Dh(t.key,r.batchId);return o=o.delete(e),i.referenceDelegate.markPotentiallyOrphaned(n,t.key)}).next(function(){i.rs=o})},Kh.prototype.Gt=function(t){},Kh.prototype.containsKey=function(t,e){var n=new Dh(e,0),n=this.rs.firstAfterOrEqual(n);return fu.resolve(e.isEqual(n&&n.key))},Kh.prototype.performConsistencyCheck=function(t){return this._n.length,fu.resolve()},Kh.prototype.hs=function(t,e){return this.cs(t)},Kh.prototype.cs=function(t){return 0===this._n.length?0:t-this._n[0].batchId},Kh.prototype.os=function(t){t=this.cs(t);return t<0||t>=this._n.length?null:this._n[t]},Kh),Ch=(jh.prototype.addEntry=function(t,e,n){var r=e.key,i=this.docs.get(r),o=i?i.size:0,i=this.ls(e);return this.docs=this.docs.insert(r,{document:e.clone(),size:i,readTime:n}),this.size+=i-o,this.Ut.addToCollectionParentIndex(t,r.path.popLast())},jh.prototype.removeEntry=function(t){var e=this.docs.get(t);e&&(this.docs=this.docs.remove(t),this.size-=e.size)},jh.prototype.getEntry=function(t,e){var n=this.docs.get(e);return fu.resolve(n?n.document.clone():Qi.newInvalidDocument(e))},jh.prototype.getEntries=function(t,e){var n=this,r=zs;return e.forEach(function(t){var e=n.docs.get(t);r=r.insert(t,e?e.document.clone():Qi.newInvalidDocument(t))}),fu.resolve(r)},jh.prototype.getDocumentsMatchingQuery=function(t,e,n){for(var r=zs,i=new Ni(e.path.child("")),o=this.docs.getIteratorFrom(i);o.hasNext();){var s=o.getNext(),a=s.key,u=s.value,s=u.document,u=u.readTime;if(!e.path.isPrefixOf(a.path))break;u.compareTo(n)<=0||Ko(e,s)&&(r=r.insert(s.key,s.clone()))}return fu.resolve(r)},jh.prototype.fs=function(t,e){return fu.forEach(this.docs,function(t){return e(t)})},jh.prototype.newChangeBuffer=function(t){return new kh(this)},jh.prototype.getSize=function(t){return fu.resolve(this.size)},jh),kh=(n(Bh,_h=A),Bh.prototype.applyChanges=function(n){var r=this,i=[];return this.changes.forEach(function(t,e){e.document.isValidDocument()?i.push(r.Ie.addEntry(n,e.document,r.getReadTime(t))):r.Ie.removeEntry(t)}),fu.waitFor(i)},Bh.prototype.getFromCache=function(t,e){return this.Ie.getEntry(t,e)},Bh.prototype.getAllFromCache=function(t,e){return this.Ie.getEntries(t,e)},Bh),Rh=(qh.prototype.forEachTarget=function(t,n){return this.ds.forEach(function(t,e){return n(e)}),fu.resolve()},qh.prototype.getLastRemoteSnapshotVersion=function(t){return fu.resolve(this.lastRemoteSnapshotVersion)},qh.prototype.getHighestSequenceNumber=function(t){return fu.resolve(this.ws)},qh.prototype.allocateTargetId=function(t){return this.highestTargetId=this.ys.next(),fu.resolve(this.highestTargetId)},qh.prototype.setTargetsMetadata=function(t,e,n){return n&&(this.lastRemoteSnapshotVersion=n),e>this.ws&&(this.ws=e),fu.resolve()},qh.prototype.te=function(t){this.ds.set(t.target,t);var e=t.targetId;e>this.highestTargetId&&(this.ys=new vc(e),this.highestTargetId=e),t.sequenceNumber>this.ws&&(this.ws=t.sequenceNumber)},qh.prototype.addTargetData=function(t,e){return this.te(e),this.targetCount+=1,fu.resolve()},qh.prototype.updateTargetData=function(t,e){return this.te(e),fu.resolve()},qh.prototype.removeTargetData=function(t,e){return this.ds.delete(e.target),this._s.Zn(e.targetId),--this.targetCount,fu.resolve()},qh.prototype.removeTargets=function(n,r,i){var o=this,s=0,a=[];return this.ds.forEach(function(t,e){e.sequenceNumber<=r&&null===i.get(e.targetId)&&(o.ds.delete(t),a.push(o.removeMatchingKeysForTargetId(n,e.targetId)),s++)}),fu.waitFor(a).next(function(){return s})},qh.prototype.getTargetCount=function(t){return fu.resolve(this.targetCount)},qh.prototype.getTargetData=function(t,e){e=this.ds.get(e)||null;return fu.resolve(e)},qh.prototype.addMatchingKeys=function(t,e,n){return this._s.Jn(e,n),fu.resolve()},qh.prototype.removeMatchingKeys=function(e,t,n){this._s.Xn(t,n);var r=this.persistence.referenceDelegate,i=[];return r&&t.forEach(function(t){i.push(r.markPotentiallyOrphaned(e,t))}),fu.waitFor(i)},qh.prototype.removeMatchingKeysForTargetId=function(t,e){return this._s.Zn(e),fu.resolve()},qh.prototype.getMatchingKeysForTargetId=function(t,e){e=this._s.es(e);return fu.resolve(e)},qh.prototype.containsKey=function(t,e){return fu.resolve(this._s.containsKey(e))},qh),xh=(Uh.prototype.start=function(){return Promise.resolve()},Uh.prototype.shutdown=function(){return this.xe=!1,Promise.resolve()},Object.defineProperty(Uh.prototype,"started",{get:function(){return this.xe},enumerable:!1,configurable:!0}),Uh.prototype.setDatabaseDeletedListener=function(){},Uh.prototype.setNetworkEnabled=function(){},Uh.prototype.getIndexManager=function(){return this.Ut},Uh.prototype.getMutationQueue=function(t){var e=this.gs[t.toKey()];return e||(e=new Nh(this.Ut,this.referenceDelegate),this.gs[t.toKey()]=e),e},Uh.prototype.getTargetCache=function(){return this.qe},Uh.prototype.getRemoteDocumentCache=function(){return this.Ue},Uh.prototype.getBundleCache=function(){return this.Ke},Uh.prototype.runTransaction=function(t,e,n){var r=this;Kr("MemoryPersistence","Starting transaction:",t);var i=new Oh(this.Ne.next());return this.referenceDelegate.Es(),n(i).next(function(t){return r.referenceDelegate.Ts(i).next(function(){return t})}).toPromise().then(function(t){return i.raiseOnCommittedEvent(),t})},Uh.prototype.Is=function(e,n){return fu.or(Object.values(this.gs).map(function(t){return function(){return t.containsKey(e,n)}}))},Uh),Oh=(n(Vh,Ih=R),Vh),Lh=(Fh.bs=function(t){return new Fh(t)},Object.defineProperty(Fh.prototype,"vs",{get:function(){if(this.Rs)return this.Rs;throw zr()},enumerable:!1,configurable:!0}),Fh.prototype.addReference=function(t,e,n){return this.As.addReference(n,e),this.vs.delete(n.toString()),fu.resolve()},Fh.prototype.removeReference=function(t,e,n){return this.As.removeReference(n,e),this.vs.add(n.toString()),fu.resolve()},Fh.prototype.markPotentiallyOrphaned=function(t,e){return this.vs.add(e.toString()),fu.resolve()},Fh.prototype.removeTarget=function(t,e){var n=this;this.As.Zn(e.targetId).forEach(function(t){return n.vs.add(t.toString())});var r=this.persistence.getTargetCache();return r.getMatchingKeysForTargetId(t,e.targetId).next(function(t){t.forEach(function(t){return n.vs.add(t.toString())})}).next(function(){return r.removeTargetData(t,e)})},Fh.prototype.Es=function(){this.Rs=new Set},Fh.prototype.Ts=function(n){var r=this,i=this.persistence.getRemoteDocumentCache().newChangeBuffer();return fu.forEach(this.vs,function(t){var e=Ni.fromPath(t);return r.Ps(n,e).next(function(t){t||i.removeEntry(e)})}).next(function(){return r.Rs=null,i.apply(n)})},Fh.prototype.updateLimboDocument=function(t,e){var n=this;return this.Ps(t,e).next(function(t){t?n.vs.delete(e.toString()):n.vs.add(e.toString())})},Fh.prototype.ps=function(t){return 0},Fh.prototype.Ps=function(t,e){var n=this;return fu.or([function(){return fu.resolve(n.As.containsKey(e))},function(){return n.persistence.getTargetCache().containsKey(t,e)},function(){return n.persistence.Is(t,e)}])},Fh),Ph=(Mh.prototype.isAuthenticated=function(){return null!=this.uid},Mh.prototype.toKey=function(){return this.isAuthenticated()?"uid:"+this.uid:"anonymous-user"},Mh.prototype.isEqual=function(t){return t.uid===this.uid},Mh);function Mh(t){this.uid=t}function Fh(t){this.persistence=t,this.As=new Ah,this.Rs=null}function Vh(t){var e=this;return(e=Ih.call(this)||this).currentSequenceNumber=t,e}function Uh(t,e){var n=this;this.gs={},this.Ne=new Pr(0),this.xe=!1,this.xe=!0,this.referenceDelegate=t(this),this.qe=new Rh(this),this.Ut=new tc,this.Ue=(t=this.Ut,new Ch(t,function(t){return n.referenceDelegate.ps(t)})),this.R=new Mu(e),this.Ke=new Sh(this.R)}function qh(t){this.persistence=t,this.ds=new Fc(Yi,Xi),this.lastRemoteSnapshotVersion=ei.min(),this.highestTargetId=0,this.ws=0,this._s=new Ah,this.targetCount=0,this.ys=vc.Jt()}function Bh(t){var e=this;return(e=_h.call(this)||this).Ie=t,e}function jh(t,e){this.Ut=t,this.ls=e,this.docs=new Vs(Ni.comparator),this.size=0}function Kh(t,e){this.Ut=t,this.referenceDelegate=e,this._n=[],this.ss=1,this.rs=new Qs(Dh.Gn)}function Gh(t,e){this.key=t,this.ns=e}function Qh(){this.Wn=new Qs(Dh.Gn),this.zn=new Qs(Dh.Hn)}function Hh(t){this.R=t,this.Qn=new Map,this.jn=new Map}function zh(t,e){return"firestore_clients_"+t+"_"+e}function Wh(t,e,n){n="firestore_mutations_"+t+"_"+n;return e.isAuthenticated()&&(n+="_"+e.uid),n}function Yh(t,e){return"firestore_targets_"+t+"_"+e}Ph.UNAUTHENTICATED=new Ph(null),Ph.GOOGLE_CREDENTIALS=new Ph("google-credentials-uid"),Ph.FIRST_PARTY=new Ph("first-party-uid"),Ph.MOCK_USER=new Ph("mock-user");var Xh,$h=(bl.Vs=function(t,e,n){var r,i=JSON.parse(n),o="object"==typeof i&&-1!==["pending","acknowledged","rejected"].indexOf(i.state)&&(void 0===i.error||"object"==typeof i.error);return o&&i.error&&(o="string"==typeof i.error.message&&"string"==typeof i.error.code)&&(r=new Ur(i.error.code,i.error.message)),o?new bl(t,e,i.state,r):(Gr("SharedClientState","Failed to parse mutation state for ID '"+e+"': "+n),null)},bl.prototype.Ss=function(){var t={state:this.state,updateTimeMs:Date.now()};return this.error&&(t.error={code:this.error.code,message:this.error.message}),JSON.stringify(t)},bl),Jh=(wl.Vs=function(t,e){var n,r=JSON.parse(e),i="object"==typeof r&&-1!==["not-current","current","rejected"].indexOf(r.state)&&(void 0===r.error||"object"==typeof r.error);return i&&r.error&&(i="string"==typeof r.error.message&&"string"==typeof r.error.code)&&(n=new Ur(r.error.code,r.error.message)),i?new wl(t,r.state,n):(Gr("SharedClientState","Failed to parse target state for ID '"+t+"': "+e),null)},wl.prototype.Ss=function(){var t={state:this.state,updateTimeMs:Date.now()};return this.error&&(t.error={code:this.error.code,message:this.error.message}),JSON.stringify(t)},wl),Zh=(vl.Vs=function(t,e){for(var n=JSON.parse(e),r="object"==typeof n&&n.activeTargetIds instanceof Array,i=ta,o=0;r&&othis.Bi&&(this.qi=this.Bi)},Vl.prototype.Gi=function(){null!==this.Ui&&(this.Ui.skipDelay(),this.Ui=null)},Vl.prototype.cancel=function(){null!==this.Ui&&(this.Ui.cancel(),this.Ui=null)},Vl.prototype.Wi=function(){return(Math.random()-.5)*this.qi},Vl),A=(Fl.prototype.tr=function(){return 1===this.state||2===this.state||4===this.state},Fl.prototype.er=function(){return 2===this.state},Fl.prototype.start=function(){3!==this.state?this.auth():this.nr()},Fl.prototype.stop=function(){return y(this,void 0,void 0,function(){return g(this,function(t){switch(t.label){case 0:return this.tr()?[4,this.close(0)]:[3,2];case 1:t.sent(),t.label=2;case 2:return[2]}})})},Fl.prototype.sr=function(){this.state=0,this.Zi.reset()},Fl.prototype.ir=function(){var t=this;this.er()&&null===this.Xi&&(this.Xi=this.Se.enqueueAfterDelay(this.zi,6e4,function(){return t.rr()}))},Fl.prototype.cr=function(t){this.ur(),this.stream.send(t)},Fl.prototype.rr=function(){return y(this,void 0,void 0,function(){return g(this,function(t){return this.er()?[2,this.close(0)]:[2]})})},Fl.prototype.ur=function(){this.Xi&&(this.Xi.cancel(),this.Xi=null)},Fl.prototype.close=function(e,n){return y(this,void 0,void 0,function(){return g(this,function(t){switch(t.label){case 0:return this.ur(),this.Zi.cancel(),this.Yi++,3!==e?this.Zi.reset():n&&n.code===Vr.RESOURCE_EXHAUSTED?(Gr(n.toString()),Gr("Using maximum backoff delay to prevent overloading the backend."),this.Zi.Qi()):n&&n.code===Vr.UNAUTHENTICATED&&this.Ji.invalidateToken(),null!==this.stream&&(this.ar(),this.stream.close(),this.stream=null),this.state=e,[4,this.listener.Ri(n)];case 1:return t.sent(),[2]}})})},Fl.prototype.ar=function(){},Fl.prototype.auth=function(){var n=this;this.state=1;var t=this.hr(this.Yi),e=this.Yi;this.Ji.getToken().then(function(t){n.Yi===e&&n.lr(t)},function(e){t(function(){var t=new Ur(Vr.UNKNOWN,"Fetching auth token failed: "+e.message);return n.dr(t)})})},Fl.prototype.lr=function(t){var e=this,n=this.hr(this.Yi);this.stream=this.wr(t),this.stream.Ii(function(){n(function(){return e.state=2,e.listener.Ii()})}),this.stream.Ri(function(t){n(function(){return e.dr(t)})}),this.stream.onMessage(function(t){n(function(){return e.onMessage(t)})})},Fl.prototype.nr=function(){var t=this;this.state=4,this.Zi.ji(function(){return y(t,void 0,void 0,function(){return g(this,function(t){return this.state=0,this.start(),[2]})})})},Fl.prototype.dr=function(t){return Kr("PersistentStream","close with error: "+t),this.stream=null,this.close(3,t)},Fl.prototype.hr=function(e){var n=this;return function(t){n.Se.enqueueAndForget(function(){return n.Yi===e?t():(Kr("PersistentStream","stream callback skipped by getCloseGuardedDispatcher."),Promise.resolve())})}},Fl),Cl=(n(Ml,Dl=A),Ml.prototype.wr=function(t){return this.Hi.Oi("Listen",t)},Ml.prototype.onMessage=function(t){this.Zi.reset();var e=function(t,e){if("targetChange"in e){e.targetChange;var n="NO_CHANGE"===(o=e.targetChange.targetChangeType||"NO_CHANGE")?0:"ADD"===o?1:"REMOVE"===o?2:"CURRENT"===o?3:"RESET"===o?4:zr(),r=e.targetChange.targetIds||[],i=(s=e.targetChange.resumeToken,t.I?(Wr(void 0===s||"string"==typeof s),di.fromBase64String(s||"")):(Wr(void 0===s||s instanceof Uint8Array),di.fromUint8Array(s||new Uint8Array))),o=(a=e.targetChange.cause)&&(u=void 0===(c=a).code?Vr.UNKNOWN:Fs(c.code),new Ur(u,c.message||"")),s=new oa(n,r,i,o||null)}else if("documentChange"in e){e.documentChange,(n=e.documentChange).document,n.document.name,n.document.updateTime;var r=Ia(t,n.document.name),i=wa(n.document.updateTime),a=new Ki({mapValue:{fields:n.document.fields}}),u=(o=Qi.newFoundDocument(r,i,a),n.targetIds||[]),c=n.removedTargetIds||[];s=new ra(u,c,o.key,o)}else if("documentDelete"in e)e.documentDelete,(n=e.documentDelete).document,r=Ia(t,n.document),i=n.readTime?wa(n.readTime):ei.min(),a=Qi.newNoDocument(r,i),o=n.removedTargetIds||[],s=new ra([],o,a.key,a);else if("documentRemove"in e)e.documentRemove,(n=e.documentRemove).document,r=Ia(t,n.document),i=n.removedTargetIds||[],s=new ra([],i,r,null);else{if(!("filter"in e))return zr();e.filter;e=e.filter;e.targetId,n=e.count||0,r=new Ns(n),i=e.targetId,s=new ia(i,r)}return s}(this.R,t),t=function(t){if(!("targetChange"in t))return ei.min();t=t.targetChange;return(!t.targetIds||!t.targetIds.length)&&t.readTime?wa(t.readTime):ei.min()}(t);return this.listener._r(e,t)},Ml.prototype.mr=function(t){var e,n,r,i={};i.database=Aa(this.R),i.addTarget=(e=this.R,(r=$i(r=(n=t).target)?{documents:xa(e,r)}:{query:Oa(e,r)}).targetId=n.targetId,0this.query.limit;){var n=xo(this.query)?h.last():h.first(),h=h.delete(n.key),c=c.delete(n.key);a.track({type:1,doc:n})}return{fo:h,mo:a,Nn:l,mutatedKeys:c}},Pf.prototype.yo=function(t,e){return t.hasLocalMutations&&e.hasCommittedMutations&&!e.hasLocalMutations},Pf.prototype.applyChanges=function(t,e,n){var o=this,r=this.fo;this.fo=t.fo,this.mutatedKeys=t.mutatedKeys;var i=t.mo.jr();i.sort(function(t,e){return r=t.type,i=e.type,n(r)-n(i)||o.lo(t.doc,e.doc);function n(t){switch(t){case 0:return 1;case 2:case 3:return 2;case 1:return 0;default:return zr()}}var r,i}),this.po(n);var s=e?this.Eo():[],n=0===this.ho.size&&this.current?1:0,e=n!==this.ao;return this.ao=n,0!==i.length||e?{snapshot:new lf(this.query,t.fo,r,i,t.mutatedKeys,0==n,e,!1),To:s}:{To:s}},Pf.prototype.zr=function(t){return this.current&&"Offline"===t?(this.current=!1,this.applyChanges({fo:this.fo,mo:new hf,mutatedKeys:this.mutatedKeys,Nn:!1},!1)):{To:[]}},Pf.prototype.Io=function(t){return!this.uo.has(t)&&!!this.fo.has(t)&&!this.fo.get(t).hasLocalMutations},Pf.prototype.po=function(t){var e=this;t&&(t.addedDocuments.forEach(function(t){return e.uo=e.uo.add(t)}),t.modifiedDocuments.forEach(function(t){}),t.removedDocuments.forEach(function(t){return e.uo=e.uo.delete(t)}),this.current=t.current)},Pf.prototype.Eo=function(){var e=this;if(!this.current)return[];var n=this.ho;this.ho=Zs(),this.fo.forEach(function(t){e.Io(t.key)&&(e.ho=e.ho.add(t.key))});var r=[];return n.forEach(function(t){e.ho.has(t)||r.push(new Cf(t))}),this.ho.forEach(function(t){n.has(t)||r.push(new Nf(t))}),r},Pf.prototype.Ao=function(t){this.uo=t.Bn,this.ho=Zs();t=this._o(t.documents);return this.applyChanges(t,!0)},Pf.prototype.Ro=function(){return lf.fromInitialDocuments(this.query,this.fo,this.mutatedKeys,0===this.ao)},Pf),Rf=function(t,e,n){this.query=t,this.targetId=e,this.view=n},xf=function(t){this.key=t,this.bo=!1},Of=(Object.defineProperty(Lf.prototype,"isPrimaryClient",{get:function(){return!0===this.$o},enumerable:!1,configurable:!0}),Lf);function Lf(t,e,n,r,i,o){this.localStore=t,this.remoteStore=e,this.eventManager=n,this.sharedClientState=r,this.currentUser=i,this.maxConcurrentLimboResolutions=o,this.vo={},this.Po=new Fc(Bo,qo),this.Vo=new Map,this.So=new Set,this.Do=new Vs(Ni.comparator),this.Co=new Map,this.No=new Ah,this.xo={},this.ko=new Map,this.Fo=vc.Yt(),this.onlineState="Unknown",this.$o=void 0}function Pf(t,e){this.query=t,this.uo=e,this.ao=null,this.current=!1,this.ho=Zs(),this.mutatedKeys=Zs(),this.lo=Go(t),this.fo=new cf(this.lo)}function Mf(i,o,s,a){return y(this,void 0,void 0,function(){var e,n,r;return g(this,function(t){switch(t.label){case 0:return i.Oo=function(t,e,n){return function(r,i,o,s){return y(this,void 0,void 0,function(){var e,n;return g(this,function(t){switch(t.label){case 0:return(e=i.view._o(o)).Nn?[4,wh(r.localStore,i.query,!1).then(function(t){t=t.documents;return i.view._o(t,e)})]:[3,2];case 1:e=t.sent(),t.label=2;case 2:return n=s&&s.targetChanges.get(i.targetId),n=i.view.applyChanges(e,r.isPrimaryClient,n),[2,(Hf(r,i.targetId,n.To),n.snapshot)]}})})}(i,t,e,n)},[4,wh(i.localStore,o,!0)];case 1:return n=t.sent(),r=new kf(o,n.Bn),e=r._o(n.documents),n=na.createSynthesizedTargetChangeForCurrentChange(s,a&&"Offline"!==i.onlineState),n=r.applyChanges(e,i.isPrimaryClient,n),Hf(i,s,n.To),r=new Rf(o,s,r),[2,(i.Po.set(o,r),i.Vo.has(s)?i.Vo.get(s).push(o):i.Vo.set(s,[o]),n.snapshot)]}})})}function Ff(f,d,p){return y(this,void 0,void 0,function(){var s,l;return g(this,function(t){switch(t.label){case 0:l=ed(f),t.label=1;case 1:return t.trys.push([1,5,,6]),[4,(i=l.localStore,a=d,c=i,h=ti.now(),o=a.reduce(function(t,e){return t.add(e.key)},Zs()),c.persistence.runTransaction("Locally write mutations","readwrite",function(s){return c.Mn.pn(s,o).next(function(t){u=t;for(var e=[],n=0,r=a;n, or >=) must be on the same field. But you have inequality filters on '"+n.toString()+"' and '"+e.field.toString()+"'");n=Lo(t);null!==n&&ag(0,e.field,n)}t=function(t,e){for(var n=0,r=t.filters;ns.length)throw new Ur(Vr.INVALID_ARGUMENT,"Too many arguments provided to "+r+"(). The number of arguments must be less than or equal to the number of orderBy() clauses");for(var a=[],u=0;u, or >=) on field '"+e.toString()+"' and so you must also use '"+e.toString()+"' as your first argument to orderBy(), but your first orderBy() is on field '"+n.toString()+"' instead.")}ug.prototype.convertValue=function(t,e){switch(void 0===e&&(e="none"),ki(t)){case 0:return null;case 1:return t.booleanValue;case 2:return Ei(t.integerValue||t.doubleValue);case 3:return this.convertTimestamp(t.timestampValue);case 4:return this.convertServerTimestamp(t,e);case 5:return t.stringValue;case 6:return this.convertBytes(Ti(t.bytesValue));case 7:return this.convertReference(t.referenceValue);case 8:return this.convertGeoPoint(t.geoPointValue);case 9:return this.convertArray(t.arrayValue,e);case 10:return this.convertObject(t.mapValue,e);default:throw zr()}},ug.prototype.convertObject=function(t,n){var r=this,i={};return oi(t.fields,function(t,e){i[t]=r.convertValue(e,n)}),i},ug.prototype.convertGeoPoint=function(t){return new Lp(Ei(t.latitude),Ei(t.longitude))},ug.prototype.convertArray=function(t,e){var n=this;return(t.values||[]).map(function(t){return n.convertValue(t,e)})},ug.prototype.convertServerTimestamp=function(t,e){switch(e){case"previous":var n=function t(e){e=e.mapValue.fields.__previous_value__;return Ii(e)?t(e):e}(t);return null==n?null:this.convertValue(n,e);case"estimate":return this.convertTimestamp(_i(t));default:return null}},ug.prototype.convertTimestamp=function(t){t=bi(t);return new ti(t.seconds,t.nanos)},ug.prototype.convertDocumentKey=function(t,e){var n=ci.fromString(t);Wr(Ba(n));t=new Fd(n.get(1),n.get(3)),n=new Ni(n.popFirst(5));return t.isEqual(e)||Gr("Document "+n+" contains a document reference within a different database ("+t.projectId+"/"+t.database+") which is not supported. It will be treated as a reference in the current database ("+e.projectId+"/"+e.database+") instead."),n},A=ug;function ug(){}function cg(t,e,n){return t?n&&(n.merge||n.mergeFields)?t.toFirestore(e,n):t.toFirestore(e):e}var hg,lg=(n(pg,hg=A),pg.prototype.convertBytes=function(t){return new xp(t)},pg.prototype.convertReference=function(t){t=this.convertDocumentKey(t,this.firestore._databaseId);return new ap(this.firestore,null,t)},pg),fg=(dg.prototype.set=function(t,e,n){this._verifyNotCommitted();t=yg(t,this._firestore),e=cg(t.converter,e,n),n=Yp(this._dataReader,"WriteBatch.set",t._key,e,null!==t.converter,n);return this._mutations.push(n.toMutation(t._key,ds.none())),this},dg.prototype.update=function(t,e,n){for(var r=[],i=3;ia[0]&&t[1]=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function f(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||0"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new c(o,e,i)},v);function v(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var m=/\{\$([^}]+)}/g;function y(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function b(e,t){t=new g(e,t);return t.subscribe.bind(t)}var g=(I.prototype.next=function(t){this.forEachObserver(function(e){e.next(t)})},I.prototype.error=function(t){this.forEachObserver(function(e){e.error(t)}),this.close(t)},I.prototype.complete=function(){this.forEachObserver(function(e){e.complete()}),this.close()},I.prototype.subscribe=function(e,t,n){var r,i=this;if(void 0===e&&void 0===t&&void 0===n)throw new Error("Missing Observer.");void 0===(r=function(e,t){if("object"!=typeof e||null===e)return!1;for(var n=0,r=t;n=(null!=o?o:e.logLevel)&&a({level:R[t].toLowerCase(),message:i,args:n,type:e.name})}}(n[e])}var H=((H={})["no-app"]="No Firebase App '{$appName}' has been created - call Firebase App.initializeApp()",H["bad-app-name"]="Illegal App name: '{$appName}",H["duplicate-app"]="Firebase App named '{$appName}' already exists",H["app-deleted"]="Firebase App named '{$appName}' already deleted",H["invalid-app-argument"]="firebase.{$appName}() takes either no argument or a Firebase App instance.",H["invalid-log-argument"]="First argument to `onLog` must be null or a function.",H),V=new d("app","Firebase",H),B="@firebase/app",M="[DEFAULT]",U=((H={})[B]="fire-core",H["@firebase/analytics"]="fire-analytics",H["@firebase/app-check"]="fire-app-check",H["@firebase/auth"]="fire-auth",H["@firebase/database"]="fire-rtdb",H["@firebase/functions"]="fire-fn",H["@firebase/installations"]="fire-iid",H["@firebase/messaging"]="fire-fcm",H["@firebase/performance"]="fire-perf",H["@firebase/remote-config"]="fire-rc",H["@firebase/storage"]="fire-gcs",H["@firebase/firestore"]="fire-fst",H["fire-js"]="fire-js",H["firebase-wrapper"]="fire-js-all",H),W=new z("@firebase/app"),G=(Object.defineProperty($.prototype,"automaticDataCollectionEnabled",{get:function(){return this.checkDestroyed_(),this.automaticDataCollectionEnabled_},set:function(e){this.checkDestroyed_(),this.automaticDataCollectionEnabled_=e},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"name",{get:function(){return this.checkDestroyed_(),this.name_},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"options",{get:function(){return this.checkDestroyed_(),this.options_},enumerable:!1,configurable:!0}),$.prototype.delete=function(){var t=this;return new Promise(function(e){t.checkDestroyed_(),e()}).then(function(){return t.firebase_.INTERNAL.removeApp(t.name_),Promise.all(t.container.getProviders().map(function(e){return e.delete()}))}).then(function(){t.isDeleted_=!0})},$.prototype._getService=function(e,t){void 0===t&&(t=M),this.checkDestroyed_();var n=this.container.getProvider(e);return n.isInitialized()||"EXPLICIT"!==(null===(e=n.getComponent())||void 0===e?void 0:e.instantiationMode)||n.initialize(),n.getImmediate({identifier:t})},$.prototype._removeServiceInstance=function(e,t){void 0===t&&(t=M),this.container.getProvider(e).clearInstance(t)},$.prototype._addComponent=function(t){try{this.container.addComponent(t)}catch(e){W.debug("Component "+t.name+" failed to register with FirebaseApp "+this.name,e)}},$.prototype._addOrOverwriteComponent=function(e){this.container.addOrOverwriteComponent(e)},$.prototype.toJSON=function(){return{name:this.name,automaticDataCollectionEnabled:this.automaticDataCollectionEnabled,options:this.options}},$.prototype.checkDestroyed_=function(){if(this.isDeleted_)throw V.create("app-deleted",{appName:this.name_})},$);function $(e,t,n){var r=this;this.firebase_=n,this.isDeleted_=!1,this.name_=t.name,this.automaticDataCollectionEnabled_=t.automaticDataCollectionEnabled||!1,this.options_=h(void 0,e),this.container=new S(t.name),this._addComponent(new E("app",function(){return r},"PUBLIC")),this.firebase_.INTERNAL.components.forEach(function(e){return r._addComponent(e)})}G.prototype.name&&G.prototype.options||G.prototype.delete||console.log("dc");var K="8.9.1";function Y(a){var s={},l=new Map,c={__esModule:!0,initializeApp:function(e,t){void 0===t&&(t={});"object"==typeof t&&null!==t||(t={name:t});var n=t;void 0===n.name&&(n.name=M);t=n.name;if("string"!=typeof t||!t)throw V.create("bad-app-name",{appName:String(t)});if(y(s,t))throw V.create("duplicate-app",{appName:t});n=new a(e,n,c);return s[t]=n},app:u,registerVersion:function(e,t,n){var r=null!==(i=U[e])&&void 0!==i?i:e;n&&(r+="-"+n);var i=r.match(/\s|\//),e=t.match(/\s|\//);i||e?(n=['Unable to register library "'+r+'" with version "'+t+'":'],i&&n.push('library name "'+r+'" contains illegal characters (whitespace or "/")'),i&&e&&n.push("and"),e&&n.push('version name "'+t+'" contains illegal characters (whitespace or "/")'),W.warn(n.join(" "))):o(new E(r+"-version",function(){return{library:r,version:t}},"VERSION"))},setLogLevel:T,onLog:function(e,t){if(null!==e&&"function"!=typeof e)throw V.create("invalid-log-argument");x(e,t)},apps:null,SDK_VERSION:K,INTERNAL:{registerComponent:o,removeApp:function(e){delete s[e]},components:l,useAsService:function(e,t){return"serverAuth"!==t?t:null}}};function u(e){if(!y(s,e=e||M))throw V.create("no-app",{appName:e});return s[e]}function o(n){var e,r=n.name;if(l.has(r))return W.debug("There were multiple attempts to register component "+r+"."),"PUBLIC"===n.type?c[r]:null;l.set(r,n),"PUBLIC"===n.type&&(e=function(e){if("function"!=typeof(e=void 0===e?u():e)[r])throw V.create("invalid-app-argument",{appName:r});return e[r]()},void 0!==n.serviceProps&&h(e,n.serviceProps),c[r]=e,a.prototype[r]=function(){for(var e=[],t=0;ts[0]&&e[1]>6,c=63&c;u||(c=64,s||(h=64)),r.push(n[o>>2],n[(3&o)<<4|a>>4],n[h],n[c])}return r.join("")},encodeString:function(t,e){return this.HAS_NATIVE_SUPPORT&&!e?btoa(t):this.encodeByteArray(function(t){for(var e=[],n=0,r=0;r>6|192:(55296==(64512&i)&&r+1>18|240,e[n++]=i>>12&63|128):e[n++]=i>>12|224,e[n++]=i>>6&63|128),e[n++]=63&i|128)}return e}(t),e)},decodeString:function(t,e){return this.HAS_NATIVE_SUPPORT&&!e?atob(t):function(t){for(var e=[],n=0,r=0;n>10)),e[r++]=String.fromCharCode(56320+(1023&i))):(o=t[n++],s=t[n++],e[r++]=String.fromCharCode((15&a)<<12|(63&o)<<6|63&s))}return e.join("")}(this.decodeStringToByteArray(t,e))},decodeStringToByteArray:function(t,e){this.init_();for(var n=e?this.charToByteMapWebSafe_:this.charToByteMap_,r=[],i=0;i>4),64!==a&&(r.push(s<<4&240|a>>2),64!==u&&r.push(a<<6&192|u))}return r},init_:function(){if(!this.byteToCharMap_){this.byteToCharMap_={},this.charToByteMap_={},this.byteToCharMapWebSafe_={},this.charToByteMapWebSafe_={};for(var t=0;t=this.ENCODED_VALS_BASE.length&&(this.charToByteMap_[this.ENCODED_VALS_WEBSAFE.charAt(t)]=t,this.charToByteMapWebSafe_[this.ENCODED_VALS.charAt(t)]=t)}}};function h(){return"undefined"!=typeof navigator&&"string"==typeof navigator.userAgent?navigator.userAgent:""}function i(){return!function(){try{return"[object process]"===Object.prototype.toString.call(global.process)}catch(t){return}}()&&navigator.userAgent.includes("Safari")&&!navigator.userAgent.includes("Chrome")}var u,c="FirebaseError",l=(n(f,u=Error),f);function f(t,e,n){e=u.call(this,e)||this;return e.code=t,e.customData=n,e.name=c,Object.setPrototypeOf(e,f.prototype),Error.captureStackTrace&&Error.captureStackTrace(e,d.prototype.create),e}var d=(p.prototype.create=function(t){for(var e=[],n=1;n"})):"Error",t=this.serviceName+": "+t+" ("+o+").";return new l(o,t,i)},p);function p(t,e,n){this.service=t,this.serviceName=e,this.errors=n}var m,v=/\{\$([^}]+)}/g;function w(t){return t&&t._delegate?t._delegate:t}(N=m=m||{})[N.DEBUG=0]="DEBUG",N[N.VERBOSE=1]="VERBOSE",N[N.INFO=2]="INFO",N[N.WARN=3]="WARN",N[N.ERROR=4]="ERROR",N[N.SILENT=5]="SILENT";function b(t,e){for(var n=[],r=2;r=t.length?void 0:t)&&t[r++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")}var N,C="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},k={},R=C||self;function x(){}function O(t){var e=typeof t;return"array"==(e="object"!=e?e:t?Array.isArray(t)?"array":e:"null")||"object"==e&&"number"==typeof t.length}function L(t){var e=typeof t;return"object"==e&&null!=t||"function"==e}var P="closure_uid_"+(1e9*Math.random()>>>0),M=0;function F(t,e,n){return t.call.apply(t.bind,arguments)}function V(e,n,t){if(!e)throw Error();if(2parseFloat(dt)){ot=String(pt);break t}}ot=dt}var yt={};function gt(){return t=function(){for(var t=0,e=X(String(ot)).split("."),n=X("9").split("."),r=Math.max(e.length,n.length),i=0;0==t&&i>>0);function Vt(e){return"function"==typeof e?e:(e[Ft]||(e[Ft]=function(t){return e.handleEvent(t)}),e[Ft])}function Ut(){j.call(this),this.i=new At(this),(this.P=this).I=null}function qt(t,e){var n,r=t.I;if(r)for(n=[];r;r=r.I)n.push(r);if(t=t.P,r=e.type||e,"string"==typeof e?e=new wt(e,t):e instanceof wt?e.target=e.target||t:(s=e,rt(e=new wt(r,t),s)),s=!0,n)for(var i=n.length-1;0<=i;i--)var o=e.g=n[i],s=Bt(o,r,!0,e)&&s;if(s=Bt(o=e.g=t,r,!0,e)&&s,s=Bt(o,r,!1,e)&&s,n)for(i=0;io.length?Oe:(o=o.substr(a,s),i.C=a+s,o)))==Oe){4==e&&(t.o=4,ve(14),u=!1),le(t.j,t.m,null,"[Incomplete Response]");break}if(r==xe){t.o=4,ve(15),le(t.j,t.m,n,"[Invalid Chunk]"),u=!1;break}le(t.j,t.m,r,null),Ke(t,r)}Me(t)&&r!=Oe&&r!=xe&&(t.h.g="",t.C=0),4!=e||0!=n.length||t.h.h||(t.o=1,ve(16),u=!1),t.i=t.i&&u,u?0>4&15).toString(16)+(15&t).toString(16)}Ye.prototype.toString=function(){var t=[],e=this.j;e&&t.push(on(e,an,!0),":");var n=this.i;return!n&&"file"!=e||(t.push("//"),(e=this.s)&&t.push(on(e,an,!0),"@"),t.push(encodeURIComponent(String(n)).replace(/%25([0-9a-fA-F]{2})/g,"%$1")),null!=(n=this.m)&&t.push(":",String(n))),(n=this.l)&&(this.i&&"/"!=n.charAt(0)&&t.push("/"),t.push(on(n,"/"==n.charAt(0)?cn:un,!0))),(n=this.h.toString())&&t.push("?",n),(n=this.o)&&t.push("#",on(n,ln)),t.join("")};var an=/[#\/\?@]/g,un=/[#\?:]/g,cn=/[#\?]/g,hn=/[#\?@]/g,ln=/#/g;function fn(t,e){this.h=this.g=null,this.i=t||null,this.j=!!e}function dn(n){n.g||(n.g=new Qe,n.h=0,n.i&&function(t,e){if(t){t=t.split("&");for(var n=0;n2*t.i&&He(t)))}function yn(t,e){return dn(t),e=mn(t,e),ze(t.g.h,e)}function gn(t,e,n){pn(t,e),0=t.j}function In(t){return t.h?1:t.g?t.g.size:0}function _n(t,e){return t.h?t.h==e:t.g&&t.g.has(e)}function Sn(t,e){t.g?t.g.add(e):t.h=e}function An(t,e){t.h&&t.h==e?t.h=null:t.g&&t.g.has(e)&&t.g.delete(e)}function Dn(t){var e,n;if(null!=t.h)return t.i.concat(t.h.D);if(null==t.g||0===t.g.size)return z(t.i);var r=t.i;try{for(var i=D(t.g.values()),o=i.next();!o.done;o=i.next())var s=o.value,r=r.concat(s.D)}catch(t){e={error:t}}finally{try{o&&!o.done&&(n=i.return)&&n.call(i)}finally{if(e)throw e.error}}return r}function Nn(){}function Cn(){this.g=new Nn}function kn(t,e,n,r,i){try{e.onload=null,e.onerror=null,e.onabort=null,e.ontimeout=null,i(r)}catch(t){}}function Rn(t){this.l=t.$b||null,this.j=t.ib||!1}function xn(t,e){Ut.call(this),this.D=t,this.u=e,this.m=void 0,this.readyState=On,this.status=0,this.responseType=this.responseText=this.response=this.statusText="",this.onreadystatechange=null,this.v=new Headers,this.h=null,this.C="GET",this.B="",this.g=!1,this.A=this.j=this.l=null}wn.prototype.cancel=function(){var e,t;if(this.i=Dn(this),this.h)this.h.cancel(),this.h=null;else if(this.g&&0!==this.g.size){try{for(var n=D(this.g.values()),r=n.next();!r.done;r=n.next())r.value.cancel()}catch(t){e={error:t}}finally{try{r&&!r.done&&(t=n.return)&&t.call(n)}finally{if(e)throw e.error}}this.g.clear()}},Nn.prototype.stringify=function(t){return R.JSON.stringify(t,void 0)},Nn.prototype.parse=function(t){return R.JSON.parse(t,void 0)},B(Rn,Te),Rn.prototype.g=function(){return new xn(this.l,this.j)},Rn.prototype.i=(bn={},function(){return bn}),B(xn,Ut);var On=0;function Ln(t){t.j.read().then(t.Sa.bind(t)).catch(t.ha.bind(t))}function Pn(t){t.readyState=4,t.l=null,t.j=null,t.A=null,Mn(t)}function Mn(t){t.onreadystatechange&&t.onreadystatechange.call(t)}(N=xn.prototype).open=function(t,e){if(this.readyState!=On)throw this.abort(),Error("Error reopening a connection");this.C=t,this.B=e,this.readyState=1,Mn(this)},N.send=function(t){if(1!=this.readyState)throw this.abort(),Error("need to call open() first. ");this.g=!0;var e={headers:this.v,method:this.C,credentials:this.m,cache:void 0};t&&(e.body=t),(this.D||R).fetch(new Request(this.B,e)).then(this.Va.bind(this),this.ha.bind(this))},N.abort=function(){this.response=this.responseText="",this.v=new Headers,this.status=0,this.j&&this.j.cancel("Request was aborted."),1<=this.readyState&&this.g&&4!=this.readyState&&(this.g=!1,Pn(this)),this.readyState=On},N.Va=function(t){if(this.g&&(this.l=t,this.h||(this.status=this.l.status,this.statusText=this.l.statusText,this.h=t.headers,this.readyState=2,Mn(this)),this.g&&(this.readyState=3,Mn(this),this.g)))if("arraybuffer"===this.responseType)t.arrayBuffer().then(this.Ta.bind(this),this.ha.bind(this));else if(void 0!==R.ReadableStream&&"body"in t){if(this.j=t.body.getReader(),this.u){if(this.responseType)throw Error('responseType must be empty for "streamBinaryChunks" mode responses.');this.response=[]}else this.response=this.responseText="",this.A=new TextDecoder;Ln(this)}else t.text().then(this.Ua.bind(this),this.ha.bind(this))},N.Sa=function(t){var e;this.g&&(this.u&&t.value?this.response.push(t.value):this.u||(e=t.value||new Uint8Array(0),(e=this.A.decode(e,{stream:!t.done}))&&(this.response=this.responseText+=e)),(t.done?Pn:Mn)(this),3==this.readyState&&Ln(this))},N.Ua=function(t){this.g&&(this.response=this.responseText=t,Pn(this))},N.Ta=function(t){this.g&&(this.response=t,Pn(this))},N.ha=function(){this.g&&Pn(this)},N.setRequestHeader=function(t,e){this.v.append(t,e)},N.getResponseHeader=function(t){return this.h&&this.h.get(t.toLowerCase())||""},N.getAllResponseHeaders=function(){if(!this.h)return"";for(var t=[],e=this.h.entries(),n=e.next();!n.done;)n=n.value,t.push(n[0]+": "+n[1]),n=e.next();return t.join("\r\n")},Object.defineProperty(xn.prototype,"withCredentials",{get:function(){return"include"===this.m},set:function(t){this.m=t?"include":"same-origin"}});var Fn=R.JSON.parse;function Vn(t){Ut.call(this),this.headers=new Qe,this.u=t||null,this.h=!1,this.C=this.g=null,this.H="",this.m=0,this.j="",this.l=this.F=this.v=this.D=!1,this.B=0,this.A=null,this.J=Un,this.K=this.L=!1}B(Vn,Ut);var Un="",qn=/^https?$/i,Bn=["POST","PUT"];function jn(t){return"content-type"==t.toLowerCase()}function Kn(t,e){t.h=!1,t.g&&(t.l=!0,t.g.abort(),t.l=!1),t.j=e,t.m=5,Gn(t),Hn(t)}function Gn(t){t.D||(t.D=!0,qt(t,"complete"),qt(t,"error"))}function Qn(t){if(t.h&&void 0!==k&&(!t.C[1]||4!=Wn(t)||2!=t.ba()))if(t.v&&4==Wn(t))ne(t.Fa,0,t);else if(qt(t,"readystatechange"),4==Wn(t)){t.h=!1;try{var e,n,r,i,o=t.ba();t:switch(o){case 200:case 201:case 202:case 204:case 206:case 304:case 1223:var s=!0;break t;default:s=!1}if((e=s)||((n=0===o)&&(!(i=String(t.H).match(We)[1]||null)&&R.self&&R.self.location&&(i=(r=R.self.location.protocol).substr(0,r.length-1)),n=!qn.test(i?i.toLowerCase():"")),e=n),e)qt(t,"complete"),qt(t,"success");else{t.m=6;try{var a=2=r.i.j-(r.m?1:0)||(r.m?(r.l=i.D.concat(r.l),0):1==r.G||2==r.G||r.C>=(r.Xa?0:r.Ya)||(r.m=be(U(r.Ha,r,i),dr(r,r.C)),r.C++,0))))&&(2!=s||!ur(t)))switch(o&&0e.length?1:0},gi),ai=(n(yi,si=C),yi.prototype.construct=function(t,e,n){return new yi(t,e,n)},yi.prototype.canonicalString=function(){return this.toArray().join("/")},yi.prototype.toString=function(){return this.canonicalString()},yi.fromString=function(){for(var t=[],e=0;et.length&&Qr(),void 0===n?n=t.length-e:n>t.length-e&&Qr(),this.segments=t,this.offset=e,this.len=n}li.EMPTY_BYTE_STRING=new li("");var mi=new RegExp(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.(\d+))?Z$/);function vi(t){if(Hr(!!t),"string"!=typeof t)return{seconds:wi(t.seconds),nanos:wi(t.nanos)};var e=0,n=mi.exec(t);Hr(!!n),n[1]&&(n=((n=n[1])+"000000000").substr(0,9),e=Number(n));t=new Date(t);return{seconds:Math.floor(t.getTime()/1e3),nanos:e}}function wi(t){return"number"==typeof t?t:"string"==typeof t?Number(t):0}function bi(t){return"string"==typeof t?li.fromBase64String(t):li.fromUint8Array(t)}function Ei(t){return"server_timestamp"===(null===(t=((null===(t=null==t?void 0:t.mapValue)||void 0===t?void 0:t.fields)||{}).__type__)||void 0===t?void 0:t.stringValue)}function Ti(t){t=vi(t.mapValue.fields.__local_write_time__.timestampValue);return new Jr(t.seconds,t.nanos)}function Ii(t){return null==t}function _i(t){return 0===t&&1/t==-1/0}function Si(t){return"number"==typeof t&&Number.isInteger(t)&&!_i(t)&&t<=Number.MAX_SAFE_INTEGER&&t>=Number.MIN_SAFE_INTEGER}var Ai=(Di.fromPath=function(t){return new Di(ai.fromString(t))},Di.fromName=function(t){return new Di(ai.fromString(t).popFirst(5))},Di.prototype.hasCollectionId=function(t){return 2<=this.path.length&&this.path.get(this.path.length-2)===t},Di.prototype.isEqual=function(t){return null!==t&&0===ai.comparator(this.path,t.path)},Di.prototype.toString=function(){return this.path.toString()},Di.comparator=function(t,e){return ai.comparator(t.path,e.path)},Di.isDocumentKey=function(t){return t.length%2==0},Di.fromSegments=function(t){return new Di(new ai(t.slice()))},Di);function Di(t){this.path=t}function Ni(t){return"nullValue"in t?0:"booleanValue"in t?1:"integerValue"in t||"doubleValue"in t?2:"timestampValue"in t?3:"stringValue"in t?5:"bytesValue"in t?6:"referenceValue"in t?7:"geoPointValue"in t?8:"arrayValue"in t?9:"mapValue"in t?Ei(t)?4:10:Qr()}function Ci(r,i){var t,e,n=Ni(r);if(n!==Ni(i))return!1;switch(n){case 0:return!0;case 1:return r.booleanValue===i.booleanValue;case 4:return Ti(r).isEqual(Ti(i));case 3:return function(t){if("string"==typeof r.timestampValue&&"string"==typeof t.timestampValue&&r.timestampValue.length===t.timestampValue.length)return r.timestampValue===t.timestampValue;var e=vi(r.timestampValue),t=vi(t.timestampValue);return e.seconds===t.seconds&&e.nanos===t.nanos}(i);case 5:return r.stringValue===i.stringValue;case 6:return e=i,bi(r.bytesValue).isEqual(bi(e.bytesValue));case 7:return r.referenceValue===i.referenceValue;case 8:return t=i,wi((e=r).geoPointValue.latitude)===wi(t.geoPointValue.latitude)&&wi(e.geoPointValue.longitude)===wi(t.geoPointValue.longitude);case 2:return function(t,e){if("integerValue"in t&&"integerValue"in e)return wi(t.integerValue)===wi(e.integerValue);if("doubleValue"in t&&"doubleValue"in e){t=wi(t.doubleValue),e=wi(e.doubleValue);return t===e?_i(t)===_i(e):isNaN(t)&&isNaN(e)}return!1}(r,i);case 9:return Xr(r.arrayValue.values||[],i.arrayValue.values||[],Ci);case 10:return function(){var t,e=r.mapValue.fields||{},n=i.mapValue.fields||{};if(ni(e)!==ni(n))return!1;for(t in e)if(e.hasOwnProperty(t)&&(void 0===n[t]||!Ci(e[t],n[t])))return!1;return!0}();default:return Qr()}}function ki(t,e){return void 0!==(t.values||[]).find(function(t){return Ci(t,e)})}function Ri(t,e){var n,r,i,o=Ni(t),s=Ni(e);if(o!==s)return Yr(o,s);switch(o){case 0:return 0;case 1:return Yr(t.booleanValue,e.booleanValue);case 2:return r=e,i=wi(t.integerValue||t.doubleValue),r=wi(r.integerValue||r.doubleValue),i":return 0=":return 0<=t;default:return Qr()}},Ji.prototype.g=function(){return 0<=["<","<=",">",">=","!=","not-in"].indexOf(this.op)},Ji);function Ji(t,e,n){var r=this;return(r=Xi.call(this)||this).field=t,r.op=e,r.value=n,r}var Zi,to,eo,no=(n(ao,eo=$i),ao.prototype.matches=function(t){t=Ai.comparator(t.key,this.key);return this.m(t)},ao),ro=(n(so,to=$i),so.prototype.matches=function(e){return this.keys.some(function(t){return t.isEqual(e.key)})},so),io=(n(oo,Zi=$i),oo.prototype.matches=function(e){return!this.keys.some(function(t){return t.isEqual(e.key)})},oo);function oo(t,e){var n=this;return(n=Zi.call(this,t,"not-in",e)||this).keys=uo(0,e),n}function so(t,e){var n=this;return(n=to.call(this,t,"in",e)||this).keys=uo(0,e),n}function ao(t,e,n){var r=this;return(r=eo.call(this,t,e,n)||this).key=Ai.fromName(n.referenceValue),r}function uo(t,e){return((null===(e=e.arrayValue)||void 0===e?void 0:e.values)||[]).map(function(t){return Ai.fromName(t.referenceValue)})}var co,ho,lo,fo,po=(n(To,fo=$i),To.prototype.matches=function(t){t=t.data.field(this.field);return Mi(t)&&ki(t.arrayValue,this.value)},To),yo=(n(Eo,lo=$i),Eo.prototype.matches=function(t){t=t.data.field(this.field);return null!==t&&ki(this.value.arrayValue,t)},Eo),go=(n(bo,ho=$i),bo.prototype.matches=function(t){if(ki(this.value.arrayValue,{nullValue:"NULL_VALUE"}))return!1;t=t.data.field(this.field);return null!==t&&!ki(this.value.arrayValue,t)},bo),mo=(n(wo,co=$i),wo.prototype.matches=function(t){var e=this,t=t.data.field(this.field);return!(!Mi(t)||!t.arrayValue.values)&&t.arrayValue.values.some(function(t){return ki(e.value.arrayValue,t)})},wo),vo=function(t,e){this.position=t,this.before=e};function wo(t,e){return co.call(this,t,"array-contains-any",e)||this}function bo(t,e){return ho.call(this,t,"not-in",e)||this}function Eo(t,e){return lo.call(this,t,"in",e)||this}function To(t,e){return fo.call(this,t,"array-contains",e)||this}function Io(t){return(t.before?"b":"a")+":"+t.position.map(Oi).join(",")}var _o=function(t,e){void 0===e&&(e="asc"),this.field=t,this.dir=e};function So(t,e,n){for(var r=0,i=0;i":"GREATER_THAN",">=":"GREATER_THAN_OR_EQUAL","==":"EQUAL","!=":"NOT_EQUAL","array-contains":"ARRAY_CONTAINS",in:"IN","not-in":"NOT_IN","array-contains-any":"ARRAY_CONTAINS_ANY"},pa=function(t,e){this.databaseId=t,this.I=e};function ya(t,e){return t.I?new Date(1e3*e.seconds).toISOString().replace(/\.\d*/,"").replace("Z","")+"."+("000000000"+e.nanoseconds).slice(-9)+"Z":{seconds:""+e.seconds,nanos:e.nanoseconds}}function ga(t,e){return t.I?e.toBase64():e.toUint8Array()}function ma(t){return Hr(!!t),Zr.fromTimestamp((t=vi(t),new Jr(t.seconds,t.nanos)))}function va(t,e){return new ai(["projects",t.projectId,"databases",t.database]).child("documents").child(e).canonicalString()}function wa(t){t=ai.fromString(t);return Hr(Ua(t)),t}function ba(t,e){return va(t.databaseId,e.path)}function Ea(t,e){e=wa(e);if(e.get(1)!==t.databaseId.projectId)throw new Fr(Mr.INVALID_ARGUMENT,"Tried to deserialize key from different project: "+e.get(1)+" vs "+t.databaseId.projectId);if(e.get(3)!==t.databaseId.database)throw new Fr(Mr.INVALID_ARGUMENT,"Tried to deserialize key from different database: "+e.get(3)+" vs "+t.databaseId.database);return new Ai(Sa(e))}function Ta(t,e){return va(t.databaseId,e)}function Ia(t){t=wa(t);return 4===t.length?ai.emptyPath():Sa(t)}function _a(t){return new ai(["projects",t.databaseId.projectId,"databases",t.databaseId.database]).canonicalString()}function Sa(t){return Hr(4";case"GREATER_THAN_OR_EQUAL":return">=";case"LESS_THAN":return"<";case"LESS_THAN_OR_EQUAL":return"<=";case"ARRAY_CONTAINS":return"array-contains";case"IN":return"in";case"NOT_IN":return"not-in";case"ARRAY_CONTAINS_ANY":return"array-contains-any";case"OPERATOR_UNSPECIFIED":default:return Qr()}}(),t.fieldFilter.value)}function Va(t){switch(t.unaryFilter.op){case"IS_NAN":var e=Ma(t.unaryFilter.field);return $i.create(e,"==",{doubleValue:NaN});case"IS_NULL":e=Ma(t.unaryFilter.field);return $i.create(e,"==",{nullValue:"NULL_VALUE"});case"IS_NOT_NAN":var n=Ma(t.unaryFilter.field);return $i.create(n,"!=",{doubleValue:NaN});case"IS_NOT_NULL":n=Ma(t.unaryFilter.field);return $i.create(n,"!=",{nullValue:"NULL_VALUE"});case"OPERATOR_UNSPECIFIED":default:return Qr()}}function Ua(t){return 4<=t.length&&"projects"===t.get(0)&&"databases"===t.get(2)}function qa(t){for(var e="",n=0;n",t),this.store.put(t));return _u(t)},Iu.prototype.add=function(t){return Br("SimpleDb","ADD",this.store.name,t,t),_u(this.store.add(t))},Iu.prototype.get=function(e){var n=this;return _u(this.store.get(e)).next(function(t){return Br("SimpleDb","GET",n.store.name,e,t=void 0===t?null:t),t})},Iu.prototype.delete=function(t){return Br("SimpleDb","DELETE",this.store.name,t),_u(this.store.delete(t))},Iu.prototype.count=function(){return Br("SimpleDb","COUNT",this.store.name),_u(this.store.count())},Iu.prototype.Nt=function(t,e){var e=this.cursor(this.options(t,e)),n=[];return this.xt(e,function(t,e){n.push(e)}).next(function(){return n})},Iu.prototype.Ft=function(t,e){Br("SimpleDb","DELETE ALL",this.store.name);e=this.options(t,e);e.kt=!1;e=this.cursor(e);return this.xt(e,function(t,e,n){return n.delete()})},Iu.prototype.$t=function(t,e){e?n=t:(n={},e=t);var n=this.cursor(n);return this.xt(n,e)},Iu.prototype.Ot=function(r){var t=this.cursor({});return new hu(function(n,e){t.onerror=function(t){t=Au(t.target.error);e(t)},t.onsuccess=function(t){var e=t.target.result;e?r(e.primaryKey,e.value).next(function(t){t?e.continue():n()}):n()}})},Iu.prototype.xt=function(t,i){var o=[];return new hu(function(r,e){t.onerror=function(t){e(t.target.error)},t.onsuccess=function(t){var e,n=t.target.result;n?(e=new du(n),(t=i(n.primaryKey,n.value,e))instanceof hu&&(t=t.catch(function(t){return e.done(),hu.reject(t)}),o.push(t)),e.isDone?r():null===e.Dt?n.continue():n.continue(e.Dt)):r()}}).next(function(){return hu.waitFor(o)})},Iu.prototype.options=function(t,e){var n;return void 0!==t&&("string"==typeof t?n=t:e=t),{index:n,range:e}},Iu.prototype.cursor=function(t){var e="next";if(t.reverse&&(e="prev"),t.index){var n=this.store.index(t.index);return t.kt?n.openKeyCursor(t.range,e):n.openCursor(t.range,e)}return this.store.openCursor(t.range,e)},Iu);function Iu(t){this.store=t}function _u(t){return new hu(function(e,n){t.onsuccess=function(t){t=t.target.result;e(t)},t.onerror=function(t){t=Au(t.target.error);n(t)}})}var Su=!1;function Au(t){var e=fu._t(h());if(12.2<=e&&e<13){e="An internal error was encountered in the Indexed Database server";if(0<=t.message.indexOf(e)){var n=new Fr("internal","IOS_INDEXEDDB_BUG1: IndexedDb has thrown '"+e+"'. This is likely due to an unavoidable bug in iOS. See https://stackoverflow.com/q/56496296/110915 for details and a potential workaround.");return Su||(Su=!0,setTimeout(function(){throw n},0)),n}}return t}var Du,Nu=(n(Cu,Du=C),Cu);function Cu(t,e){var n=this;return(n=Du.call(this)||this).Mt=t,n.currentSequenceNumber=e,n}function ku(t,e){return fu.It(t.Mt,e)}var Ru=(Fu.prototype.applyToRemoteDocument=function(t,e){for(var n,r,i,o,s,a,u=e.mutationResults,c=0;c=i),o=Gu(r.R,e)),n.done()}).next(function(){return o})},lc.prototype.getHighestUnacknowledgedBatchId=function(t){var e=IDBKeyRange.upperBound([this.userId,Number.POSITIVE_INFINITY]),r=-1;return dc(t).$t({index:Ha.userMutationsIndex,range:e,reverse:!0},function(t,e,n){r=e.batchId,n.done()}).next(function(){return r})},lc.prototype.getAllMutationBatches=function(t){var e=this,n=IDBKeyRange.bound([this.userId,-1],[this.userId,Number.POSITIVE_INFINITY]);return dc(t).Nt(Ha.userMutationsIndex,n).next(function(t){return t.map(function(t){return Gu(e.R,t)})})},lc.prototype.getAllMutationBatchesAffectingDocumentKey=function(o,s){var a=this,t=za.prefixForPath(this.userId,s.path),t=IDBKeyRange.lowerBound(t),u=[];return pc(o).$t({range:t},function(t,e,n){var r=t[0],i=t[1],t=t[2],i=ja(i);if(r===a.userId&&s.path.isEqual(i))return dc(o).get(t).next(function(t){if(!t)throw Qr();Hr(t.userId===a.userId),u.push(Gu(a.R,t))});n.done()}).next(function(){return u})},lc.prototype.getAllMutationBatchesAffectingDocumentKeys=function(e,t){var s=this,a=new Ks(Yr),n=[];return t.forEach(function(o){var t=za.prefixForPath(s.userId,o.path),t=IDBKeyRange.lowerBound(t),t=pc(e).$t({range:t},function(t,e,n){var r=t[0],i=t[1],t=t[2],i=ja(i);r===s.userId&&o.path.isEqual(i)?a=a.add(t):n.done()});n.push(t)}),hu.waitFor(n).next(function(){return s.Wt(e,a)})},lc.prototype.getAllMutationBatchesAffectingQuery=function(t,e){var o=this,s=e.path,a=s.length+1,e=za.prefixForPath(this.userId,s),e=IDBKeyRange.lowerBound(e),u=new Ks(Yr);return pc(t).$t({range:e},function(t,e,n){var r=t[0],i=t[1],t=t[2],i=ja(i);r===o.userId&&s.isPrefixOf(i)?i.length===a&&(u=u.add(t)):n.done()}).next(function(){return o.Wt(t,u)})},lc.prototype.Wt=function(e,t){var n=this,r=[],i=[];return t.forEach(function(t){i.push(dc(e).get(t).next(function(t){if(null===t)throw Qr();Hr(t.userId===n.userId),r.push(Gu(n.R,t))}))}),hu.waitFor(i).next(function(){return r})},lc.prototype.removeMutationBatch=function(e,n){var r=this;return uc(e.Mt,this.userId,n).next(function(t){return e.addOnCommittedListener(function(){r.Gt(n.batchId)}),hu.forEach(t,function(t){return r.referenceDelegate.markPotentiallyOrphaned(e,t)})})},lc.prototype.Gt=function(t){delete this.Kt[t]},lc.prototype.performConsistencyCheck=function(e){var i=this;return this.checkEmpty(e).next(function(t){if(!t)return hu.resolve();var t=IDBKeyRange.lowerBound(za.prefixForUser(i.userId)),r=[];return pc(e).$t({range:t},function(t,e,n){t[0]===i.userId?(t=ja(t[1]),r.push(t)):n.done()}).next(function(){Hr(0===r.length)})})},lc.prototype.containsKey=function(t,e){return fc(t,this.userId,e)},lc.prototype.zt=function(t){var e=this;return yc(t).get(this.userId).next(function(t){return t||new Qa(e.userId,-1,"")})},lc);function lc(t,e,n,r){this.userId=t,this.R=e,this.Ut=n,this.referenceDelegate=r,this.Kt={}}function fc(t,o,e){var e=za.prefixForPath(o,e.path),s=e[1],e=IDBKeyRange.lowerBound(e),a=!1;return pc(t).$t({range:e,kt:!0},function(t,e,n){var r=t[0],i=t[1];t[2],r===o&&i===s&&(a=!0),n.done()}).next(function(){return a})}function dc(t){return ku(t,Ha.store)}function pc(t){return ku(t,za.store)}function yc(t){return ku(t,Qa.store)}var gc=(wc.prototype.next=function(){return this.Ht+=2,this.Ht},wc.Jt=function(){return new wc(0)},wc.Yt=function(){return new wc(-1)},wc),mc=(vc.prototype.allocateTargetId=function(n){var r=this;return this.Xt(n).next(function(t){var e=new gc(t.highestTargetId);return t.highestTargetId=e.next(),r.Zt(n,t).next(function(){return t.highestTargetId})})},vc.prototype.getLastRemoteSnapshotVersion=function(t){return this.Xt(t).next(function(t){return Zr.fromTimestamp(new Jr(t.lastRemoteSnapshotVersion.seconds,t.lastRemoteSnapshotVersion.nanoseconds))})},vc.prototype.getHighestSequenceNumber=function(t){return this.Xt(t).next(function(t){return t.highestListenSequenceNumber})},vc.prototype.setTargetsMetadata=function(e,n,r){var i=this;return this.Xt(e).next(function(t){return t.highestListenSequenceNumber=n,r&&(t.lastRemoteSnapshotVersion=r.toTimestamp()),n>t.highestListenSequenceNumber&&(t.highestListenSequenceNumber=n),i.Zt(e,t)})},vc.prototype.addTargetData=function(e,n){var r=this;return this.te(e,n).next(function(){return r.Xt(e).next(function(t){return t.targetCount+=1,r.ee(n,t),r.Zt(e,t)})})},vc.prototype.updateTargetData=function(t,e){return this.te(t,e)},vc.prototype.removeTargetData=function(e,t){var n=this;return this.removeMatchingKeysForTargetId(e,t.targetId).next(function(){return bc(e).delete(t.targetId)}).next(function(){return n.Xt(e)}).next(function(t){return Hr(0e.highestTargetId&&(e.highestTargetId=t.targetId,n=!0),t.sequenceNumber>e.highestListenSequenceNumber&&(e.highestListenSequenceNumber=t.sequenceNumber,n=!0),n},vc.prototype.getTargetCount=function(t){return this.Xt(t).next(function(t){return t.targetCount})},vc.prototype.getTargetData=function(t,r){var e=zi(r),e=IDBKeyRange.bound([e,Number.NEGATIVE_INFINITY],[e,Number.POSITIVE_INFINITY]),i=null;return bc(t).$t({range:e,index:Za.queryTargetsIndexName},function(t,e,n){e=Qu(e);Wi(r,e.target)&&(i=e,n.done())}).next(function(){return i})},vc.prototype.addMatchingKeys=function(n,t,r){var i=this,o=[],s=Tc(n);return t.forEach(function(t){var e=qa(t.path);o.push(s.put(new tu(r,e))),o.push(i.referenceDelegate.addReference(n,r,t))}),hu.waitFor(o)},vc.prototype.removeMatchingKeys=function(n,t,r){var i=this,o=Tc(n);return hu.forEach(t,function(t){var e=qa(t.path);return hu.waitFor([o.delete([r,e]),i.referenceDelegate.removeReference(n,r,t)])})},vc.prototype.removeMatchingKeysForTargetId=function(t,e){t=Tc(t),e=IDBKeyRange.bound([e],[e+1],!1,!0);return t.delete(e)},vc.prototype.getMatchingKeysForTargetId=function(t,e){var e=IDBKeyRange.bound([e],[e+1],!1,!0),t=Tc(t),r=$s();return t.$t({range:e,kt:!0},function(t,e,n){t=ja(t[1]),t=new Ai(t);r=r.add(t)}).next(function(){return r})},vc.prototype.containsKey=function(t,e){var e=qa(e.path),e=IDBKeyRange.bound([e],[$r(e)],!1,!0),i=0;return Tc(t).$t({index:tu.documentTargetsIndex,kt:!0,range:e},function(t,e,n){var r=t[0];t[1],0!==r&&(i++,n.done())}).next(function(){return 0h.params.maximumSequenceNumbersToCollect?(Br("LruGarbageCollector","Capping sequence numbers to collect down to the maximum of "+h.params.maximumSequenceNumbersToCollect+" from "+t),h.params.maximumSequenceNumbersToCollect):t,s=Date.now(),h.nthSequenceNumber(e,i)}).next(function(t){return r=t,a=Date.now(),h.removeTargets(e,r,n)}).next(function(t){return o=t,u=Date.now(),h.removeOrphanedDocuments(e,r)}).next(function(t){return c=Date.now(),qr()<=m.DEBUG&&Br("LruGarbageCollector","LRU Garbage Collection\n\tCounted targets in "+(s-l)+"ms\n\tDetermined least recently used "+i+" in "+(a-s)+"ms\n\tRemoved "+o+" targets in "+(u-a)+"ms\n\tRemoved "+t+" documents in "+(c-u)+"ms\nTotal Duration: "+(c-l)+"ms"),hu.resolve({didRun:!0,sequenceNumbersCollected:i,targetsRemoved:o,documentsRemoved:t})})},kc),Nc=(Cc.prototype.he=function(t){var n=this.de(t);return this.db.getTargetCache().getTargetCount(t).next(function(e){return n.next(function(t){return e+t})})},Cc.prototype.de=function(t){var e=0;return this.le(t,function(t){e++}).next(function(){return e})},Cc.prototype.forEachTarget=function(t,e){return this.db.getTargetCache().forEachTarget(t,e)},Cc.prototype.le=function(t,n){return this.we(t,function(t,e){return n(e)})},Cc.prototype.addReference=function(t,e,n){return Oc(t,n)},Cc.prototype.removeReference=function(t,e,n){return Oc(t,n)},Cc.prototype.removeTargets=function(t,e,n){return this.db.getTargetCache().removeTargets(t,e,n)},Cc.prototype.markPotentiallyOrphaned=Oc,Cc.prototype._e=function(t,e){return r=e,i=!1,yc(n=t).Ot(function(t){return fc(n,t,r).next(function(t){return t&&(i=!0),hu.resolve(!t)})}).next(function(){return i});var n,r,i},Cc.prototype.removeOrphanedDocuments=function(n,r){var i=this,o=this.db.getRemoteDocumentCache().newChangeBuffer(),s=[],a=0;return this.we(n,function(e,t){t<=r&&(t=i._e(n,e).next(function(t){if(!t)return a++,o.getEntry(n,e).next(function(){return o.removeEntry(e),Tc(n).delete([0,qa(e.path)])})}),s.push(t))}).next(function(){return hu.waitFor(s)}).next(function(){return o.apply(n)}).next(function(){return a})},Cc.prototype.removeTarget=function(t,e){e=e.withSequenceNumber(t.currentSequenceNumber);return this.db.getTargetCache().updateTargetData(t,e)},Cc.prototype.updateLimboDocument=Oc,Cc.prototype.we=function(t,r){var i,t=Tc(t),o=Or.o;return t.$t({index:tu.documentTargetsIndex},function(t,e){var n=t[0];t[1];t=e.path,e=e.sequenceNumber;0===n?(o!==Or.o&&r(new Ai(ja(i)),o),o=e,i=t):o=Or.o}).next(function(){o!==Or.o&&r(new Ai(ja(i)),o)})},Cc.prototype.getCacheSize=function(t){return this.db.getRemoteDocumentCache().getSize(t)},Cc);function Cc(t,e){this.db=t,this.garbageCollector=new Dc(this,e)}function kc(t,e){this.ae=t,this.params=e}function Rc(t,e){this.garbageCollector=t,this.asyncQueue=e,this.oe=!1,this.ce=null}function xc(t){this.ne=t,this.buffer=new Ks(_c),this.se=0}function Oc(t,e){return Tc(t).put((t=t.currentSequenceNumber,new tu(0,qa(e.path),t)))}var Lc,Pc=(Bc.prototype.get=function(t){var e=this.mapKeyFn(t),e=this.inner[e];if(void 0!==e)for(var n=0,r=e;n "+n),1))},Xc.prototype.We=function(){var t=this;null!==this.document&&"function"==typeof this.document.addEventListener&&(this.ke=function(){t.Se.enqueueAndForget(function(){return t.inForeground="visible"===t.document.visibilityState,t.je()})},this.document.addEventListener("visibilitychange",this.ke),this.inForeground="visible"===this.document.visibilityState)},Xc.prototype.an=function(){this.ke&&(this.document.removeEventListener("visibilitychange",this.ke),this.ke=null)},Xc.prototype.Ge=function(){var t,e=this;"function"==typeof(null===(t=this.window)||void 0===t?void 0:t.addEventListener)&&(this.Fe=function(){e.un(),i()&&navigator.appVersion.match("Version/14")&&e.Se.enterRestrictedMode(!0),e.Se.enqueueAndForget(function(){return e.shutdown()})},this.window.addEventListener("pagehide",this.Fe))},Xc.prototype.hn=function(){this.Fe&&(this.window.removeEventListener("pagehide",this.Fe),this.Fe=null)},Xc.prototype.cn=function(t){var e;try{var n=null!==(null===(e=this.Qe)||void 0===e?void 0:e.getItem(this.on(t)));return Br("IndexedDbPersistence","Client '"+t+"' "+(n?"is":"is not")+" zombied in LocalStorage"),n}catch(t){return jr("IndexedDbPersistence","Failed to get zombied client id.",t),!1}},Xc.prototype.un=function(){if(this.Qe)try{this.Qe.setItem(this.on(this.clientId),String(Date.now()))}catch(t){jr("Failed to set zombie client id.",t)}},Xc.prototype.ln=function(){if(this.Qe)try{this.Qe.removeItem(this.on(this.clientId))}catch(t){}},Xc.prototype.on=function(t){return"firestore_zombie_"+this.persistenceKey+"_"+t},Xc);function Xc(t,e,n,r,i,o,s,a,u,c){if(this.allowTabSynchronization=t,this.persistenceKey=e,this.clientId=n,this.Se=i,this.window=o,this.document=s,this.De=u,this.Ce=c,this.Ne=null,this.xe=!1,this.isPrimary=!1,this.networkEnabled=!0,this.Fe=null,this.inForeground=!1,this.ke=null,this.$e=null,this.Oe=Number.NEGATIVE_INFINITY,this.Me=function(t){return Promise.resolve()},!Xc.yt())throw new Fr(Mr.UNIMPLEMENTED,"This platform is either missing IndexedDB or is known to have an incomplete implementation. Offline persistence has been disabled.");this.referenceDelegate=new Nc(this,r),this.Le=e+"main",this.R=new Lu(a),this.Be=new fu(this.Le,11,new Qc(this.R)),this.qe=new mc(this.referenceDelegate,this.R),this.Ut=new tc,this.Ue=(e=this.R,a=this.Ut,new Mc(e,a)),this.Ke=new Wu,this.window&&this.window.localStorage?this.Qe=this.window.localStorage:(this.Qe=null,!1===c&&jr("IndexedDbPersistence","LocalStorage is unavailable. As a result, persistence may not work reliably. In particular enablePersistence() could fail immediately after refreshing the page."))}function $c(t){return ku(t,Ka.store)}function Jc(t){return ku(t,ru.store)}function Zc(t,e){var n=t.projectId;return t.isDefaultDatabase||(n+="."+t.database),"firestore/"+e+"/"+n+"/"}function th(t,e){this.progress=t,this.wn=e}var eh=(uh.prototype.mn=function(e,n){var r=this;return this._n.getAllMutationBatchesAffectingDocumentKey(e,n).next(function(t){return r.yn(e,n,t)})},uh.prototype.yn=function(t,e,r){return this.Ue.getEntry(t,e).next(function(t){for(var e=0,n=r;ee?this._n[e]:null)},Bh.prototype.getHighestUnacknowledgedBatchId=function(){return hu.resolve(0===this._n.length?-1:this.ss-1)},Bh.prototype.getAllMutationBatches=function(t){return hu.resolve(this._n.slice())},Bh.prototype.getAllMutationBatchesAffectingDocumentKey=function(t,e){var n=this,r=new Sh(e,0),e=new Sh(e,Number.POSITIVE_INFINITY),i=[];return this.rs.forEachInRange([r,e],function(t){t=n.os(t.ns);i.push(t)}),hu.resolve(i)},Bh.prototype.getAllMutationBatchesAffectingDocumentKeys=function(t,e){var n=this,r=new Ks(Yr);return e.forEach(function(t){var e=new Sh(t,0),t=new Sh(t,Number.POSITIVE_INFINITY);n.rs.forEachInRange([e,t],function(t){r=r.add(t.ns)})}),hu.resolve(this.us(r))},Bh.prototype.getAllMutationBatchesAffectingQuery=function(t,e){var n=e.path,r=n.length+1,e=n;Ai.isDocumentKey(e)||(e=e.child(""));var e=new Sh(new Ai(e),0),i=new Ks(Yr);return this.rs.forEachWhile(function(t){var e=t.key.path;return!!n.isPrefixOf(e)&&(e.length===r&&(i=i.add(t.ns)),!0)},e),hu.resolve(this.us(i))},Bh.prototype.us=function(t){var e=this,n=[];return t.forEach(function(t){t=e.os(t);null!==t&&n.push(t)}),n},Bh.prototype.removeMutationBatch=function(n,r){var i=this;Hr(0===this.hs(r.batchId,"removed")),this._n.shift();var o=this.rs;return hu.forEach(r.mutations,function(t){var e=new Sh(t.key,r.batchId);return o=o.delete(e),i.referenceDelegate.markPotentiallyOrphaned(n,t.key)}).next(function(){i.rs=o})},Bh.prototype.Gt=function(t){},Bh.prototype.containsKey=function(t,e){var n=new Sh(e,0),n=this.rs.firstAfterOrEqual(n);return hu.resolve(e.isEqual(n&&n.key))},Bh.prototype.performConsistencyCheck=function(t){return this._n.length,hu.resolve()},Bh.prototype.hs=function(t,e){return this.cs(t)},Bh.prototype.cs=function(t){return 0===this._n.length?0:t-this._n[0].batchId},Bh.prototype.os=function(t){t=this.cs(t);return t<0||t>=this._n.length?null:this._n[t]},Bh),Dh=(qh.prototype.addEntry=function(t,e,n){var r=e.key,i=this.docs.get(r),o=i?i.size:0,i=this.ls(e);return this.docs=this.docs.insert(r,{document:e.clone(),size:i,readTime:n}),this.size+=i-o,this.Ut.addToCollectionParentIndex(t,r.path.popLast())},qh.prototype.removeEntry=function(t){var e=this.docs.get(t);e&&(this.docs=this.docs.remove(t),this.size-=e.size)},qh.prototype.getEntry=function(t,e){var n=this.docs.get(e);return hu.resolve(n?n.document.clone():Ki.newInvalidDocument(e))},qh.prototype.getEntries=function(t,e){var n=this,r=Qs;return e.forEach(function(t){var e=n.docs.get(t);r=r.insert(t,e?e.document.clone():Ki.newInvalidDocument(t))}),hu.resolve(r)},qh.prototype.getDocumentsMatchingQuery=function(t,e,n){for(var r=Qs,i=new Ai(e.path.child("")),o=this.docs.getIteratorFrom(i);o.hasNext();){var s=o.getNext(),a=s.key,u=s.value,s=u.document,u=u.readTime;if(!e.path.isPrefixOf(a.path))break;u.compareTo(n)<=0||Bo(e,s)&&(r=r.insert(s.key,s.clone()))}return hu.resolve(r)},qh.prototype.fs=function(t,e){return hu.forEach(this.docs,function(t){return e(t)})},qh.prototype.newChangeBuffer=function(t){return new Nh(this)},qh.prototype.getSize=function(t){return hu.resolve(this.size)},qh),Nh=(n(Uh,Th=_),Uh.prototype.applyChanges=function(n){var r=this,i=[];return this.changes.forEach(function(t,e){e.document.isValidDocument()?i.push(r.Ie.addEntry(n,e.document,r.getReadTime(t))):r.Ie.removeEntry(t)}),hu.waitFor(i)},Uh.prototype.getFromCache=function(t,e){return this.Ie.getEntry(t,e)},Uh.prototype.getAllFromCache=function(t,e){return this.Ie.getEntries(t,e)},Uh),Ch=(Vh.prototype.forEachTarget=function(t,n){return this.ds.forEach(function(t,e){return n(e)}),hu.resolve()},Vh.prototype.getLastRemoteSnapshotVersion=function(t){return hu.resolve(this.lastRemoteSnapshotVersion)},Vh.prototype.getHighestSequenceNumber=function(t){return hu.resolve(this.ws)},Vh.prototype.allocateTargetId=function(t){return this.highestTargetId=this.ys.next(),hu.resolve(this.highestTargetId)},Vh.prototype.setTargetsMetadata=function(t,e,n){return n&&(this.lastRemoteSnapshotVersion=n),e>this.ws&&(this.ws=e),hu.resolve()},Vh.prototype.te=function(t){this.ds.set(t.target,t);var e=t.targetId;e>this.highestTargetId&&(this.ys=new gc(e),this.highestTargetId=e),t.sequenceNumber>this.ws&&(this.ws=t.sequenceNumber)},Vh.prototype.addTargetData=function(t,e){return this.te(e),this.targetCount+=1,hu.resolve()},Vh.prototype.updateTargetData=function(t,e){return this.te(e),hu.resolve()},Vh.prototype.removeTargetData=function(t,e){return this.ds.delete(e.target),this._s.Zn(e.targetId),--this.targetCount,hu.resolve()},Vh.prototype.removeTargets=function(n,r,i){var o=this,s=0,a=[];return this.ds.forEach(function(t,e){e.sequenceNumber<=r&&null===i.get(e.targetId)&&(o.ds.delete(t),a.push(o.removeMatchingKeysForTargetId(n,e.targetId)),s++)}),hu.waitFor(a).next(function(){return s})},Vh.prototype.getTargetCount=function(t){return hu.resolve(this.targetCount)},Vh.prototype.getTargetData=function(t,e){e=this.ds.get(e)||null;return hu.resolve(e)},Vh.prototype.addMatchingKeys=function(t,e,n){return this._s.Jn(e,n),hu.resolve()},Vh.prototype.removeMatchingKeys=function(e,t,n){this._s.Xn(t,n);var r=this.persistence.referenceDelegate,i=[];return r&&t.forEach(function(t){i.push(r.markPotentiallyOrphaned(e,t))}),hu.waitFor(i)},Vh.prototype.removeMatchingKeysForTargetId=function(t,e){return this._s.Zn(e),hu.resolve()},Vh.prototype.getMatchingKeysForTargetId=function(t,e){e=this._s.es(e);return hu.resolve(e)},Vh.prototype.containsKey=function(t,e){return hu.resolve(this._s.containsKey(e))},Vh),kh=(Fh.prototype.start=function(){return Promise.resolve()},Fh.prototype.shutdown=function(){return this.xe=!1,Promise.resolve()},Object.defineProperty(Fh.prototype,"started",{get:function(){return this.xe},enumerable:!1,configurable:!0}),Fh.prototype.setDatabaseDeletedListener=function(){},Fh.prototype.setNetworkEnabled=function(){},Fh.prototype.getIndexManager=function(){return this.Ut},Fh.prototype.getMutationQueue=function(t){var e=this.gs[t.toKey()];return e||(e=new Ah(this.Ut,this.referenceDelegate),this.gs[t.toKey()]=e),e},Fh.prototype.getTargetCache=function(){return this.qe},Fh.prototype.getRemoteDocumentCache=function(){return this.Ue},Fh.prototype.getBundleCache=function(){return this.Ke},Fh.prototype.runTransaction=function(t,e,n){var r=this;Br("MemoryPersistence","Starting transaction:",t);var i=new Rh(this.Ne.next());return this.referenceDelegate.Es(),n(i).next(function(t){return r.referenceDelegate.Ts(i).next(function(){return t})}).toPromise().then(function(t){return i.raiseOnCommittedEvent(),t})},Fh.prototype.Is=function(e,n){return hu.or(Object.values(this.gs).map(function(t){return function(){return t.containsKey(e,n)}}))},Fh),Rh=(n(Mh,Eh=C),Mh),xh=(Ph.bs=function(t){return new Ph(t)},Object.defineProperty(Ph.prototype,"vs",{get:function(){if(this.Rs)return this.Rs;throw Qr()},enumerable:!1,configurable:!0}),Ph.prototype.addReference=function(t,e,n){return this.As.addReference(n,e),this.vs.delete(n.toString()),hu.resolve()},Ph.prototype.removeReference=function(t,e,n){return this.As.removeReference(n,e),this.vs.add(n.toString()),hu.resolve()},Ph.prototype.markPotentiallyOrphaned=function(t,e){return this.vs.add(e.toString()),hu.resolve()},Ph.prototype.removeTarget=function(t,e){var n=this;this.As.Zn(e.targetId).forEach(function(t){return n.vs.add(t.toString())});var r=this.persistence.getTargetCache();return r.getMatchingKeysForTargetId(t,e.targetId).next(function(t){t.forEach(function(t){return n.vs.add(t.toString())})}).next(function(){return r.removeTargetData(t,e)})},Ph.prototype.Es=function(){this.Rs=new Set},Ph.prototype.Ts=function(n){var r=this,i=this.persistence.getRemoteDocumentCache().newChangeBuffer();return hu.forEach(this.vs,function(t){var e=Ai.fromPath(t);return r.Ps(n,e).next(function(t){t||i.removeEntry(e)})}).next(function(){return r.Rs=null,i.apply(n)})},Ph.prototype.updateLimboDocument=function(t,e){var n=this;return this.Ps(t,e).next(function(t){t?n.vs.delete(e.toString()):n.vs.add(e.toString())})},Ph.prototype.ps=function(t){return 0},Ph.prototype.Ps=function(t,e){var n=this;return hu.or([function(){return hu.resolve(n.As.containsKey(e))},function(){return n.persistence.getTargetCache().containsKey(t,e)},function(){return n.persistence.Is(t,e)}])},Ph),Oh=(Lh.prototype.isAuthenticated=function(){return null!=this.uid},Lh.prototype.toKey=function(){return this.isAuthenticated()?"uid:"+this.uid:"anonymous-user"},Lh.prototype.isEqual=function(t){return t.uid===this.uid},Lh);function Lh(t){this.uid=t}function Ph(t){this.persistence=t,this.As=new _h,this.Rs=null}function Mh(t){var e=this;return(e=Eh.call(this)||this).currentSequenceNumber=t,e}function Fh(t,e){var n=this;this.gs={},this.Ne=new Or(0),this.xe=!1,this.xe=!0,this.referenceDelegate=t(this),this.qe=new Ch(this),this.Ut=new Ju,this.Ue=(t=this.Ut,new Dh(t,function(t){return n.referenceDelegate.ps(t)})),this.R=new Lu(e),this.Ke=new Ih(this.R)}function Vh(t){this.persistence=t,this.ds=new Pc(zi,Wi),this.lastRemoteSnapshotVersion=Zr.min(),this.highestTargetId=0,this.ws=0,this._s=new _h,this.targetCount=0,this.ys=gc.Jt()}function Uh(t){var e=this;return(e=Th.call(this)||this).Ie=t,e}function qh(t,e){this.Ut=t,this.ls=e,this.docs=new Ms(Ai.comparator),this.size=0}function Bh(t,e){this.Ut=t,this.referenceDelegate=e,this._n=[],this.ss=1,this.rs=new Ks(Sh.Gn)}function jh(t,e){this.key=t,this.ns=e}function Kh(){this.Wn=new Ks(Sh.Gn),this.zn=new Ks(Sh.Hn)}function Gh(t){this.R=t,this.Qn=new Map,this.jn=new Map}function Qh(t,e){return"firestore_clients_"+t+"_"+e}function Hh(t,e,n){n="firestore_mutations_"+t+"_"+n;return e.isAuthenticated()&&(n+="_"+e.uid),n}function zh(t,e){return"firestore_targets_"+t+"_"+e}Oh.UNAUTHENTICATED=new Oh(null),Oh.GOOGLE_CREDENTIALS=new Oh("google-credentials-uid"),Oh.FIRST_PARTY=new Oh("first-party-uid");var Wh,Yh=(vl.Vs=function(t,e,n){var r,i=JSON.parse(n),o="object"==typeof i&&-1!==["pending","acknowledged","rejected"].indexOf(i.state)&&(void 0===i.error||"object"==typeof i.error);return o&&i.error&&(o="string"==typeof i.error.message&&"string"==typeof i.error.code)&&(r=new Fr(i.error.code,i.error.message)),o?new vl(t,e,i.state,r):(jr("SharedClientState","Failed to parse mutation state for ID '"+e+"': "+n),null)},vl.prototype.Ss=function(){var t={state:this.state,updateTimeMs:Date.now()};return this.error&&(t.error={code:this.error.code,message:this.error.message}),JSON.stringify(t)},vl),Xh=(ml.Vs=function(t,e){var n,r=JSON.parse(e),i="object"==typeof r&&-1!==["not-current","current","rejected"].indexOf(r.state)&&(void 0===r.error||"object"==typeof r.error);return i&&r.error&&(i="string"==typeof r.error.message&&"string"==typeof r.error.code)&&(n=new Fr(r.error.code,r.error.message)),i?new ml(t,r.state,n):(jr("SharedClientState","Failed to parse target state for ID '"+t+"': "+e),null)},ml.prototype.Ss=function(){var t={state:this.state,updateTimeMs:Date.now()};return this.error&&(t.error={code:this.error.code,message:this.error.message}),JSON.stringify(t)},ml),$h=(gl.Vs=function(t,e){for(var n=JSON.parse(e),r="object"==typeof n&&n.activeTargetIds instanceof Array,i=Js,o=0;r&&othis.Bi&&(this.qi=this.Bi)},Ml.prototype.Gi=function(){null!==this.Ui&&(this.Ui.skipDelay(),this.Ui=null)},Ml.prototype.cancel=function(){null!==this.Ui&&(this.Ui.cancel(),this.Ui=null)},Ml.prototype.Wi=function(){return(Math.random()-.5)*this.qi},Ml),_=(Pl.prototype.tr=function(){return 1===this.state||2===this.state||4===this.state},Pl.prototype.er=function(){return 2===this.state},Pl.prototype.start=function(){3!==this.state?this.auth():this.nr()},Pl.prototype.stop=function(){return y(this,void 0,void 0,function(){return g(this,function(t){switch(t.label){case 0:return this.tr()?[4,this.close(0)]:[3,2];case 1:t.sent(),t.label=2;case 2:return[2]}})})},Pl.prototype.sr=function(){this.state=0,this.Zi.reset()},Pl.prototype.ir=function(){var t=this;this.er()&&null===this.Xi&&(this.Xi=this.Se.enqueueAfterDelay(this.zi,6e4,function(){return t.rr()}))},Pl.prototype.cr=function(t){this.ur(),this.stream.send(t)},Pl.prototype.rr=function(){return y(this,void 0,void 0,function(){return g(this,function(t){return this.er()?[2,this.close(0)]:[2]})})},Pl.prototype.ur=function(){this.Xi&&(this.Xi.cancel(),this.Xi=null)},Pl.prototype.close=function(e,n){return y(this,void 0,void 0,function(){return g(this,function(t){switch(t.label){case 0:return this.ur(),this.Zi.cancel(),this.Yi++,3!==e?this.Zi.reset():n&&n.code===Mr.RESOURCE_EXHAUSTED?(jr(n.toString()),jr("Using maximum backoff delay to prevent overloading the backend."),this.Zi.Qi()):n&&n.code===Mr.UNAUTHENTICATED&&this.Ji.invalidateToken(),null!==this.stream&&(this.ar(),this.stream.close(),this.stream=null),this.state=e,[4,this.listener.Ri(n)];case 1:return t.sent(),[2]}})})},Pl.prototype.ar=function(){},Pl.prototype.auth=function(){var n=this;this.state=1;var t=this.hr(this.Yi),e=this.Yi;this.Ji.getToken().then(function(t){n.Yi===e&&n.lr(t)},function(e){t(function(){var t=new Fr(Mr.UNKNOWN,"Fetching auth token failed: "+e.message);return n.dr(t)})})},Pl.prototype.lr=function(t){var e=this,n=this.hr(this.Yi);this.stream=this.wr(t),this.stream.Ii(function(){n(function(){return e.state=2,e.listener.Ii()})}),this.stream.Ri(function(t){n(function(){return e.dr(t)})}),this.stream.onMessage(function(t){n(function(){return e.onMessage(t)})})},Pl.prototype.nr=function(){var t=this;this.state=4,this.Zi.ji(function(){return y(t,void 0,void 0,function(){return g(this,function(t){return this.state=0,this.start(),[2]})})})},Pl.prototype.dr=function(t){return Br("PersistentStream","close with error: "+t),this.stream=null,this.close(3,t)},Pl.prototype.hr=function(e){var n=this;return function(t){n.Se.enqueueAndForget(function(){return n.Yi===e?t():(Br("PersistentStream","stream callback skipped by getCloseGuardedDispatcher."),Promise.resolve())})}},Pl),Dl=(n(Ll,Sl=_),Ll.prototype.wr=function(t){return this.Hi.Oi("Listen",t)},Ll.prototype.onMessage=function(t){this.Zi.reset();var e=function(t,e){if("targetChange"in e){e.targetChange;var n="NO_CHANGE"===(o=e.targetChange.targetChangeType||"NO_CHANGE")?0:"ADD"===o?1:"REMOVE"===o?2:"CURRENT"===o?3:"RESET"===o?4:Qr(),r=e.targetChange.targetIds||[],i=(s=e.targetChange.resumeToken,t.I?(Hr(void 0===s||"string"==typeof s),li.fromBase64String(s||"")):(Hr(void 0===s||s instanceof Uint8Array),li.fromUint8Array(s||new Uint8Array))),o=(a=e.targetChange.cause)&&(u=void 0===(c=a).code?Mr.UNKNOWN:Ps(c.code),new Fr(u,c.message||"")),s=new ra(n,r,i,o||null)}else if("documentChange"in e){e.documentChange,(n=e.documentChange).document,n.document.name,n.document.updateTime;var r=Ea(t,n.document.name),i=ma(n.document.updateTime),a=new Bi({mapValue:{fields:n.document.fields}}),u=(o=Ki.newFoundDocument(r,i,a),n.targetIds||[]),c=n.removedTargetIds||[];s=new ea(u,c,o.key,o)}else if("documentDelete"in e)e.documentDelete,(n=e.documentDelete).document,r=Ea(t,n.document),i=n.readTime?ma(n.readTime):Zr.min(),a=Ki.newNoDocument(r,i),o=n.removedTargetIds||[],s=new ea([],o,a.key,a);else if("documentRemove"in e)e.documentRemove,(n=e.documentRemove).document,r=Ea(t,n.document),i=n.removedTargetIds||[],s=new ea([],i,r,null);else{if(!("filter"in e))return Qr();e.filter;e=e.filter;e.targetId,n=e.count||0,r=new As(n),i=e.targetId,s=new na(i,r)}return s}(this.R,t),t=function(t){if(!("targetChange"in t))return Zr.min();t=t.targetChange;return(!t.targetIds||!t.targetIds.length)&&t.readTime?ma(t.readTime):Zr.min()}(t);return this.listener._r(e,t)},Ll.prototype.mr=function(t){var e,n,r,i={};i.database=_a(this.R),i.addTarget=(e=this.R,(r=Yi(r=(n=t).target)?{documents:ka(e,r)}:{query:Ra(e,r)}).targetId=n.targetId,0this.query.limit;){var n=ko(this.query)?h.last():h.first(),h=h.delete(n.key),c=c.delete(n.key);a.track({type:1,doc:n})}return{fo:h,mo:a,Nn:l,mutatedKeys:c}},Of.prototype.yo=function(t,e){return t.hasLocalMutations&&e.hasCommittedMutations&&!e.hasLocalMutations},Of.prototype.applyChanges=function(t,e,n){var o=this,r=this.fo;this.fo=t.fo,this.mutatedKeys=t.mutatedKeys;var i=t.mo.jr();i.sort(function(t,e){return r=t.type,i=e.type,n(r)-n(i)||o.lo(t.doc,e.doc);function n(t){switch(t){case 0:return 1;case 2:case 3:return 2;case 1:return 0;default:return Qr()}}var r,i}),this.po(n);var s=e?this.Eo():[],n=0===this.ho.size&&this.current?1:0,e=n!==this.ao;return this.ao=n,0!==i.length||e?{snapshot:new cf(this.query,t.fo,r,i,t.mutatedKeys,0==n,e,!1),To:s}:{To:s}},Of.prototype.zr=function(t){return this.current&&"Offline"===t?(this.current=!1,this.applyChanges({fo:this.fo,mo:new uf,mutatedKeys:this.mutatedKeys,Nn:!1},!1)):{To:[]}},Of.prototype.Io=function(t){return!this.uo.has(t)&&!!this.fo.has(t)&&!this.fo.get(t).hasLocalMutations},Of.prototype.po=function(t){var e=this;t&&(t.addedDocuments.forEach(function(t){return e.uo=e.uo.add(t)}),t.modifiedDocuments.forEach(function(t){}),t.removedDocuments.forEach(function(t){return e.uo=e.uo.delete(t)}),this.current=t.current)},Of.prototype.Eo=function(){var e=this;if(!this.current)return[];var n=this.ho;this.ho=$s(),this.fo.forEach(function(t){e.Io(t.key)&&(e.ho=e.ho.add(t.key))});var r=[];return n.forEach(function(t){e.ho.has(t)||r.push(new Df(t))}),this.ho.forEach(function(t){n.has(t)||r.push(new Af(t))}),r},Of.prototype.Ao=function(t){this.uo=t.Bn,this.ho=$s();t=this._o(t.documents);return this.applyChanges(t,!0)},Of.prototype.Ro=function(){return cf.fromInitialDocuments(this.query,this.fo,this.mutatedKeys,0===this.ao)},Of),Cf=function(t,e,n){this.query=t,this.targetId=e,this.view=n},kf=function(t){this.key=t,this.bo=!1},Rf=(Object.defineProperty(xf.prototype,"isPrimaryClient",{get:function(){return!0===this.$o},enumerable:!1,configurable:!0}),xf);function xf(t,e,n,r,i,o){this.localStore=t,this.remoteStore=e,this.eventManager=n,this.sharedClientState=r,this.currentUser=i,this.maxConcurrentLimboResolutions=o,this.vo={},this.Po=new Pc(Uo,Vo),this.Vo=new Map,this.So=new Set,this.Do=new Ms(Ai.comparator),this.Co=new Map,this.No=new _h,this.xo={},this.Fo=new Map,this.ko=gc.Yt(),this.onlineState="Unknown",this.$o=void 0}function Of(t,e){this.query=t,this.uo=e,this.ao=null,this.current=!1,this.ho=$s(),this.mutatedKeys=$s(),this.lo=jo(t),this.fo=new af(this.lo)}function Lf(i,o,s,a){return y(this,void 0,void 0,function(){var e,n,r;return g(this,function(t){switch(t.label){case 0:return i.Oo=function(t,e,n){return function(r,i,o,s){return y(this,void 0,void 0,function(){var e,n;return g(this,function(t){switch(t.label){case 0:return(e=i.view._o(o)).Nn?[4,mh(r.localStore,i.query,!1).then(function(t){t=t.documents;return i.view._o(t,e)})]:[3,2];case 1:e=t.sent(),t.label=2;case 2:return n=s&&s.targetChanges.get(i.targetId),n=i.view.applyChanges(e,r.isPrimaryClient,n),[2,(Gf(r,i.targetId,n.To),n.snapshot)]}})})}(i,t,e,n)},[4,mh(i.localStore,o,!0)];case 1:return n=t.sent(),r=new Nf(o,n.Bn),e=r._o(n.documents),n=ta.createSynthesizedTargetChangeForCurrentChange(s,a&&"Offline"!==i.onlineState),n=r.applyChanges(e,i.isPrimaryClient,n),Gf(i,s,n.To),r=new Cf(o,s,r),[2,(i.Po.set(o,r),i.Vo.has(s)?i.Vo.get(s).push(o):i.Vo.set(s,[o]),n.snapshot)]}})})}function Pf(f,d,p){return y(this,void 0,void 0,function(){var s,l;return g(this,function(t){switch(t.label){case 0:l=Zf(f),t.label=1;case 1:return t.trys.push([1,5,,6]),[4,(i=l.localStore,a=d,c=i,h=Jr.now(),o=a.reduce(function(t,e){return t.add(e.key)},$s()),c.persistence.runTransaction("Locally write mutations","readwrite",function(s){return c.Mn.pn(s,o).next(function(t){u=t;for(var e=[],n=0,r=a;n, or >=) must be on the same field. But you have inequality filters on '"+n.toString()+"' and '"+e.field.toString()+"'");n=xo(t);null!==n&&og(0,e.field,n)}t=function(t,e){for(var n=0,r=t.filters;ns.length)throw new Fr(Mr.INVALID_ARGUMENT,"Too many arguments provided to "+r+"(). The number of arguments must be less than or equal to the number of orderBy() clauses");for(var a=[],u=0;u, or >=) on field '"+e.toString()+"' and so you must also use '"+e.toString()+"' as your first argument to orderBy(), but your first orderBy() is on field '"+n.toString()+"' instead.")}sg.prototype.convertValue=function(t,e){switch(void 0===e&&(e="none"),Ni(t)){case 0:return null;case 1:return t.booleanValue;case 2:return wi(t.integerValue||t.doubleValue);case 3:return this.convertTimestamp(t.timestampValue);case 4:return this.convertServerTimestamp(t,e);case 5:return t.stringValue;case 6:return this.convertBytes(bi(t.bytesValue));case 7:return this.convertReference(t.referenceValue);case 8:return this.convertGeoPoint(t.geoPointValue);case 9:return this.convertArray(t.arrayValue,e);case 10:return this.convertObject(t.mapValue,e);default:throw Qr()}},sg.prototype.convertObject=function(t,n){var r=this,i={};return ri(t.fields,function(t,e){i[t]=r.convertValue(e,n)}),i},sg.prototype.convertGeoPoint=function(t){return new xp(wi(t.latitude),wi(t.longitude))},sg.prototype.convertArray=function(t,e){var n=this;return(t.values||[]).map(function(t){return n.convertValue(t,e)})},sg.prototype.convertServerTimestamp=function(t,e){switch(e){case"previous":var n=function t(e){e=e.mapValue.fields.__previous_value__;return Ei(e)?t(e):e}(t);return null==n?null:this.convertValue(n,e);case"estimate":return this.convertTimestamp(Ti(t));default:return null}},sg.prototype.convertTimestamp=function(t){t=vi(t);return new Jr(t.seconds,t.nanos)},sg.prototype.convertDocumentKey=function(t,e){var n=ai.fromString(t);Hr(Ua(n));t=new Pd(n.get(1),n.get(3)),n=new Ai(n.popFirst(5));return t.isEqual(e)||jr("Document "+n+" contains a document reference within a different database ("+t.projectId+"/"+t.database+") which is not supported. It will be treated as a reference in the current database ("+e.projectId+"/"+e.database+") instead."),n},_=sg;function sg(){}function ag(t,e,n){return t?n&&(n.merge||n.mergeFields)?t.toFirestore(e,n):t.toFirestore(e):e}var ug,cg=(n(fg,ug=_),fg.prototype.convertBytes=function(t){return new kp(t)},fg.prototype.convertReference=function(t){t=this.convertDocumentKey(t,this.firestore._databaseId);return new op(this.firestore,null,t)},fg),hg=(lg.prototype.set=function(t,e,n){this._verifyNotCommitted();t=dg(t,this._firestore),e=ag(t.converter,e,n),n=zp(this._dataReader,"WriteBatch.set",t._key,e,null!==t.converter,n);return this._mutations.push(n.toMutation(t._key,ls.none())),this},lg.prototype.update=function(t,e,n){for(var r=[],i=3;i - + + + + + + + + + + + + + + + + + + + + @@ -32,8 +56,24 @@ I/O Pinball Machine - Flutter - - + + + + + @@ -107,4 +147,4 @@ - + \ No newline at end of file