diff --git a/assets/images/loading_game/io_pinball.png b/assets/images/loading_game/io_pinball.png new file mode 100644 index 00000000..c8d9fadc Binary files /dev/null and b/assets/images/loading_game/io_pinball.png differ diff --git a/lib/assets_manager/cubit/assets_manager_cubit.dart b/lib/assets_manager/cubit/assets_manager_cubit.dart index b97483d4..5d3dd7c9 100644 --- a/lib/assets_manager/cubit/assets_manager_cubit.dart +++ b/lib/assets_manager/cubit/assets_manager_cubit.dart @@ -1,27 +1,39 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_audio/pinball_audio.dart'; part 'assets_manager_state.dart'; -/// {@template assets_manager_cubit} -/// Cubit responsable for pre loading any game assets -/// {@endtemplate} class AssetsManagerCubit extends Cubit { - /// {@macro assets_manager_cubit} - AssetsManagerCubit(List loadables) - : super( - AssetsManagerState.initial( - loadables: loadables, - ), - ); + AssetsManagerCubit(this._game, this._player) + : super(const AssetsManagerState.initial()); + + final PinballGame _game; + final PinballPlayer _player; - /// Loads the assets Future load() async { + /// Assigning loadables is a very expensive operation. With this purposeful + /// delay here, which is a bit random in duration but enough to let the UI + /// do its job without adding too much delay for the user, we are letting + /// the UI paint first, and then we start loading the assets. + await Future.delayed(const Duration(milliseconds: 300)); + emit( + state.copyWith( + loadables: [ + _game.preFetchLeaderboard(), + ..._game.preLoadAssets(), + ..._player.load(), + ...BonusAnimation.loadAssets(), + ...SelectedCharacter.loadAssets(), + ], + ), + ); final all = state.loadables.map((loadable) async { await loadable; emit(state.copyWith(loaded: [...state.loaded, loadable])); }).toList(); - await Future.wait(all); } } diff --git a/lib/assets_manager/cubit/assets_manager_state.dart b/lib/assets_manager/cubit/assets_manager_state.dart index 8ef1e874..4847adc6 100644 --- a/lib/assets_manager/cubit/assets_manager_state.dart +++ b/lib/assets_manager/cubit/assets_manager_state.dart @@ -11,9 +11,8 @@ class AssetsManagerState extends Equatable { }); /// {@macro assets_manager_state} - const AssetsManagerState.initial({ - required List loadables, - }) : this(loadables: loadables, loaded: const []); + const AssetsManagerState.initial() + : this(loadables: const [], loaded: const []); /// List of futures to load final List loadables; @@ -22,7 +21,11 @@ class AssetsManagerState extends Equatable { final List loaded; /// Returns a value between 0 and 1 to indicate the loading progress - double get progress => loaded.length / loadables.length; + double get progress => + loadables.isEmpty ? 0 : loaded.length / loadables.length; + + /// Only returns false if all the assets have been loaded + bool get isLoading => progress != 1; /// Returns a copy of this instance with the given parameters /// updated diff --git a/lib/assets_manager/views/assets_loading_page.dart b/lib/assets_manager/views/assets_loading_page.dart index ddb76803..4e75a3a5 100644 --- a/lib/assets_manager/views/assets_loading_page.dart +++ b/lib/assets_manager/views/assets_loading_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/assets_manager/assets_manager.dart'; +import 'package:pinball/gen/gen.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -20,10 +21,9 @@ class AssetsLoadingPage extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - l10n.ioPinball, - style: headline1!.copyWith(fontSize: 80), - textAlign: TextAlign.center, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Assets.images.loadingGame.ioPinball.image(), ), const SizedBox(height: 40), AnimatedEllipsisText( diff --git a/lib/game/behaviors/behaviors.dart b/lib/game/behaviors/behaviors.dart index 301bc61e..5900f2b3 100644 --- a/lib/game/behaviors/behaviors.dart +++ b/lib/game/behaviors/behaviors.dart @@ -1,5 +1,6 @@ export 'ball_spawning_behavior.dart'; export 'ball_theming_behavior.dart'; +export 'bonus_ball_spawning_behavior.dart'; export 'bonus_noise_behavior.dart'; export 'bumper_noise_behavior.dart'; export 'camera_focusing_behavior.dart'; diff --git a/lib/game/behaviors/bonus_ball_spawning_behavior.dart b/lib/game/behaviors/bonus_ball_spawning_behavior.dart new file mode 100644 index 00000000..26fe423d --- /dev/null +++ b/lib/game/behaviors/bonus_ball_spawning_behavior.dart @@ -0,0 +1,30 @@ +import 'package:flame/components.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template bonus_ball_spawning_behavior} +/// After a duration, spawns a bonus ball from the [DinoWalls] and boosts it +/// into the middle of the board. +/// {@endtemplate} +class BonusBallSpawningBehavior extends TimerComponent with HasGameRef { + /// {@macro bonus_ball_spawning_behavior} + BonusBallSpawningBehavior() + : super( + period: 5, + removeOnFinish: true, + ); + + @override + void onTick() { + final characterTheme = readBloc() + .state + .characterTheme; + gameRef.descendants().whereType().single.add( + Ball(assetPath: characterTheme.ball.keyName) + ..add(BallImpulsingBehavior(impulse: Vector2(-40, 0))) + ..initialPosition = Vector2(29.2, -24.5) + ..zIndex = ZIndexes.ballOnBoard, + ); + } +} diff --git a/lib/game/behaviors/bonus_noise_behavior.dart b/lib/game/behaviors/bonus_noise_behavior.dart index 70c8ad3e..c071f064 100644 --- a/lib/game/behaviors/bonus_noise_behavior.dart +++ b/lib/game/behaviors/bonus_noise_behavior.dart @@ -25,10 +25,13 @@ class BonusNoiseBehavior extends Component { audioPlayer.play(PinballAudio.sparky); break; case GameBonus.dinoChomp: + audioPlayer.play(PinballAudio.dino); break; case GameBonus.androidSpaceship: + audioPlayer.play(PinballAudio.android); break; case GameBonus.dashNest: + audioPlayer.play(PinballAudio.dash); break; } }, diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index c45da958..8fcab789 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -5,7 +5,7 @@ enum GameBonus { /// Bonus achieved when the ball activates all Google letters. googleWord, - /// Bonus achieved when the user activates all dash nest bumpers. + /// Bonus achieved when the user activates all dash bumpers. dashNest, /// Bonus achieved when a ball enters Sparky's computer. diff --git a/lib/game/components/android_acres/android_acres.dart b/lib/game/components/android_acres/android_acres.dart index 7f9fff13..902eb11c 100644 --- a/lib/game/components/android_acres/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; @@ -15,42 +16,43 @@ class AndroidAcres extends Component { AndroidAcres() : super( children: [ - SpaceshipRamp( + FlameBlocProvider( + create: AndroidSpaceshipCubit.new, children: [ - RampShotBehavior( - points: Points.fiveThousand, - ), - RampBonusBehavior( - points: Points.oneMillion, + SpaceshipRamp( + children: [ + RampShotBehavior(points: Points.fiveThousand), + RampBonusBehavior(points: Points.oneMillion), + ], ), + SpaceshipRail(), + AndroidSpaceship(position: Vector2(-26.5, -28.5)), + AndroidAnimatronic( + children: [ + ScoringContactBehavior(points: Points.twoHundredThousand), + ], + )..initialPosition = Vector2(-26, -28.25), + AndroidBumper.a( + children: [ + ScoringContactBehavior(points: Points.twentyThousand), + BumperNoiseBehavior(), + ], + )..initialPosition = Vector2(-25.2, 1.5), + AndroidBumper.b( + children: [ + ScoringContactBehavior(points: Points.twentyThousand), + BumperNoiseBehavior(), + ], + )..initialPosition = Vector2(-32.9, -9.3), + AndroidBumper.cow( + children: [ + ScoringContactBehavior(points: Points.twentyThousand), + BumperNoiseBehavior(), + ], + )..initialPosition = Vector2(-20.7, -13), + AndroidSpaceshipBonusBehavior(), ], ), - SpaceshipRail(), - AndroidSpaceship(position: Vector2(-26.5, -28.5)), - AndroidAnimatronic( - children: [ - ScoringContactBehavior(points: Points.twoHundredThousand), - ], - )..initialPosition = Vector2(-26, -28.25), - AndroidBumper.a( - children: [ - ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), - ], - )..initialPosition = Vector2(-25.2, 1.5), - AndroidBumper.b( - children: [ - ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), - ], - )..initialPosition = Vector2(-32.9, -9.3), - AndroidBumper.cow( - children: [ - ScoringContactBehavior(points: Points.twentyThousand), - BumperNoiseBehavior(), - ], - )..initialPosition = Vector2(-20.7, -13), - AndroidSpaceshipBonusBehavior(), ], ); diff --git a/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart index cbb6e516..3b6f59ec 100644 --- a/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart +++ b/lib/game/components/android_acres/behaviors/android_spaceship_bonus_behavior.dart @@ -5,18 +5,21 @@ import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus. -class AndroidSpaceshipBonusBehavior extends Component - with ParentIsA, FlameBlocReader { +class AndroidSpaceshipBonusBehavior extends Component { @override - void onMount() { - super.onMount(); - final androidSpaceship = parent.firstChild()!; - androidSpaceship.bloc.stream.listen((state) { - final listenWhen = state == AndroidSpaceshipState.withBonus; - if (!listenWhen) return; - - bloc.add(const BonusActivated(GameBonus.androidSpaceship)); - androidSpaceship.bloc.onBonusAwarded(); - }); + Future onLoad() async { + await super.onLoad(); + await add( + FlameBlocListener( + listenWhen: (_, state) => state == AndroidSpaceshipState.withBonus, + onNewState: (state) { + readBloc().add( + const BonusActivated(GameBonus.androidSpaceship), + ); + readBloc() + .onBonusAwarded(); + }, + ), + ); } } diff --git a/lib/game/components/backbox/backbox.dart b/lib/game/components/backbox/backbox.dart index e8b19c87..e79029cc 100644 --- a/lib/game/components/backbox/backbox.dart +++ b/lib/game/components/backbox/backbox.dart @@ -5,27 +5,37 @@ import 'package:flutter/material.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart'; import 'package:pinball/game/components/backbox/displays/displays.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' hide Assets; +import 'package:platform_helper/platform_helper.dart'; /// {@template backbox} /// The [Backbox] of the pinball machine. /// {@endtemplate} -class Backbox extends PositionComponent with ZIndex { +class Backbox extends PositionComponent with ZIndex, HasGameRef { /// {@macro backbox} Backbox({ required LeaderboardRepository leaderboardRepository, - }) : _bloc = BackboxBloc(leaderboardRepository: leaderboardRepository); + required List? entries, + }) : _bloc = BackboxBloc( + leaderboardRepository: leaderboardRepository, + initialEntries: entries, + ), + _platformHelper = PlatformHelper(); /// {@macro backbox} @visibleForTesting Backbox.test({ required BackboxBloc bloc, - }) : _bloc = bloc; + required PlatformHelper platformHelper, + }) : _bloc = bloc, + _platformHelper = platformHelper; late final Component _display; final BackboxBloc _bloc; + final PlatformHelper _platformHelper; late StreamSubscription _subscription; @override @@ -34,8 +44,6 @@ class Backbox extends PositionComponent with ZIndex { anchor = Anchor.bottomCenter; zIndex = ZIndexes.backbox; - _bloc.add(LeaderboardRequested()); - await add(_BackboxSpriteComponent()); await add(_display = Component()); _build(_bloc.state); @@ -58,6 +66,9 @@ class Backbox extends PositionComponent with ZIndex { } else if (state is LeaderboardSuccessState) { _display.add(LeaderboardDisplay(entries: state.entries)); } else if (state is InitialsFormState) { + if (_platformHelper.isMobile) { + gameRef.overlays.add(PinballGame.mobileControlsOverlay); + } _display.add( InitialsInputDisplay( score: state.score, diff --git a/lib/game/components/backbox/bloc/backbox_bloc.dart b/lib/game/components/backbox/bloc/backbox_bloc.dart index df29c5f1..9d1178ba 100644 --- a/lib/game/components/backbox/bloc/backbox_bloc.dart +++ b/lib/game/components/backbox/bloc/backbox_bloc.dart @@ -14,8 +14,13 @@ class BackboxBloc extends Bloc { /// {@macro backbox_bloc} BackboxBloc({ required LeaderboardRepository leaderboardRepository, + required List? initialEntries, }) : _leaderboardRepository = leaderboardRepository, - super(LoadingState()) { + super( + initialEntries != null + ? LeaderboardSuccessState(entries: initialEntries) + : LeaderboardFailureState(), + ) { on(_onPlayerInitialsRequested); on(_onPlayerInitialsSubmitted); on(_onScoreShareRequested); diff --git a/lib/game/components/bottom_group.dart b/lib/game/components/bottom_group.dart index 4c6b2822..bc644f96 100644 --- a/lib/game/components/bottom_group.dart +++ b/lib/game/components/bottom_group.dart @@ -1,6 +1,5 @@ import 'package:flame/components.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; @@ -40,7 +39,7 @@ class _BottomGroupSide extends Component { final direction = _side.direction; final centerXAdjustment = _side.isLeft ? -0.45 : -6.8; - final flipper = ControlledFlipper( + final flipper = Flipper( side: _side, )..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6); final baseboard = Baseboard(side: _side) diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 08dc5cb0..324f379a 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,7 +1,6 @@ export 'android_acres/android_acres.dart'; export 'backbox/backbox.dart'; export 'bottom_group.dart'; -export 'controlled_flipper.dart'; export 'controlled_plunger.dart'; export 'dino_desert/dino_desert.dart'; export 'drain/drain.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 index f37299c7..3c4ef02a 100644 --- a/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart +++ b/lib/game/components/flutter_forest/behaviors/flutter_forest_bonus_behavior.dart @@ -1,15 +1,15 @@ import 'package:flame/components.dart'; import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// Bonus obtained at the [FlutterForest]. /// -/// When all [DashNestBumper]s are hit at least once three times, the [Signpost] +/// When all [DashBumper]s are hit at least once three times, the [Signpost] /// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest] -/// is awarded, and the [DashNestBumper.main] releases a new [Ball]. +/// is awarded, and the [DashBumper.main] releases a new [Ball]. class FlutterForestBonusBehavior extends Component with ParentIsA, @@ -19,15 +19,14 @@ class FlutterForestBonusBehavior extends Component void onMount() { super.onMount(); - final bumpers = parent.children.whereType(); + final bumpers = parent.children.whereType(); final signpost = parent.firstChild()!; final animatronic = parent.firstChild()!; - final canvas = gameRef.descendants().whereType().single; for (final bumper in bumpers) { bumper.bloc.stream.listen((state) { final activatedAllBumpers = bumpers.every( - (bumper) => bumper.bloc.state == DashNestBumperState.active, + (bumper) => bumper.bloc.state == DashBumperState.active, ); if (activatedAllBumpers) { @@ -38,15 +37,7 @@ class FlutterForestBonusBehavior extends Component if (signpost.bloc.isFullyProgressed()) { bloc.add(const BonusActivated(GameBonus.dashNest)); - final characterTheme = - readBloc() - .state - .characterTheme; - canvas.add( - Ball(assetPath: characterTheme.ball.keyName) - ..initialPosition = Vector2(29.2, -24.5) - ..zIndex = ZIndexes.ballOnBoard, - ); + add(BonusBallSpawningBehavior()); animatronic.playing = true; signpost.bloc.onProgressed(); } diff --git a/lib/game/components/flutter_forest/flutter_forest.dart b/lib/game/components/flutter_forest/flutter_forest.dart index 1cc055ae..39783bb1 100644 --- a/lib/game/components/flutter_forest/flutter_forest.dart +++ b/lib/game/components/flutter_forest/flutter_forest.dart @@ -9,7 +9,7 @@ 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. +/// off [DashBumper]s. /// {@endtemplate} class FlutterForest extends Component with ZIndex { /// {@macro flutter_forest} @@ -22,19 +22,19 @@ class FlutterForest extends Component with ZIndex { BumperNoiseBehavior(), ], )..initialPosition = Vector2(7.95, -58.35), - DashNestBumper.main( + DashBumper.main( children: [ ScoringContactBehavior(points: Points.twoHundredThousand), BumperNoiseBehavior(), ], )..initialPosition = Vector2(18.55, -59.35), - DashNestBumper.a( + DashBumper.a( children: [ ScoringContactBehavior(points: Points.twentyThousand), BumperNoiseBehavior(), ], )..initialPosition = Vector2(8.95, -51.95), - DashNestBumper.b( + DashBumper.b( children: [ ScoringContactBehavior(points: Points.twentyThousand), BumperNoiseBehavior(), diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index 81da96d5..cf2f75b3 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -3,6 +3,7 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// Listens to the [GameBloc] and updates the game accordingly. @@ -20,6 +21,11 @@ class GameBlocStatusListener extends Component break; case GameStatus.playing: readProvider().play(PinballAudio.backgroundMusic); + gameRef + .descendants() + .whereType() + .forEach(_addFlipperKeyControls); + gameRef.overlays.remove(PinballGame.playButtonOverlay); break; case GameStatus.gameOver: @@ -30,7 +36,23 @@ class GameBlocStatusListener extends Component .state .characterTheme, ); + + gameRef + .descendants() + .whereType() + .forEach(_removeFlipperKeyControls); break; } } + + void _addFlipperKeyControls(Flipper flipper) { + flipper + ..add(FlipperKeyControllingBehavior()) + ..moveDown(); + } + + void _removeFlipperKeyControls(Flipper flipper) => flipper + .descendants() + .whereType() + .forEach(flipper.remove); } diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 7271c8af..e0b66b15 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -1,3 +1,4 @@ +import 'package:flame/extensions.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart' as components; import 'package:pinball_theme/pinball_theme.dart' hide Assets; @@ -5,7 +6,7 @@ import 'package:pinball_theme/pinball_theme.dart' hide Assets; /// Add methods to help loading and caching game assets. extension PinballGameAssetsX on PinballGame { /// Returns a list of assets to be loaded - List preLoadAssets() { + List> preLoadAssets() { const dashTheme = DashTheme(); const sparkyTheme = SparkyTheme(); const androidTheme = AndroidTheme(); diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 712b2f96..689b96f7 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -37,8 +37,8 @@ class PinballGame extends PinballForge2DGame /// Identifier of the play button overlay static const playButtonOverlay = 'play_button'; - /// Identifier of the replay button overlay - static const replayButtonOverlay = 'replay_button'; + /// Identifier of the mobile controls overlay + static const mobileControlsOverlay = 'mobile_controls'; @override Color backgroundColor() => Colors.transparent; @@ -55,6 +55,18 @@ class PinballGame extends PinballForge2DGame final GameBloc _gameBloc; + List? _entries; + + Future preFetchLeaderboard() async { + try { + _entries = await leaderboardRepository.fetchTop10Leaderboard(); + } catch (_) { + // An initial null leaderboard means that we couldn't fetch + // the entries for the [Backbox] and it will show the relevant display. + _entries = null; + } + } + @override Future onLoad() async { await add( @@ -93,6 +105,7 @@ class PinballGame extends PinballForge2DGame Boundaries(), Backbox( leaderboardRepository: leaderboardRepository, + entries: _entries, ), GoogleWord(position: Vector2(-4.45, 1.8)), Multipliers(), diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index cec6759d..dc36c74c 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -21,12 +21,6 @@ class PinballGamePage extends StatelessWidget { final bool isDebugMode; - static Route route({bool isDebugMode = kDebugMode}) { - return MaterialPageRoute( - builder: (_) => PinballGamePage(isDebugMode: isDebugMode), - ); - } - @override Widget build(BuildContext context) { final characterThemeBloc = context.read(); @@ -48,52 +42,39 @@ class PinballGamePage extends StatelessWidget { l10n: context.l10n, gameBloc: gameBloc, ); - - final loadables = [ - ...game.preLoadAssets(), - ...player.load(), - ...BonusAnimation.loadAssets(), - ...SelectedCharacter.loadAssets(), - ]; - - return BlocProvider( - create: (_) => AssetsManagerCubit(loadables)..load(), - child: PinballGameView(game: game), + return Container( + decoration: const CrtBackground(), + child: Scaffold( + backgroundColor: PinballColors.transparent, + body: BlocProvider( + create: (_) => AssetsManagerCubit(game, player)..load(), + child: PinballGameView(game), + ), + ), ); } } class PinballGameView extends StatelessWidget { - const PinballGameView({ - Key? key, - required this.game, - }) : super(key: key); + const PinballGameView(this.game, {Key? key}) : super(key: key); final PinballGame game; @override Widget build(BuildContext context) { - final isLoading = context.select( - (AssetsManagerCubit bloc) => bloc.state.progress != 1, - ); - return Container( - decoration: const CrtBackground(), - child: Scaffold( - backgroundColor: PinballColors.transparent, - body: isLoading + return BlocBuilder( + builder: (context, state) { + return state.isLoading ? const AssetsLoadingPage() - : PinballGameLoadedView(game: game), - ), + : PinballGameLoadedView(game); + }, ); } } @visibleForTesting class PinballGameLoadedView extends StatelessWidget { - const PinballGameLoadedView({ - Key? key, - required this.game, - }) : super(key: key); + const PinballGameLoadedView(this.game, {Key? key}) : super(key: key); final PinballGame game; @@ -122,12 +103,12 @@ class PinballGameLoadedView extends StatelessWidget { child: PlayButtonOverlay(), ); }, - PinballGame.replayButtonOverlay: (context, game) { - return const Positioned( - bottom: 20, - right: 0, + PinballGame.mobileControlsOverlay: (context, game) { + return Positioned( + bottom: 0, left: 0, - child: ReplayButtonOverlay(), + right: 0, + child: MobileControls(game: game), ); }, }, diff --git a/lib/game/view/widgets/mobile_controls.dart b/lib/game/view/widgets/mobile_controls.dart new file mode 100644 index 00000000..c5761eb6 --- /dev/null +++ b/lib/game/view/widgets/mobile_controls.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template mobile_controls} +/// Widget with the controls used to enable the user initials input on mobile. +/// {@endtemplate} +class MobileControls extends StatelessWidget { + /// {@macro mobile_controls} + const MobileControls({ + Key? key, + required this.game, + }) : super(key: key); + + /// Game instance + final PinballGame game; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + MobileDpad( + onTapUp: () => game.triggerVirtualKeyUp(LogicalKeyboardKey.arrowUp), + onTapDown: () => game.triggerVirtualKeyUp( + LogicalKeyboardKey.arrowDown, + ), + onTapLeft: () => game.triggerVirtualKeyUp( + LogicalKeyboardKey.arrowLeft, + ), + onTapRight: () => game.triggerVirtualKeyUp( + LogicalKeyboardKey.arrowRight, + ), + ), + PinballButton( + text: l10n.enter, + onTap: () => game.triggerVirtualKeyUp(LogicalKeyboardKey.enter), + ), + ], + ); + } +} diff --git a/lib/game/view/widgets/mobile_dpad.dart b/lib/game/view/widgets/mobile_dpad.dart new file mode 100644 index 00000000..abad496b --- /dev/null +++ b/lib/game/view/widgets/mobile_dpad.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template mobile_dpad} +/// Widget rendering 4 directional input arrows. +/// {@endtemplate} +class MobileDpad extends StatelessWidget { + /// {@template mobile_dpad} + const MobileDpad({ + Key? key, + required this.onTapUp, + required this.onTapDown, + required this.onTapLeft, + required this.onTapRight, + }) : super(key: key); + + static const _size = 180.0; + + /// Called when dpad up is pressed + final VoidCallback onTapUp; + + /// Called when dpad down is pressed + final VoidCallback onTapDown; + + /// Called when dpad left is pressed + final VoidCallback onTapLeft; + + /// Called when dpad right is pressed + final VoidCallback onTapRight; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: _size, + height: _size, + child: Column( + children: [ + Row( + children: [ + const Spacer(), + PinballDpadButton( + direction: PinballDpadDirection.up, + onTap: onTapUp, + ), + const Spacer(), + ], + ), + Row( + children: [ + PinballDpadButton( + direction: PinballDpadDirection.left, + onTap: onTapLeft, + ), + const Spacer(), + PinballDpadButton( + direction: PinballDpadDirection.right, + onTap: onTapRight, + ), + ], + ), + Row( + children: [ + const Spacer(), + PinballDpadButton( + direction: PinballDpadDirection.down, + onTap: onTapDown, + ), + const Spacer(), + ], + ), + ], + ), + ); + } +} diff --git a/lib/game/view/widgets/widgets.dart b/lib/game/view/widgets/widgets.dart index 33bb003a..0093cad2 100644 --- a/lib/game/view/widgets/widgets.dart +++ b/lib/game/view/widgets/widgets.dart @@ -1,5 +1,7 @@ export 'bonus_animation.dart'; export 'game_hud.dart'; +export 'mobile_controls.dart'; +export 'mobile_dpad.dart'; export 'play_button_overlay.dart'; export 'replay_button_overlay.dart'; export 'round_count_display.dart'; diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 33d2bbd1..f0b6fdeb 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -15,6 +15,8 @@ class $AssetsImagesGen { $AssetsImagesComponentsGen get components => const $AssetsImagesComponentsGen(); $AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen(); + $AssetsImagesLoadingGameGen get loadingGame => + const $AssetsImagesLoadingGameGen(); $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); } @@ -62,6 +64,14 @@ class $AssetsImagesLinkBoxGen { const AssetGenImage('assets/images/link_box/info_icon.png'); } +class $AssetsImagesLoadingGameGen { + const $AssetsImagesLoadingGameGen(); + + /// File path: assets/images/loading_game/io_pinball.png + AssetGenImage get ioPinball => + const AssetGenImage('assets/images/loading_game/io_pinball.png'); +} + class $AssetsImagesScoreGen { const $AssetsImagesScoreGen(); diff --git a/lib/how_to_play/widgets/how_to_play_dialog.dart b/lib/how_to_play/widgets/how_to_play_dialog.dart index 0113319e..55aff1db 100644 --- a/lib/how_to_play/widgets/how_to_play_dialog.dart +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -240,7 +240,7 @@ class _DesktopFlipperControls extends StatelessWidget { children: [ Text( l10n.flipperControls, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.headline4, ), const SizedBox(height: 10), Column( diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f3ec6e84..98ccfc46 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -183,5 +183,9 @@ "openSourceCode": "open source code.", "@openSourceCode": { "description": "Text shown on description of info screen" + }, + "enter": "Enter", + "@enter": { + "description": "Text shown on the mobile controls enter button" } } diff --git a/lib/more_information/more_information_dialog.dart b/lib/more_information/more_information_dialog.dart index 179c06f5..b9a9ddbb 100644 --- a/lib/more_information/more_information_dialog.dart +++ b/lib/more_information/more_information_dialog.dart @@ -55,8 +55,6 @@ class _LinkBoxHeader extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final indent = MediaQuery.of(context).size.width / 5; - return Column( children: [ Text( @@ -68,11 +66,9 @@ class _LinkBoxHeader extends StatelessWidget { overflow: TextOverflow.ellipsis, ), const SizedBox(height: 12), - Divider( - color: PinballColors.white, - endIndent: indent, - indent: indent, - thickness: 2, + const SizedBox( + width: 200, + child: Divider(color: PinballColors.white, thickness: 2), ), ], ); diff --git a/packages/pinball_audio/assets/sfx/android.mp3 b/packages/pinball_audio/assets/sfx/android.mp3 new file mode 100644 index 00000000..33bebe07 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/android.mp3 differ diff --git a/packages/pinball_audio/assets/sfx/dash.mp3 b/packages/pinball_audio/assets/sfx/dash.mp3 new file mode 100644 index 00000000..1180fc4e Binary files /dev/null and b/packages/pinball_audio/assets/sfx/dash.mp3 differ diff --git a/packages/pinball_audio/assets/sfx/dino.mp3 b/packages/pinball_audio/assets/sfx/dino.mp3 new file mode 100644 index 00000000..e8e4a1d5 Binary files /dev/null and b/packages/pinball_audio/assets/sfx/dino.mp3 differ diff --git a/packages/pinball_audio/lib/gen/assets.gen.dart b/packages/pinball_audio/lib/gen/assets.gen.dart index 08e83d87..bdd8e09e 100644 --- a/packages/pinball_audio/lib/gen/assets.gen.dart +++ b/packages/pinball_audio/lib/gen/assets.gen.dart @@ -14,8 +14,11 @@ class $AssetsMusicGen { class $AssetsSfxGen { const $AssetsSfxGen(); + String get android => 'assets/sfx/android.mp3'; String get bumperA => 'assets/sfx/bumper_a.mp3'; String get bumperB => 'assets/sfx/bumper_b.mp3'; + String get dash => 'assets/sfx/dash.mp3'; + String get dino => 'assets/sfx/dino.mp3'; String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3'; String get google => 'assets/sfx/google.mp3'; String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; diff --git a/packages/pinball_audio/lib/src/pinball_audio.dart b/packages/pinball_audio/lib/src/pinball_audio.dart index 95c993c5..767fc3a6 100644 --- a/packages/pinball_audio/lib/src/pinball_audio.dart +++ b/packages/pinball_audio/lib/src/pinball_audio.dart @@ -28,6 +28,15 @@ enum PinballAudio { /// Sparky sparky, + + /// Android + android, + + /// Dino + dino, + + /// Dash + dash, } /// Defines the contract of the creation of an [AudioPool]. @@ -169,6 +178,21 @@ class PinballPlayer { playSingleAudio: _playSingleAudio, path: Assets.sfx.sparky, ), + PinballAudio.dino: _SimplePlayAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.dino, + ), + PinballAudio.dash: _SimplePlayAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.dash, + ), + PinballAudio.android: _SimplePlayAudio( + preCacheSingleAudio: _preCacheSingleAudio, + playSingleAudio: _playSingleAudio, + path: Assets.sfx.android, + ), PinballAudio.launcher: _SimplePlayAudio( preCacheSingleAudio: _preCacheSingleAudio, playSingleAudio: _playSingleAudio, diff --git a/packages/pinball_audio/test/src/pinball_audio_test.dart b/packages/pinball_audio/test/src/pinball_audio_test.dart index 39060eb2..e95eb8f7 100644 --- a/packages/pinball_audio/test/src/pinball_audio_test.dart +++ b/packages/pinball_audio/test/src/pinball_audio_test.dart @@ -145,6 +145,18 @@ void main() { () => preCacheSingleAudio .onCall('packages/pinball_audio/assets/sfx/sparky.mp3'), ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/dino.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/android.mp3'), + ).called(1); + verify( + () => preCacheSingleAudio + .onCall('packages/pinball_audio/assets/sfx/dash.mp3'), + ).called(1); verify( () => preCacheSingleAudio.onCall( 'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3', @@ -239,6 +251,42 @@ void main() { }); }); + group('dino', () { + test('plays the correct file', () async { + await Future.wait(player.load()); + player.play(PinballAudio.dino); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.dino}'), + ).called(1); + }); + }); + + group('android', () { + test('plays the correct file', () async { + await Future.wait(player.load()); + player.play(PinballAudio.android); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.android}'), + ).called(1); + }); + }); + + group('dash', () { + test('plays the correct file', () async { + await Future.wait(player.load()); + player.play(PinballAudio.dash); + + verify( + () => playSingleAudio + .onCall('packages/pinball_audio/${Assets.sfx.dash}'), + ).called(1); + }); + }); + group('launcher', () { test('plays the correct file', () async { await Future.wait(player.load()); diff --git a/packages/pinball_components/assets/images/boundary/bottom.png b/packages/pinball_components/assets/images/boundary/bottom.png index 806f7051..523e6156 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/dino/top-wall.png b/packages/pinball_components/assets/images/dino/top-wall.png index 7ee69411..76dc4f12 100644 Binary files a/packages/pinball_components/assets/images/dino/top-wall.png and b/packages/pinball_components/assets/images/dino/top-wall.png differ diff --git a/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart index b3c721a1..0fd4628d 100644 --- a/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart +++ b/packages/pinball_components/lib/src/components/android_spaceship/android_spaceship.dart @@ -11,10 +11,8 @@ import 'package:pinball_flame/pinball_flame.dart'; export 'cubit/android_spaceship_cubit.dart'; class AndroidSpaceship extends Component { - AndroidSpaceship({ - required Vector2 position, - }) : bloc = AndroidSpaceshipCubit(), - super( + AndroidSpaceship({required Vector2 position}) + : super( children: [ _SpaceshipSaucer()..initialPosition = position, _SpaceshipSaucerSpriteAnimationComponent()..position = position, @@ -38,17 +36,8 @@ class AndroidSpaceship extends Component { /// This can be used for testing [AndroidSpaceship]'s behaviors in isolation. @visibleForTesting AndroidSpaceship.test({ - required this.bloc, Iterable? children, }) : super(children: children); - - final AndroidSpaceshipCubit bloc; - - @override - void onRemove() { - bloc.close(); - super.onRemove(); - } } class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { diff --git a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart index de5ed1ff..b577b7b3 100644 --- a/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart +++ b/packages/pinball_components/lib/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior.dart.dart @@ -1,14 +1,18 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; class AndroidSpaceshipEntranceBallContactBehavior - extends ContactBehavior { + extends ContactBehavior + with FlameBlocReader { @override void beginContact(Object other, Contact contact) { super.beginContact(other, contact); if (other is! Ball) return; - parent.parent.bloc.onBallEntered(); + bloc.onBallEntered(); } } diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/ball_impulsing_behavior.dart b/packages/pinball_components/lib/src/components/ball/behaviors/ball_impulsing_behavior.dart new file mode 100644 index 00000000..d875ef7c --- /dev/null +++ b/packages/pinball_components/lib/src/components/ball/behaviors/ball_impulsing_behavior.dart @@ -0,0 +1,22 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ball_impulsing_behavior} +/// Impulses the [Ball] in a given direction. +/// {@endtemplate} +class BallImpulsingBehavior extends Component with ParentIsA { + /// {@macro ball_impulsing_behavior} + BallImpulsingBehavior({ + required Vector2 impulse, + }) : _impulse = impulse; + + final Vector2 _impulse; + + @override + Future onLoad() async { + await super.onLoad(); + parent.body.linearVelocity = _impulse; + shouldRemove = true; + } +} diff --git a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart index 1068a20e..d2be36a9 100644 --- a/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart +++ b/packages/pinball_components/lib/src/components/ball/behaviors/behaviors.dart @@ -1,3 +1,4 @@ export 'ball_gravitating_behavior.dart'; +export 'ball_impulsing_behavior.dart'; export 'ball_scaling_behavior.dart'; export 'ball_turbo_charging_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 7fda6272..58319bac 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -10,12 +10,12 @@ export 'boundaries.dart'; export 'camera_zoom.dart'; export 'chrome_dino/chrome_dino.dart'; export 'dash_animatronic.dart'; -export 'dash_nest_bumper/dash_nest_bumper.dart'; +export 'dash_bumper/dash_bumper.dart'; export 'dino_walls.dart'; export 'error_component.dart'; export 'fire_effect.dart'; export 'flapper/flapper.dart'; -export 'flipper.dart'; +export 'flipper/flipper.dart'; export 'google_letter/google_letter.dart'; export 'initial_position.dart'; export 'joint_anchor.dart'; @@ -26,7 +26,7 @@ export 'multiball/multiball.dart'; export 'multiplier/multiplier.dart'; export 'plunger.dart'; export 'rocket.dart'; -export 'score_component.dart'; +export 'score_component/score_component.dart'; export 'shapes/shapes.dart'; export 'signpost/signpost.dart'; export 'skill_shot/skill_shot.dart'; diff --git a/packages/pinball_components/lib/src/components/dash_animatronic.dart b/packages/pinball_components/lib/src/components/dash_animatronic.dart index faa604e9..bb7d983b 100644 --- a/packages/pinball_components/lib/src/components/dash_animatronic.dart +++ b/packages/pinball_components/lib/src/components/dash_animatronic.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template dash_animatronic} -/// Animated Dash that sits on top of the [DashNestBumper.main]. +/// Animated Dash that sits on top of the [DashBumper.main]. /// {@endtemplate} class DashAnimatronic extends SpriteAnimationComponent with HasGameRef { /// {@macro dash_animatronic} diff --git a/packages/pinball_components/lib/src/components/dash_bumper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/dash_bumper/behaviors/behaviors.dart new file mode 100644 index 00000000..0167887f --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_bumper/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'dash_bumper_ball_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_bumper/behaviors/dash_bumper_ball_contact_behavior.dart similarity index 79% rename from packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_contact_behavior.dart rename to packages/pinball_components/lib/src/components/dash_bumper/behaviors/dash_bumper_ball_contact_behavior.dart index 934adef6..d147515c 100644 --- 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_bumper/behaviors/dash_bumper_ball_contact_behavior.dart @@ -2,8 +2,7 @@ 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 { +class DashBumperBallContactBehavior extends ContactBehavior { @override void beginContact(Object other, Contact contact) { super.beginContact(other, contact); diff --git a/packages/pinball_components/lib/src/components/dash_bumper/cubit/dash_bumper_cubit.dart b/packages/pinball_components/lib/src/components/dash_bumper/cubit/dash_bumper_cubit.dart new file mode 100644 index 00000000..84e626c4 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_bumper/cubit/dash_bumper_cubit.dart @@ -0,0 +1,17 @@ +import 'package:bloc/bloc.dart'; + +part 'dash_bumper_state.dart'; + +class DashBumperCubit extends Cubit { + DashBumperCubit() : super(DashBumperState.inactive); + + /// Event added when the bumper contacts with a ball. + void onBallContacted() { + emit(DashBumperState.active); + } + + /// Event added when the bumper should return to its initial configuration. + void onReset() { + emit(DashBumperState.inactive); + } +} diff --git a/packages/pinball_components/lib/src/components/dash_bumper/cubit/dash_bumper_state.dart b/packages/pinball_components/lib/src/components/dash_bumper/cubit/dash_bumper_state.dart new file mode 100644 index 00000000..f15d2e57 --- /dev/null +++ b/packages/pinball_components/lib/src/components/dash_bumper/cubit/dash_bumper_state.dart @@ -0,0 +1,10 @@ +part of 'dash_bumper_cubit.dart'; + +/// Indicates the [DashBumperCubit]'s current state. +enum DashBumperState { + /// A lit up bumper. + active, + + /// A dimmed bumper. + inactive, +} diff --git a/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart b/packages/pinball_components/lib/src/components/dash_bumper/dash_bumper.dart similarity index 70% rename from packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart rename to packages/pinball_components/lib/src/components/dash_bumper/dash_bumper.dart index d657c485..1b960610 100644 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper/dash_nest_bumper.dart +++ b/packages/pinball_components/lib/src/components/dash_bumper/dash_bumper.dart @@ -5,17 +5,17 @@ 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/bumping_behavior.dart'; -import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart'; +import 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart'; import 'package:pinball_flame/pinball_flame.dart'; -export 'cubit/dash_nest_bumper_cubit.dart'; +export 'cubit/dash_bumper_cubit.dart'; -/// {@template dash_nest_bumper} -/// Bumper with a nest appearance. +/// {@template dash_bumper} +/// Bumper for the flutter forest. /// {@endtemplate} -class DashNestBumper extends BodyComponent with InitialPosition { - /// {@macro dash_nest_bumper} - DashNestBumper._({ +class DashBumper extends BodyComponent with InitialPosition { + /// {@macro dash_bumper} + DashBumper._({ required double majorRadius, required double minorRadius, required String activeAssetPath, @@ -28,19 +28,22 @@ class DashNestBumper extends BodyComponent with InitialPosition { super( renderBody: false, children: [ - _DashNestBumperSpriteGroupComponent( + _DashBumperSpriteGroupComponent( activeAssetPath: activeAssetPath, inactiveAssetPath: inactiveAssetPath, position: spritePosition, current: bloc.state, ), - DashNestBumperBallContactBehavior(), + DashBumperBallContactBehavior(), ...?children, ], ); - /// {@macro dash_nest_bumper} - DashNestBumper.main({ + /// {@macro dash_bumper} + /// + /// [DashBumper.main], usually positioned with a [DashAnimatronic] on top of + /// it. + DashBumper.main({ Iterable? children, }) : this._( majorRadius: 5.1, @@ -48,15 +51,18 @@ class DashNestBumper extends BodyComponent with InitialPosition { activeAssetPath: Assets.images.dash.bumper.main.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName, spritePosition: Vector2(0, -0.3), - bloc: DashNestBumperCubit(), + bloc: DashBumperCubit(), children: [ ...?children, BumpingBehavior(strength: 20), ], ); - /// {@macro dash_nest_bumper} - DashNestBumper.a({ + /// {@macro dash_bumper} + /// + /// [DashBumper.a] is positioned at the right side of the [DashBumper.main] in + /// the flutter forest. + DashBumper.a({ Iterable? children, }) : this._( majorRadius: 3, @@ -64,15 +70,18 @@ class DashNestBumper extends BodyComponent with InitialPosition { activeAssetPath: Assets.images.dash.bumper.a.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, spritePosition: Vector2(0.3, -1.3), - bloc: DashNestBumperCubit(), + bloc: DashBumperCubit(), children: [ ...?children, BumpingBehavior(strength: 20), ], ); - /// {@macro dash_nest_bumper} - DashNestBumper.b({ + /// {@macro dash_bumper} + /// + /// [DashBumper.b] is positioned at the left side of the [DashBumper.main] in + /// the flutter forest. + DashBumper.b({ Iterable? children, }) : this._( majorRadius: 3.1, @@ -80,25 +89,26 @@ class DashNestBumper extends BodyComponent with InitialPosition { activeAssetPath: Assets.images.dash.bumper.b.active.keyName, inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, spritePosition: Vector2(0.4, -1.2), - bloc: DashNestBumperCubit(), + bloc: DashBumperCubit(), children: [ ...?children, BumpingBehavior(strength: 20), ], ); - /// Creates an [DashNestBumper] without any children. + /// Creates a [DashBumper] without any children. /// - /// This can be used for testing [DashNestBumper]'s behaviors in isolation. + /// This can be used for testing [DashBumper]'s behaviors in isolation. @visibleForTesting - DashNestBumper.test({required this.bloc}) + DashBumper.test({required this.bloc}) : _majorRadius = 3, _minorRadius = 2.5; final double _majorRadius; final double _minorRadius; - final DashNestBumperCubit bloc; + // ignore: public_member_api_docs + final DashBumperCubit bloc; @override void onRemove() { @@ -121,14 +131,14 @@ class DashNestBumper extends BodyComponent with InitialPosition { } } -class _DashNestBumperSpriteGroupComponent - extends SpriteGroupComponent - with HasGameRef, ParentIsA { - _DashNestBumperSpriteGroupComponent({ +class _DashBumperSpriteGroupComponent + extends SpriteGroupComponent + with HasGameRef, ParentIsA { + _DashBumperSpriteGroupComponent({ required String activeAssetPath, required String inactiveAssetPath, required Vector2 position, - required DashNestBumperState current, + required DashBumperState current, }) : _activeAssetPath = activeAssetPath, _inactiveAssetPath = inactiveAssetPath, super( @@ -146,9 +156,9 @@ class _DashNestBumperSpriteGroupComponent parent.bloc.stream.listen((state) => current = state); final sprites = { - DashNestBumperState.active: + DashBumperState.active: Sprite(gameRef.images.fromCache(_activeAssetPath)), - DashNestBumperState.inactive: + DashBumperState.inactive: Sprite(gameRef.images.fromCache(_inactiveAssetPath)), }; this.sprites = sprites; 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 deleted file mode 100644 index 839cbd67..00000000 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper/behaviors/behaviors.dart +++ /dev/null @@ -1 +0,0 @@ -export 'dash_nest_bumper_contact_behavior.dart'; 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 deleted file mode 100644 index 04d511a4..00000000 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit.dart +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index c169069f..00000000 --- a/packages/pinball_components/lib/src/components/dash_nest_bumper/cubit/dash_nest_bumper_state.dart +++ /dev/null @@ -1,10 +0,0 @@ -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/flipper/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart new file mode 100644 index 00000000..ef3630e7 --- /dev/null +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'flipper_jointing_behavior.dart'; +export 'flipper_key_controlling_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_jointing_behavior.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_jointing_behavior.dart new file mode 100644 index 00000000..eb29c181 --- /dev/null +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_jointing_behavior.dart @@ -0,0 +1,62 @@ +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'; + +/// Joints the [Flipper] to allow pivoting around one end. +class FlipperJointingBehavior extends Component + with ParentIsA, HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + + final anchor = _FlipperAnchor(flipper: parent); + await add(anchor); + + final jointDef = _FlipperAnchorRevoluteJointDef( + flipper: parent, + anchor: anchor, + ); + parent.world.createJoint(RevoluteJoint(jointDef)); + } +} + +/// {@template flipper_anchor} +/// [JointAnchor] positioned at the end of a [Flipper]. +/// +/// The end of a [Flipper] depends on its [Flipper.side]. +/// {@endtemplate} +class _FlipperAnchor extends JointAnchor { + /// {@macro flipper_anchor} + _FlipperAnchor({ + required Flipper flipper, + }) { + initialPosition = Vector2( + (Flipper.size.x * flipper.side.direction) / 2 - + (1.65 * flipper.side.direction), + -0.15, + ); + } +} + +/// {@template flipper_anchor_revolute_joint_def} +/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve a potivoting +/// motion. +/// {@endtemplate} +class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef { + /// {@macro flipper_anchor_revolute_joint_def} + _FlipperAnchorRevoluteJointDef({ + required Flipper flipper, + required _FlipperAnchor anchor, + }) { + initialize( + flipper.body, + anchor.body, + flipper.body.position + anchor.body.position, + ); + + enableLimit = true; + upperAngle = 0.611; + lowerAngle = -upperAngle; + } +} diff --git a/lib/game/components/controlled_flipper.dart b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart similarity index 50% rename from lib/game/components/controlled_flipper.dart rename to packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart index 1d5502c6..95566e75 100644 --- a/lib/game/components/controlled_flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper/behaviors/flipper_key_controlling_behavior.dart @@ -1,49 +1,33 @@ import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/services.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; -/// {@template controlled_flipper} -/// A [Flipper] with a [FlipperController] attached. -/// {@endtemplate} -class ControlledFlipper extends Flipper with Controls { - /// {@macro controlled_flipper} - ControlledFlipper({ - required BoardSide side, - }) : super(side: side) { - controller = FlipperController(this); - } -} - -/// {@template flipper_controller} -/// A [ComponentController] that controls a [Flipper]s movement. -/// {@endtemplate} -class FlipperController extends ComponentController - with KeyboardHandler, FlameBlocReader { - /// {@macro flipper_controller} - FlipperController(Flipper flipper) - : _keys = flipper.side.flipperKeys, - super(flipper); - +/// Allows controlling the [Flipper]'s movement with keyboard input. +class FlipperKeyControllingBehavior extends Component + with KeyboardHandler, ParentIsA { /// The [LogicalKeyboardKey]s that will control the [Flipper]. /// /// [onKeyEvent] method listens to when one of these keys is pressed. - final List _keys; + late final List _keys; + + @override + Future onLoad() async { + await super.onLoad(); + _keys = parent.side.flipperKeys; + } @override bool onKeyEvent( RawKeyEvent event, Set keysPressed, ) { - if (!bloc.state.status.isPlaying) return true; if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { - component.moveUp(); + parent.moveUp(); } else if (event is RawKeyUpEvent) { - component.moveDown(); + parent.moveDown(); } return false; diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper/flipper.dart similarity index 56% rename from packages/pinball_components/lib/src/components/flipper.dart rename to packages/pinball_components/lib/src/components/flipper/flipper.dart index ca033440..280c157f 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper/flipper.dart @@ -2,8 +2,11 @@ import 'dart:async'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/foundation.dart'; import 'package:pinball_components/pinball_components.dart'; +export 'behaviors/behaviors.dart'; + /// {@template flipper} /// A bat, typically found in pairs at the bottom of the board. /// @@ -15,9 +18,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { required this.side, }) : super( renderBody: false, - children: [_FlipperSpriteComponent(side: side)], + children: [ + _FlipperSpriteComponent(side: side), + FlipperJointingBehavior(), + ], ); + /// Creates a [Flipper] without any children. + /// + /// This can be used for testing [Flipper]'s behaviors in isolation. + @visibleForTesting + Flipper.test({required this.side}); + /// The size of the [Flipper]. static final size = Vector2(13.5, 4.3); @@ -44,19 +56,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { body.linearVelocity = Vector2(0, -_speed); } - /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. - Future _anchorToJoint() async { - final anchor = _FlipperAnchor(flipper: this); - await add(anchor); - - final jointDef = _FlipperAnchorRevoluteJointDef( - flipper: this, - anchor: anchor, - ); - final joint = _FlipperJoint(jointDef); - world.createJoint(joint); - } - List _createFixtureDefs() { final direction = side.direction; @@ -73,7 +72,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { assetShadow, 0, ); - final bigCircleFixtureDef = FixtureDef(bigCircleShape); final smallCircleShape = CircleShape()..radius = size.y * 0.23; smallCircleShape.position.setValues( @@ -82,7 +80,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { assetShadow, 0, ); - final smallCircleFixtureDef = FixtureDef(smallCircleShape); final trapeziumVertices = side.isLeft ? [ @@ -98,26 +95,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { Vector2(smallCircleShape.position.x, -smallCircleShape.radius), ]; final trapezium = PolygonShape()..set(trapeziumVertices); - final trapeziumFixtureDef = FixtureDef( - trapezium, - density: 50, - friction: .1, - ); return [ - bigCircleFixtureDef, - smallCircleFixtureDef, - trapeziumFixtureDef, + FixtureDef(bigCircleShape), + FixtureDef(smallCircleShape), + FixtureDef( + trapezium, + density: 50, + friction: .1, + ), ]; } - @override - Future onLoad() async { - await super.onLoad(); - - await _anchorToJoint(); - } - @override Body createBody() { final bodyDef = BodyDef( @@ -131,15 +120,6 @@ 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 { @@ -163,73 +143,3 @@ class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { size = sprite.originalSize / 10; } } - -/// {@template flipper_anchor} -/// [JointAnchor] positioned at the end of a [Flipper]. -/// -/// The end of a [Flipper] depends on its [Flipper.side]. -/// {@endtemplate} -class _FlipperAnchor extends JointAnchor { - /// {@macro flipper_anchor} - _FlipperAnchor({ - required Flipper flipper, - }) { - initialPosition = Vector2( - (Flipper.size.x * flipper.side.direction) / 2 - - (1.65 * flipper.side.direction), - -0.15, - ); - } -} - -/// {@template flipper_anchor_revolute_joint_def} -/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion. -/// {@endtemplate} -class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef { - /// {@macro flipper_anchor_revolute_joint_def} - _FlipperAnchorRevoluteJointDef({ - required Flipper flipper, - required _FlipperAnchor anchor, - }) : side = flipper.side { - enableLimit = true; - initialize( - flipper.body, - anchor.body, - flipper.body.position + anchor.body.position, - ); - } - - final BoardSide side; -} - -/// {@template flipper_joint} -/// [RevoluteJoint] that controls the arc motion of a [Flipper]. -/// {@endtemplate} -class _FlipperJoint extends RevoluteJoint { - /// {@macro flipper_joint} - _FlipperJoint(_FlipperAnchorRevoluteJointDef def) - : side = def.side, - super(def) { - lock(); - } - - /// Half the angle of the arc motion. - static const _halfSweepingAngle = 0.611; - - final BoardSide side; - - /// Locks the [Flipper] to its resting position. - /// - /// The joint is locked when initialized in order to force the [Flipper] - /// at its resting position. - void lock() { - final angle = _halfSweepingAngle * side.direction; - setLimits(angle, angle); - } - - /// Unlocks the [Flipper] from its resting position. - void unlock() { - const angle = _halfSweepingAngle; - setLimits(-angle, angle); - } -} diff --git a/packages/pinball_components/lib/src/components/score_component/behaviors/behaviors.dart b/packages/pinball_components/lib/src/components/score_component/behaviors/behaviors.dart new file mode 100644 index 00000000..dba073f0 --- /dev/null +++ b/packages/pinball_components/lib/src/components/score_component/behaviors/behaviors.dart @@ -0,0 +1 @@ +export 'score_component_scaling_behavior.dart'; diff --git a/packages/pinball_components/lib/src/components/score_component/behaviors/score_component_scaling_behavior.dart b/packages/pinball_components/lib/src/components/score_component/behaviors/score_component_scaling_behavior.dart new file mode 100644 index 00000000..5e3d184b --- /dev/null +++ b/packages/pinball_components/lib/src/components/score_component/behaviors/score_component_scaling_behavior.dart @@ -0,0 +1,24 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// Scales a [ScoreComponent] according to its position on the board. +class ScoreComponentScalingBehavior extends Component + with ParentIsA { + @override + void update(double dt) { + super.update(dt); + final boardHeight = BoardDimensions.bounds.height; + const maxShrinkValue = 0.83; + + final augmentedPosition = parent.position.y * 3; + final standardizedYPosition = augmentedPosition + (boardHeight / 2); + final scaleFactor = maxShrinkValue + + ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue)); + + parent.scale.setValues( + scaleFactor, + scaleFactor, + ); + } +} diff --git a/packages/pinball_components/lib/src/components/score_component.dart b/packages/pinball_components/lib/src/components/score_component/score_component.dart similarity index 77% rename from packages/pinball_components/lib/src/components/score_component.dart rename to packages/pinball_components/lib/src/components/score_component/score_component.dart index 0b9940aa..4908c337 100644 --- a/packages/pinball_components/lib/src/components/score_component.dart +++ b/packages/pinball_components/lib/src/components/score_component/score_component.dart @@ -2,7 +2,9 @@ import 'dart:async'; 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_components/src/components/score_component/behaviors/score_component_scaling_behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; enum Points { @@ -26,10 +28,25 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex { super( position: position, anchor: Anchor.center, + children: [ScoreComponentScalingBehavior()], ) { zIndex = ZIndexes.score; } + /// Creates a [ScoreComponent] without any children. + /// + /// This can be used for testing [ScoreComponent]'s behaviors in isolation. + @visibleForTesting + ScoreComponent.test({ + required this.points, + required Vector2 position, + required EffectController effectController, + }) : _effectController = effectController, + super( + position: position, + anchor: Anchor.center, + ); + late Points points; late final Effect _effect; diff --git a/packages/pinball_components/lib/src/components/signpost/signpost.dart b/packages/pinball_components/lib/src/components/signpost/signpost.dart index 115f1845..3ba486c1 100644 --- a/packages/pinball_components/lib/src/components/signpost/signpost.dart +++ b/packages/pinball_components/lib/src/components/signpost/signpost.dart @@ -9,7 +9,7 @@ export 'cubit/signpost_cubit.dart'; /// {@template signpost} /// A sign, found in the Flutter Forest. /// -/// Lights up a new sign whenever all three [DashNestBumper]s are hit. +/// Lights up a new sign whenever all three [DashBumper]s are hit. /// {@endtemplate} class Signpost extends BodyComponent with InitialPosition { /// {@macro signpost} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart index 57895c45..db98a30a 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior.dart @@ -9,7 +9,7 @@ import 'package:pinball_flame/pinball_flame.dart'; /// the [SpaceshipRamp]. /// {@endtemplate} class RampBallAscendingContactBehavior - extends ContactBehavior { + extends ContactBehavior { @override void beginContact(Object other, Contact contact) { super.beginContact(other, contact); diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart index 0796be92..07a5e79b 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart @@ -27,11 +27,6 @@ class SpaceshipRamp extends Component { required this.bloc, }) : super( children: [ - RampScoringSensor( - children: [ - RampBallAscendingContactBehavior(), - ], - )..initialPosition = Vector2(1.7, -20.4), _SpaceshipRampOpening( outsideLayer: Layer.spaceship, outsidePriority: ZIndexes.ballOnSpaceship, @@ -40,9 +35,9 @@ class SpaceshipRamp extends Component { ..initialPosition = Vector2(-13.7, -18.6) ..layer = Layer.spaceshipEntranceRamp, _SpaceshipRampBackground(), - _SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5), + SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5), _SpaceshipRampForegroundRailing(), - _SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), + SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), _SpaceshipRampBackgroundRailingSpriteComponent(), SpaceshipRampArrowSpriteComponent( current: bloc.state.hits, @@ -246,18 +241,24 @@ extension on SpaceshipRampArrowSpriteState { } } -class _SpaceshipRampBoardOpening extends BodyComponent - with Layered, ZIndex, InitialPosition { - _SpaceshipRampBoardOpening() +class SpaceshipRampBoardOpening extends BodyComponent + with Layered, ZIndex, InitialPosition, ParentIsA { + SpaceshipRampBoardOpening() : super( renderBody: false, children: [ _SpaceshipRampBoardOpeningSpriteComponent(), + RampBallAscendingContactBehavior()..applyTo(['inside']), LayerContactBehavior(layer: Layer.spaceshipEntranceRamp) ..applyTo(['inside']), - LayerContactBehavior(layer: Layer.board)..applyTo(['outside']), - ZIndexContactBehavior(zIndex: ZIndexes.ballOnBoard) - ..applyTo(['outside']), + LayerContactBehavior( + layer: Layer.board, + onBegin: false, + )..applyTo(['outside']), + ZIndexContactBehavior( + zIndex: ZIndexes.ballOnBoard, + onBegin: false, + )..applyTo(['outside']), ZIndexContactBehavior(zIndex: ZIndexes.ballOnSpaceshipRamp) ..applyTo(['middle', 'inside']), ], @@ -266,6 +267,13 @@ class _SpaceshipRampBoardOpening extends BodyComponent layer = Layer.opening; } + /// Creates a [SpaceshipRampBoardOpening] without any children. + /// + /// This can be used for testing [SpaceshipRampBoardOpening]'s behaviors in + /// isolation. + @visibleForTesting + SpaceshipRampBoardOpening.test(); + List _createFixtureDefs() { final topEdge = EdgeShape() ..set( @@ -426,9 +434,19 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent } } -class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition { - _SpaceshipRampBase() : super(renderBody: false) { - layer = Layer.board; +@visibleForTesting +class SpaceshipRampBase extends BodyComponent + with InitialPosition, ContactCallbacks { + SpaceshipRampBase() : super(renderBody: false); + + @override + void preSolve(Object other, Contact contact, Manifold oldManifold) { + super.preSolve(other, contact, oldManifold); + if (other is! Layered) return; + // Although, the Layer should already be taking care of the contact + // filtering, this is to ensure the ball doesn't collide with the ramp base + // when the filtering is calculated on different time steps. + contact.setEnabled(other.layer == Layer.board); } @override @@ -441,7 +459,7 @@ class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition { Vector2(4.1, 1.5), ], ); - final bodyDef = BodyDef(position: initialPosition); + final bodyDef = BodyDef(position: initialPosition, userData: this); return world.createBody(bodyDef)..createFixtureFromShape(shape); } } @@ -480,46 +498,3 @@ class _SpaceshipRampOpening extends LayerSensor { ); } } - -/// {@template ramp_scoring_sensor} -/// Small sensor body used to detect when a ball has entered the -/// [SpaceshipRamp]. -/// {@endtemplate} -class RampScoringSensor extends BodyComponent - with ParentIsA, InitialPosition, Layered { - /// {@macro ramp_scoring_sensor} - RampScoringSensor({ - Iterable? children, - }) : super( - children: children, - renderBody: false, - ) { - layer = Layer.spaceshipEntranceRamp; - } - - /// Creates a [RampScoringSensor] without any children. - @visibleForTesting - RampScoringSensor.test(); - - @override - Body createBody() { - final shape = PolygonShape() - ..setAsBox( - 2.6, - .5, - initialPosition, - -5 * math.pi / 180, - ); - - final fixtureDef = FixtureDef( - shape, - isSensor: true, - ); - final bodyDef = BodyDef( - position: initialPosition, - userData: this, - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} 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 index b0e1b0c8..ed000201 100644 --- a/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart +++ b/packages/pinball_components/lib/src/components/sparky_bumper/sparky_bumper.dart @@ -11,7 +11,7 @@ import 'package:pinball_flame/pinball_flame.dart'; export 'cubit/sparky_bumper_cubit.dart'; /// {@template sparky_bumper} -/// Bumper for Sparky area. +/// Bumper for the Sparky Scorch. /// {@endtemplate} class SparkyBumper extends BodyComponent with InitialPosition, ZIndex { /// {@macro sparky_bumper} diff --git a/packages/pinball_components/lib/src/components/sparky_computer/sparky_computer.dart b/packages/pinball_components/lib/src/components/sparky_computer/sparky_computer.dart index c96c5007..966eff02 100644 --- a/packages/pinball_components/lib/src/components/sparky_computer/sparky_computer.dart +++ b/packages/pinball_components/lib/src/components/sparky_computer/sparky_computer.dart @@ -65,7 +65,7 @@ class SparkyComputer extends BodyComponent { ..setAsBox( 1, 0.1, - Vector2(-13.2, -49.9), + Vector2(-13.1, -49.7), -0.18, ); diff --git a/packages/pinball_components/lib/src/components/z_indexes.dart b/packages/pinball_components/lib/src/components/z_indexes.dart index 2fbcc3af..0d0fab98 100644 --- a/packages/pinball_components/lib/src/components/z_indexes.dart +++ b/packages/pinball_components/lib/src/components/z_indexes.dart @@ -106,7 +106,7 @@ abstract class ZIndexes { // Score - static const score = _above + spaceshipRampForegroundRailing; + static const score = _above + sparkyAnimatronic; // Debug information diff --git a/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart b/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart index 789fa8b4..bdb23141 100644 --- a/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/bottom_group/flipper_game.dart @@ -1,6 +1,4 @@ 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'; @@ -23,16 +21,6 @@ class FlipperGame extends BallGame with KeyboardEvents { - Press right arrow key or "D" to move the right flipper. '''; - static const _leftFlipperKeys = [ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.keyA, - ]; - - static const _rightFlipperKeys = [ - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.keyD, - ]; - late Flipper leftFlipper; late Flipper rightFlipper; @@ -50,32 +38,4 @@ class FlipperGame extends BallGame with KeyboardEvents { await traceAllBodies(); } - - @override - KeyEventResult onKeyEvent( - RawKeyEvent event, - Set keysPressed, - ) { - final movedLeftFlipper = _leftFlipperKeys.contains(event.logicalKey); - if (movedLeftFlipper) { - if (event is RawKeyDownEvent) { - leftFlipper.moveUp(); - } else if (event is RawKeyUpEvent) { - leftFlipper.moveDown(); - } - } - - final movedRightFlipper = _rightFlipperKeys.contains(event.logicalKey); - if (movedRightFlipper) { - if (event is RawKeyDownEvent) { - rightFlipper.moveUp(); - } else if (event is RawKeyUpEvent) { - rightFlipper.moveDown(); - } - } - - return movedLeftFlipper || movedRightFlipper - ? KeyEventResult.handled - : KeyEventResult.ignored; - } } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/dash_bumper_a_game.dart similarity index 77% rename from packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart rename to packages/pinball_components/sandbox/lib/stories/flutter_forest/dash_bumper_a_game.dart index 071f6aa1..d81540b0 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/dash_bumper_a_game.dart @@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SmallDashNestBumperAGame extends BallGame { - SmallDashNestBumperAGame() +class DashBumperAGame extends BallGame { + DashBumperAGame() : super( imagesFileNames: [ Assets.images.dash.bumper.a.active.keyName, @@ -14,7 +14,7 @@ class SmallDashNestBumperAGame extends BallGame { ); static const description = ''' - Shows how a SmallDashNestBumper ("a") is rendered. + Shows how the "a" DashBumper is rendered. - Activate the "trace" parameter to overlay the body. '''; @@ -24,7 +24,7 @@ class SmallDashNestBumperAGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2.zero()); - await add(DashNestBumper.a()..priority = 1); + await add(DashBumper.a()..priority = 1); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/dash_bumper_b_game.dart similarity index 77% rename from packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart rename to packages/pinball_components/sandbox/lib/stories/flutter_forest/dash_bumper_b_game.dart index a47b9962..05664a3a 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/dash_bumper_b_game.dart @@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class SmallDashNestBumperBGame extends BallGame { - SmallDashNestBumperBGame() +class DashBumperBGame extends BallGame { + DashBumperBGame() : super( imagesFileNames: [ Assets.images.dash.bumper.b.active.keyName, @@ -14,7 +14,7 @@ class SmallDashNestBumperBGame extends BallGame { ); static const description = ''' - Shows how a SmallDashNestBumper ("b") is rendered. + Shows how the "b" DashBumper is rendered. - Activate the "trace" parameter to overlay the body. '''; @@ -24,7 +24,7 @@ class SmallDashNestBumperBGame extends BallGame { await super.onLoad(); camera.followVector2(Vector2.zero()); - await add(DashNestBumper.b()..priority = 1); + await add(DashBumper.b()..priority = 1); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/dash_bumper_main_game.dart similarity index 79% rename from packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart rename to packages/pinball_components/sandbox/lib/stories/flutter_forest/dash_bumper_main_game.dart index 3580a175..6a927eb9 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/dash_bumper_main_game.dart @@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; -class BigDashNestBumperGame extends BallGame { - BigDashNestBumperGame() +class DashBumperMainGame extends BallGame { + DashBumperMainGame() : super( imagesFileNames: [ Assets.images.dash.bumper.main.active.keyName, @@ -14,7 +14,7 @@ class BigDashNestBumperGame extends BallGame { ); static const description = ''' - Shows how a BigDashNestBumper is rendered. + Shows how the "main" DashBumper is rendered. - Activate the "trace" parameter to overlay the body. '''; @@ -25,7 +25,7 @@ class BigDashNestBumperGame extends BallGame { camera.followVector2(Vector2.zero()); await add( - DashNestBumper.main()..priority = 1, + DashBumper.main()..priority = 1, ); await traceAllBodies(); } 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 dd557a27..fcd64f79 100644 --- a/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart @@ -1,9 +1,9 @@ import 'package:dashbook/dashbook.dart'; import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/flutter_forest/big_dash_nest_bumper_game.dart'; +import 'package:sandbox/stories/flutter_forest/dash_bumper_a_game.dart'; +import 'package:sandbox/stories/flutter_forest/dash_bumper_b_game.dart'; +import 'package:sandbox/stories/flutter_forest/dash_bumper_main_game.dart'; 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 addFlutterForestStories(Dashbook dashbook) { dashbook.storiesOf('Flutter Forest') @@ -13,18 +13,18 @@ void addFlutterForestStories(Dashbook dashbook) { gameBuilder: (_) => SignpostGame(), ) ..addGame( - title: 'Big Dash Nest Bumper', - description: BigDashNestBumperGame.description, - gameBuilder: (_) => BigDashNestBumperGame(), + title: 'Main Dash Bumper', + description: DashBumperMainGame.description, + gameBuilder: (_) => DashBumperMainGame(), ) ..addGame( - title: 'Small Dash Nest Bumper A', - description: SmallDashNestBumperAGame.description, - gameBuilder: (_) => SmallDashNestBumperAGame(), + title: 'Dash Bumper A', + description: DashBumperAGame.description, + gameBuilder: (_) => DashBumperAGame(), ) ..addGame( - title: 'Small Dash Nest Bumper B', - description: SmallDashNestBumperBGame.description, - gameBuilder: (_) => SmallDashNestBumperBGame(), + title: 'Dash Bumper B', + description: DashBumperBGame.description, + gameBuilder: (_) => DashBumperBGame(), ); } diff --git a/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart b/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart index fff69b0a..70edd32e 100644 --- a/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart +++ b/packages/pinball_components/test/src/components/android_spaceship/android_spaceship_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: cascade_invocations -import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -21,9 +21,18 @@ void main() { Assets.images.android.spaceship.lightBeam.keyName, ]; final flameTester = FlameTester(() => TestGame(assets)); + late AndroidSpaceshipCubit bloc; + + setUp(() { + bloc = _MockAndroidSpaceshipCubit(); + }); flameTester.test('loads correctly', (game) async { - final component = AndroidSpaceship(position: Vector2.zero()); + final component = + FlameBlocProvider.value( + value: bloc, + children: [AndroidSpaceship(position: Vector2.zero())], + ); await game.ensureAdd(component); expect(game.contains(component), isTrue); }); @@ -33,7 +42,13 @@ void main() { setUp: (game, tester) async { await game.images.loadAll(assets); final canvas = ZCanvasComponent( - children: [AndroidSpaceship(position: Vector2.zero())], + children: [ + FlameBlocProvider.value( + value: bloc, + children: [AndroidSpaceship(position: Vector2.zero())], + ), + ], ); await game.ensureAdd(canvas); game.camera.followVector2(Vector2.zero()); @@ -70,28 +85,16 @@ void main() { }, ); - flameTester.test('closes bloc when removed', (game) async { - final bloc = _MockAndroidSpaceshipCubit(); - whenListen( - bloc, - const Stream.empty(), - initialState: AndroidSpaceshipState.withoutBonus, - ); - when(bloc.close).thenAnswer((_) async {}); - final androidSpaceship = AndroidSpaceship.test(bloc: bloc); - - await game.ensureAdd(androidSpaceship); - game.remove(androidSpaceship); - await game.ready(); - - verify(bloc.close).called(1); - }); - flameTester.test( 'AndroidSpaceshipEntrance has an ' 'AndroidSpaceshipEntranceBallContactBehavior', (game) async { final androidSpaceship = AndroidSpaceship(position: Vector2.zero()); - await game.ensureAdd(androidSpaceship); + final provider = + FlameBlocProvider.value( + value: bloc, + children: [androidSpaceship], + ); + await game.ensureAdd(provider); final androidSpaceshipEntrance = androidSpaceship.firstChild(); diff --git a/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart b/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart index d6056beb..4b0f16ea 100644 --- a/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/android_spaceship/behaviors/android_spaceship_entrance_ball_contact_behavior_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: cascade_invocations import 'package:bloc_test/bloc_test.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -43,16 +44,19 @@ void main() { ); final entrance = AndroidSpaceshipEntrance(); - final androidSpaceship = AndroidSpaceship.test( - bloc: bloc, - children: [entrance], + final androidSpaceship = FlameBlocProvider.value( + value: bloc, + children: [ + AndroidSpaceship.test(children: [entrance]) + ], ); await entrance.add(behavior); await game.ensureAdd(androidSpaceship); behavior.beginContact(_MockBall(), _MockContact()); - verify(androidSpaceship.bloc.onBallEntered).called(1); + verify(bloc.onBallEntered).called(1); }, ); }, diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_implusing_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_implusing_behavior_test.dart new file mode 100644 index 00000000..53ab4553 --- /dev/null +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_implusing_behavior_test.dart @@ -0,0 +1,53 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'BallImpulsingBehavior', + () { + final asset = theme.Assets.images.dash.ball.keyName; + final flameTester = FlameTester(() => TestGame([asset])); + + test('can be instantiated', () { + expect( + BallImpulsingBehavior(impulse: Vector2.zero()), + isA(), + ); + }); + + flameTester.test( + 'impulses the ball with the given velocity when loaded ' + 'and then removes itself', + (game) async { + final ball = Ball.test(); + await game.ensureAdd(ball); + final impulse = Vector2.all(1); + final behavior = BallImpulsingBehavior(impulse: impulse); + await ball.ensureAdd(behavior); + + expect( + ball.body.linearVelocity.x, + equals(impulse.x), + ); + expect( + ball.body.linearVelocity.y, + equals(impulse.y), + ); + expect( + game.descendants().whereType().isEmpty, + isTrue, + ); + }, + ); + }, + ); +} diff --git a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart index c4c66d4a..1fe84ae4 100644 --- a/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart +++ b/packages/pinball_components/test/src/components/ball/behaviors/ball_scaling_behavior_test.dart @@ -10,8 +10,9 @@ import '../../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final asset = theme.Assets.images.dash.ball.keyName; - final flameTester = FlameTester(() => TestGame([asset])); + final flameTester = FlameTester( + () => TestGame([theme.Assets.images.dash.ball.keyName]), + ); group('BallScalingBehavior', () { test('can be instantiated', () { 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_bumper_ball_contact_behavior_test.dart similarity index 55% rename from packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_nest_bumper_ball_contact_behavior_test.dart rename to packages/pinball_components/test/src/components/dash_nest_bumper/behaviors/dash_bumper_ball_contact_behavior_test.dart index 10627df6..3c8f51db 100644 --- 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_bumper_ball_contact_behavior_test.dart @@ -6,11 +6,11 @@ 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 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart'; import '../../../../helpers/helpers.dart'; -class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} +class _MockDashBumperCubit extends Mock implements DashBumperCubit {} class _MockBall extends Mock implements Ball {} @@ -21,33 +21,33 @@ void main() { final flameTester = FlameTester(TestGame.new); group( - 'DashNestBumperBallContactBehavior', + 'DashBumperBallContactBehavior', () { test('can be instantiated', () { expect( - DashNestBumperBallContactBehavior(), - isA(), + DashBumperBallContactBehavior(), + isA(), ); }); flameTester.test( 'beginContact emits onBallContacted when contacts with a ball', (game) async { - final behavior = DashNestBumperBallContactBehavior(); - final bloc = _MockDashNestBumperCubit(); + final behavior = DashBumperBallContactBehavior(); + final bloc = _MockDashBumperCubit(); whenListen( bloc, - const Stream.empty(), - initialState: DashNestBumperState.active, + const Stream.empty(), + initialState: DashBumperState.active, ); - final dashNestBumper = DashNestBumper.test(bloc: bloc); - await dashNestBumper.add(behavior); - await game.ensureAdd(dashNestBumper); + final bumper = DashBumper.test(bloc: bloc); + await bumper.add(behavior); + await game.ensureAdd(bumper); behavior.beginContact(_MockBall(), _MockContact()); - verify(dashNestBumper.bloc.onBallContacted).called(1); + verify(bumper.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_bumper_cubit_test.dart similarity index 53% rename from packages/pinball_components/test/src/components/dash_nest_bumper/cubit/dash_nest_bumper_cubit_test.dart rename to packages/pinball_components/test/src/components/dash_nest_bumper/cubit/dash_bumper_cubit_test.dart index 7e26bbf3..1b255cd5 100644 --- 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_bumper_cubit_test.dart @@ -4,20 +4,20 @@ import 'package:pinball_components/pinball_components.dart'; void main() { group( - 'DashNestBumperCubit', + 'DashBumperCubit', () { - blocTest( + blocTest( 'onBallContacted emits active', - build: DashNestBumperCubit.new, + build: DashBumperCubit.new, act: (bloc) => bloc.onBallContacted(), - expect: () => [DashNestBumperState.active], + expect: () => [DashBumperState.active], ); - blocTest( + blocTest( 'onReset emits inactive', - build: DashNestBumperCubit.new, + build: DashBumperCubit.new, act: (bloc) => bloc.onReset(), - expect: () => [DashNestBumperState.inactive], + expect: () => [DashBumperState.inactive], ); }, ); diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper/dash_bumper_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper/dash_bumper_test.dart new file mode 100644 index 00000000..a8ad8410 --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper/dash_bumper_test.dart @@ -0,0 +1,139 @@ +// 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/bumping_behavior.dart'; +import 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart'; + +import '../../../helpers/helpers.dart'; + +class _MockDashBumperCubit extends Mock implements DashBumperCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('DashBumper', () { + final flameTester = FlameTester( + () => TestGame( + [ + 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, + ], + ), + ); + + flameTester.test('"main" loads correctly', (game) async { + final bumper = DashBumper.main(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"a" loads correctly', (game) async { + final bumper = DashBumper.a(); + await game.ensureAdd(bumper); + + expect(game.contains(bumper), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final bumper = DashBumper.b(); + await game.ensureAdd(bumper); + expect(game.contains(bumper), isTrue); + }); + + // ignore: public_member_api_docs + flameTester.test('closes bloc when removed', (game) async { + final bloc = _MockDashBumperCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: DashBumperState.inactive, + ); + when(bloc.close).thenAnswer((_) async {}); + final bumper = DashBumper.test(bloc: bloc); + + await game.ensureAdd(bumper); + game.remove(bumper); + await game.ready(); + + verify(bloc.close).called(1); + }); + + flameTester.test('adds a bumperBallContactBehavior', (game) async { + final bumper = DashBumper.a(); + await game.ensureAdd(bumper); + expect( + bumper.children.whereType().single, + isNotNull, + ); + }); + + group("'main' adds", () { + flameTester.test('new children', (game) async { + final component = Component(); + final bumper = DashBumper.main( + children: [component], + ); + await game.ensureAdd(bumper); + expect(bumper.children, contains(component)); + }); + + flameTester.test('a BumpingBehavior', (game) async { + final bumper = DashBumper.main(); + await game.ensureAdd(bumper); + expect( + bumper.children.whereType().single, + isNotNull, + ); + }); + }); + + group("'a' adds", () { + flameTester.test('new children', (game) async { + final component = Component(); + final bumper = DashBumper.a( + children: [component], + ); + await game.ensureAdd(bumper); + expect(bumper.children, contains(component)); + }); + + flameTester.test('a BumpingBehavior', (game) async { + final bumper = DashBumper.a(); + await game.ensureAdd(bumper); + expect( + bumper.children.whereType().single, + isNotNull, + ); + }); + }); + + group("'b' adds", () { + flameTester.test('new children', (game) async { + final component = Component(); + final bumper = DashBumper.b( + children: [component], + ); + await game.ensureAdd(bumper); + expect(bumper.children, contains(component)); + }); + + flameTester.test('a BumpingBehavior', (game) async { + final bumper = DashBumper.b(); + await game.ensureAdd(bumper); + expect( + bumper.children.whereType().single, + isNotNull, + ); + }); + }); + }); +} 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 deleted file mode 100644 index baf2132f..00000000 --- a/packages/pinball_components/test/src/components/dash_nest_bumper/dash_nest_bumper_test.dart +++ /dev/null @@ -1,137 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/components.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball_components/pinball_components.dart'; -import 'package:pinball_components/src/components/bumping_behavior.dart'; -import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart'; - -import '../../../helpers/helpers.dart'; - -class _MockDashNestBumperCubit extends Mock implements DashNestBumperCubit {} - -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('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); - }); - - flameTester.test('adds a DashNestBumperBallContactBehavior', (game) async { - final dashNestBumper = DashNestBumper.a(); - await game.ensureAdd(dashNestBumper); - expect( - dashNestBumper.children - .whereType() - .single, - isNotNull, - ); - }); - - group("'main' adds", () { - flameTester.test('new children', (game) async { - final component = Component(); - final dashNestBumper = DashNestBumper.main( - children: [component], - ); - await game.ensureAdd(dashNestBumper); - expect(dashNestBumper.children, contains(component)); - }); - - flameTester.test('a BumpingBehavior', (game) async { - final dashNestBumper = DashNestBumper.main(); - await game.ensureAdd(dashNestBumper); - expect( - dashNestBumper.children.whereType().single, - isNotNull, - ); - }); - }); - - group("'a' adds", () { - flameTester.test('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 BumpingBehavior', (game) async { - final dashNestBumper = DashNestBumper.a(); - await game.ensureAdd(dashNestBumper); - expect( - dashNestBumper.children.whereType().single, - isNotNull, - ); - }); - }); - - group("'b' adds", () { - flameTester.test('new children', (game) async { - final component = Component(); - final dashNestBumper = DashNestBumper.b( - children: [component], - ); - await game.ensureAdd(dashNestBumper); - expect(dashNestBumper.children, contains(component)); - }); - - flameTester.test('a BumpingBehavior', (game) async { - final dashNestBumper = DashNestBumper.b(); - await game.ensureAdd(dashNestBumper); - expect( - dashNestBumper.children.whereType().single, - isNotNull, - ); - }); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart new file mode 100644 index 00000000..3d6c3b83 --- /dev/null +++ b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_jointing_behavior_test.dart @@ -0,0 +1,38 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/components/components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('FlipperJointingBehavior', () { + final flameTester = FlameTester(TestGame.new); + + test('can be instantiated', () { + expect( + FlipperJointingBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final behavior = FlipperJointingBehavior(); + final parent = Flipper.test(side: BoardSide.left); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + expect(parent.contains(behavior), isTrue); + }); + + flameTester.test('creates a joint', (game) async { + final behavior = FlipperJointingBehavior(); + final parent = Flipper.test(side: BoardSide.left); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + expect(parent.body.joints, isNotEmpty); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart new file mode 100644 index 00000000..11af6187 --- /dev/null +++ b/packages/pinball_components/test/src/components/flipper/behaviors/flipper_key_controlling_behavior_test.dart @@ -0,0 +1,357 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +class _MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +class _MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('FlipperKeyControllingBehavior', () { + final flameTester = FlameTester(TestGame.new); + + group( + 'onKeyEvent', + () { + late Flipper rightFlipper; + late Flipper leftFlipper; + + setUp(() { + rightFlipper = Flipper.test(side: BoardSide.right); + leftFlipper = Flipper.test(side: BoardSide.left); + }); + + group('on right Flipper', () { + flameTester.test( + 'moves upwards when right arrow is pressed', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowRight, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isNegative); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves downwards when right arrow is released', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowRight, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isPositive); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves upwards when D is pressed', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyD, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isNegative); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves downwards when D is released', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyD, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isPositive); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + group("doesn't move when", () { + flameTester.test( + 'left arrow is pressed', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowLeft, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isZero); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'left arrow is released', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowLeft, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isZero); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'A is pressed', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyA, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isZero); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'A is released', + (game) async { + await game.ensureAdd(rightFlipper); + final behavior = FlipperKeyControllingBehavior(); + await rightFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyA, + ); + + behavior.onKeyEvent(event, {}); + + expect(rightFlipper.body.linearVelocity.y, isZero); + expect(rightFlipper.body.linearVelocity.x, isZero); + }, + ); + }); + }); + + group('on left Flipper', () { + flameTester.test( + 'moves upwards when left arrow is pressed', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowLeft, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isNegative); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves downwards when left arrow is released', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowLeft, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isPositive); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves upwards when A is pressed', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyA, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isNegative); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'moves downwards when A is released', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyA, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isPositive); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + group("doesn't move when", () { + flameTester.test( + 'right arrow is pressed', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowRight, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isZero); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'right arrow is released', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.arrowRight, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isZero); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'D is pressed', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyD, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isZero); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + + flameTester.test( + 'D is released', + (game) async { + await game.ensureAdd(leftFlipper); + final behavior = FlipperKeyControllingBehavior(); + await leftFlipper.ensureAdd(behavior); + + final event = _MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn( + LogicalKeyboardKey.keyD, + ); + + behavior.onKeyEvent(event, {}); + + expect(leftFlipper.body.linearVelocity.y, isZero); + expect(leftFlipper.body.linearVelocity.x, isZero); + }, + ); + }); + }); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/flipper_test.dart b/packages/pinball_components/test/src/components/flipper/flipper_test.dart similarity index 97% rename from packages/pinball_components/test/src/components/flipper_test.dart rename to packages/pinball_components/test/src/components/flipper/flipper_test.dart index 53b0e108..4569f3ec 100644 --- a/packages/pinball_components/test/src/components/flipper_test.dart +++ b/packages/pinball_components/test/src/components/flipper/flipper_test.dart @@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -36,7 +36,7 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/flipper.png'), + matchesGoldenFile('../golden/flipper.png'), ); }, ); diff --git a/packages/pinball_components/test/src/components/golden/boundaries.png b/packages/pinball_components/test/src/components/golden/boundaries.png index 68f57a86..e8075f63 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/dino-walls.png b/packages/pinball_components/test/src/components/golden/dino-walls.png index 31b317c1..1a50449d 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/score_component/behaviors/score_component_scaling_behavior_test.dart b/packages/pinball_components/test/src/components/score_component/behaviors/score_component_scaling_behavior_test.dart new file mode 100644 index 00000000..9bf0db2f --- /dev/null +++ b/packages/pinball_components/test/src/components/score_component/behaviors/score_component_scaling_behavior_test.dart @@ -0,0 +1,74 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/score_component/behaviors/behaviors.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + group('ScoreComponentScalingBehavior', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester( + () => TestGame([ + Assets.images.score.fiveThousand.keyName, + Assets.images.score.twentyThousand.keyName, + Assets.images.score.twoHundredThousand.keyName, + Assets.images.score.oneMillion.keyName, + ]), + ); + + test('can be instantiated', () { + expect( + ScoreComponentScalingBehavior(), + isA(), + ); + }); + + flameTester.test('can be loaded', (game) async { + final parent = ScoreComponent.test( + points: Points.fiveThousand, + position: Vector2.zero(), + effectController: EffectController(duration: 1), + ); + final behavior = ScoreComponentScalingBehavior(); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + expect(parent.children, contains(behavior)); + }); + + flameTester.test( + 'scales the sprite', + (game) async { + final parent1 = ScoreComponent.test( + points: Points.fiveThousand, + position: Vector2(0, 10), + effectController: EffectController(duration: 1), + ); + final parent2 = ScoreComponent.test( + points: Points.fiveThousand, + position: Vector2(0, -10), + effectController: EffectController(duration: 1), + ); + await game.ensureAddAll([parent1, parent2]); + + await parent1.ensureAdd(ScoreComponentScalingBehavior()); + await parent2.ensureAdd(ScoreComponentScalingBehavior()); + game.update(1); + + expect( + parent1.scale.x, + greaterThan(parent2.scale.x), + ); + expect( + parent1.scale.y, + greaterThan(parent2.scale.y), + ); + }, + ); + }); +} diff --git a/packages/pinball_components/test/src/components/score_component_test.dart b/packages/pinball_components/test/src/components/score_component/score_component_test.dart similarity index 72% rename from packages/pinball_components/test/src/components/score_component_test.dart rename to packages/pinball_components/test/src/components/score_component/score_component_test.dart index f2bd52e3..00c08d5c 100644 --- a/packages/pinball_components/test/src/components/score_component_test.dart +++ b/packages/pinball_components/test/src/components/score_component/score_component_test.dart @@ -5,24 +5,37 @@ import 'package:flame/effects.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/score_component/behaviors/behaviors.dart'; -import '../../helpers/helpers.dart'; +import '../../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final assets = [ - Assets.images.score.fiveThousand.keyName, - Assets.images.score.twentyThousand.keyName, - Assets.images.score.twoHundredThousand.keyName, - Assets.images.score.oneMillion.keyName, - ]; - final flameTester = FlameTester(() => TestGame(assets)); + final flameTester = FlameTester( + () => TestGame([ + Assets.images.score.fiveThousand.keyName, + Assets.images.score.twentyThousand.keyName, + Assets.images.score.twoHundredThousand.keyName, + Assets.images.score.oneMillion.keyName, + ]), + ); group('ScoreComponent', () { + test('can be instantiated', () { + expect( + ScoreComponent( + points: Points.fiveThousand, + position: Vector2.zero(), + effectController: EffectController(duration: 1), + ), + isA(), + ); + }); + flameTester.testGameWidget( 'loads correctly', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); game.camera.followVector2(Vector2.zero()); await game.ensureAdd( ScoreComponent( @@ -38,13 +51,32 @@ void main() { }, ); + flameTester.test( + 'adds a ScoreComponentScalingBehavior', + (game) async { + await game.onLoad(); + game.camera.followVector2(Vector2.zero()); + final component = ScoreComponent( + points: Points.oneMillion, + position: Vector2.zero(), + effectController: EffectController(duration: 1), + ); + await game.ensureAdd(component); + + expect( + component.children.whereType().length, + equals(1), + ); + }, + ); + flameTester.testGameWidget( 'has a movement effect', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); game.camera.followVector2(Vector2.zero()); await game.ensureAdd( - ScoreComponent( + ScoreComponent.test( points: Points.oneMillion, position: Vector2.zero(), effectController: EffectController(duration: 1), @@ -63,10 +95,10 @@ void main() { flameTester.testGameWidget( 'is removed once finished', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); game.camera.followVector2(Vector2.zero()); await game.ensureAdd( - ScoreComponent( + ScoreComponent.test( points: Points.oneMillion, position: Vector2.zero(), effectController: EffectController(duration: 1), @@ -83,12 +115,14 @@ void main() { ); group('renders correctly', () { + const goldensPath = '../golden/score/'; + flameTester.testGameWidget( '5000 points', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); await game.ensureAdd( - ScoreComponent( + ScoreComponent.test( points: Points.fiveThousand, position: Vector2.zero(), effectController: EffectController(duration: 1), @@ -104,7 +138,7 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/score/5k.png'), + matchesGoldenFile('${goldensPath}5k.png'), ); }, ); @@ -112,9 +146,9 @@ void main() { flameTester.testGameWidget( '20000 points', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); await game.ensureAdd( - ScoreComponent( + ScoreComponent.test( points: Points.twentyThousand, position: Vector2.zero(), effectController: EffectController(duration: 1), @@ -130,7 +164,7 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/score/20k.png'), + matchesGoldenFile('${goldensPath}20k.png'), ); }, ); @@ -138,9 +172,9 @@ void main() { flameTester.testGameWidget( '200000 points', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); await game.ensureAdd( - ScoreComponent( + ScoreComponent.test( points: Points.twoHundredThousand, position: Vector2.zero(), effectController: EffectController(duration: 1), @@ -156,7 +190,7 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/score/200k.png'), + matchesGoldenFile('${goldensPath}200k.png'), ); }, ); @@ -164,9 +198,9 @@ void main() { flameTester.testGameWidget( '1000000 points', setUp: (game, tester) async { - await game.images.loadAll(assets); + await game.onLoad(); await game.ensureAdd( - ScoreComponent( + ScoreComponent.test( points: Points.oneMillion, position: Vector2.zero(), effectController: EffectController(duration: 1), @@ -182,7 +216,7 @@ void main() { verify: (game, tester) async { await expectLater( find.byGame(), - matchesGoldenFile('golden/score/1m.png'), + matchesGoldenFile('${goldensPath}1m.png'), ); }, ); diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart index ea37550a..d1f03ce7 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/behavior/ramp_ball_ascending_contact_behavior_test.dart @@ -67,16 +67,16 @@ void main() { initialState: const SpaceshipRampState.initial(), ); - final rampSensor = RampScoringSensor.test(); + final parent = SpaceshipRampBoardOpening.test(); final spaceshipRamp = SpaceshipRamp.test( bloc: bloc, ); when(() => body.linearVelocity).thenReturn(Vector2(0, -1)); - await spaceshipRamp.add(rampSensor); + await spaceshipRamp.add(parent); await game.ensureAddAll([spaceshipRamp, ball]); - await rampSensor.add(behavior); + await parent.add(behavior); behavior.beginContact(ball, _MockContact()); @@ -95,16 +95,16 @@ void main() { initialState: const SpaceshipRampState.initial(), ); - final rampSensor = RampScoringSensor.test(); + final parent = SpaceshipRampBoardOpening.test(); final spaceshipRamp = SpaceshipRamp.test( bloc: bloc, ); when(() => body.linearVelocity).thenReturn(Vector2(0, 1)); - await spaceshipRamp.add(rampSensor); + await spaceshipRamp.add(parent); await game.ensureAddAll([spaceshipRamp, ball]); - await rampSensor.add(behavior); + await parent.add(behavior); behavior.beginContact(ball, _MockContact()); diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart index b74cfb88..1c9c968d 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart @@ -2,16 +2,24 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; import '../../../helpers/helpers.dart'; class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} +class _MockBall extends Mock implements Ball {} + +class _MockContact extends Mock implements Contact {} + +class _MockManifold extends Mock implements Manifold {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ @@ -275,4 +283,71 @@ void main() { }); }); }); + + group('SpaceshipRampBase', () { + test('can be instantiated', () { + expect(SpaceshipRampBase(), isA()); + }); + + flameTester.test('can be loaded', (game) async { + final component = SpaceshipRampBase(); + await game.ensureAdd(component); + expect(game.children, contains(component)); + }); + + flameTester.test( + 'postSolves disables contact when ball is not on Layer.board', + (game) async { + final ball = _MockBall(); + final contact = _MockContact(); + when(() => ball.layer).thenReturn(Layer.spaceshipEntranceRamp); + final component = SpaceshipRampBase(); + await game.ensureAdd(component); + + component.preSolve(ball, contact, _MockManifold()); + + verify(() => contact.setEnabled(false)).called(1); + }, + ); + + flameTester.test( + 'postSolves enables contact when ball is on Layer.board', + (game) async { + final ball = _MockBall(); + final contact = _MockContact(); + when(() => ball.layer).thenReturn(Layer.board); + final component = SpaceshipRampBase(); + await game.ensureAdd(component); + + component.preSolve(ball, contact, _MockManifold()); + + verify(() => contact.setEnabled(true)).called(1); + }, + ); + }); + + group('SpaceshipRampBoardOpening', () { + test('can be instantiated', () { + expect(SpaceshipRampBoardOpening(), isA()); + }); + + flameTester.test('can be loaded', (game) async { + final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit()); + final component = SpaceshipRampBoardOpening(); + await game.ensureAdd(parent); + await parent.ensureAdd(component); + expect(parent.children, contains(component)); + }); + + flameTester.test('adds a RampBallAscendingContactBehavior', (game) async { + final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit()); + final component = SpaceshipRampBoardOpening(); + await game.ensureAdd(parent); + await parent.ensureAdd(component); + expect( + component.children.whereType().length, + equals(1), + ); + }); + }); } diff --git a/packages/pinball_flame/lib/src/behaviors/layer_contact_behavior.dart b/packages/pinball_flame/lib/src/behaviors/layer_contact_behavior.dart index a73b94a5..ef475bc9 100644 --- a/packages/pinball_flame/lib/src/behaviors/layer_contact_behavior.dart +++ b/packages/pinball_flame/lib/src/behaviors/layer_contact_behavior.dart @@ -6,15 +6,20 @@ import 'package:pinball_flame/pinball_flame.dart'; /// {@endtemplate} class LayerContactBehavior extends ContactBehavior { /// {@macro layer_contact_behavior} - LayerContactBehavior({required Layer layer}) : _layer = layer; - - final Layer _layer; + LayerContactBehavior({ + required Layer layer, + bool onBegin = true, + }) { + if (onBegin) { + onBeginContact = (other, _) => _changeLayer(other, layer); + } else { + onEndContact = (other, _) => _changeLayer(other, layer); + } + } - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); + void _changeLayer(Object other, Layer layer) { if (other is! Layered) return; - if (other.layer == _layer) return; - other.layer = _layer; + if (other.layer == layer) return; + other.layer = layer; } } diff --git a/packages/pinball_flame/lib/src/behaviors/z_index_contact_behavior.dart b/packages/pinball_flame/lib/src/behaviors/z_index_contact_behavior.dart index ea9bfcad..c763e2cf 100644 --- a/packages/pinball_flame/lib/src/behaviors/z_index_contact_behavior.dart +++ b/packages/pinball_flame/lib/src/behaviors/z_index_contact_behavior.dart @@ -6,15 +6,20 @@ import 'package:pinball_flame/pinball_flame.dart'; /// {@endtemplate} class ZIndexContactBehavior extends ContactBehavior { /// {@macro layer_contact_behavior} - ZIndexContactBehavior({required int zIndex}) : _zIndex = zIndex; - - final int _zIndex; + ZIndexContactBehavior({ + required int zIndex, + bool onBegin = true, + }) { + if (onBegin) { + onBeginContact = (other, _) => _changeZIndex(other, zIndex); + } else { + onEndContact = (other, _) => _changeZIndex(other, zIndex); + } + } - @override - void beginContact(Object other, Contact contact) { - super.beginContact(other, contact); + void _changeZIndex(Object other, int zIndex) { if (other is! ZIndex) return; - if (other.zIndex == _zIndex) return; - other.zIndex = _zIndex; + if (other.zIndex == zIndex) return; + other.zIndex = zIndex; } } diff --git a/packages/pinball_flame/lib/src/keyboard_input_controller.dart b/packages/pinball_flame/lib/src/keyboard_input_controller.dart index 8249e599..b0d64ee6 100644 --- a/packages/pinball_flame/lib/src/keyboard_input_controller.dart +++ b/packages/pinball_flame/lib/src/keyboard_input_controller.dart @@ -1,4 +1,5 @@ import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flutter/services.dart'; /// The signature for a key handle function @@ -18,6 +19,17 @@ class KeyboardInputController extends Component with KeyboardHandler { final Map _keyUp; final Map _keyDown; + /// Trigger a virtual key up event. + bool onVirtualKeyUp(LogicalKeyboardKey key) { + final handler = _keyUp[key]; + + if (handler != null) { + return handler(); + } + + return true; + } + @override bool onKeyEvent(RawKeyEvent event, Set keysPressed) { final isUp = event is RawKeyUpEvent; @@ -32,3 +44,18 @@ class KeyboardInputController extends Component with KeyboardHandler { return true; } } + +/// Add the ability to virtually trigger key events to a [FlameGame]'s +/// [KeyboardInputController]. +extension VirtualKeyEvents on FlameGame { + /// Trigger a key up + void triggerVirtualKeyUp(LogicalKeyboardKey key) { + final keyControllers = descendants().whereType(); + + for (final controller in keyControllers) { + if (!controller.onVirtualKeyUp(key)) { + break; + } + } + } +} diff --git a/packages/pinball_flame/test/src/behaviors/layer_contact_behavior_test.dart b/packages/pinball_flame/test/src/behaviors/layer_contact_behavior_test.dart index 49040977..d4b7ba18 100644 --- a/packages/pinball_flame/test/src/behaviors/layer_contact_behavior_test.dart +++ b/packages/pinball_flame/test/src/behaviors/layer_contact_behavior_test.dart @@ -56,5 +56,23 @@ void main() { expect(component.layer, newLayer); }); + + flameTester.test('endContact changes layer', (game) async { + const oldLayer = Layer.all; + const newLayer = Layer.board; + final behavior = LayerContactBehavior( + layer: newLayer, + onBegin: false, + ); + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + final component = _TestLayeredBodyComponent(layer: oldLayer); + + behavior.endContact(component, _MockContact()); + + expect(component.layer, newLayer); + }); }); } diff --git a/packages/pinball_flame/test/src/behaviors/z_index_contact_behavior_test.dart b/packages/pinball_flame/test/src/behaviors/z_index_contact_behavior_test.dart index ad09004c..292a51fc 100644 --- a/packages/pinball_flame/test/src/behaviors/z_index_contact_behavior_test.dart +++ b/packages/pinball_flame/test/src/behaviors/z_index_contact_behavior_test.dart @@ -56,5 +56,20 @@ void main() { expect(component.zIndex, newIndex); }); + + flameTester.test('endContact changes zIndex', (game) async { + const oldIndex = 0; + const newIndex = 1; + final behavior = ZIndexContactBehavior(zIndex: newIndex, onBegin: false); + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + final component = _TestZIndexBodyComponent(zIndex: oldIndex); + + behavior.endContact(component, _MockContact()); + + expect(component.zIndex, newIndex); + }); }); } diff --git a/packages/pinball_flame/test/src/keyboard_input_controller_test.dart b/packages/pinball_flame/test/src/keyboard_input_controller_test.dart index 7b554e8c..f3c783ad 100644 --- a/packages/pinball_flame/test/src/keyboard_input_controller_test.dart +++ b/packages/pinball_flame/test/src/keyboard_input_controller_test.dart @@ -1,11 +1,36 @@ // ignore_for_file: cascade_invocations, one_member_abstracts +import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball_flame/pinball_flame.dart'; +class _TestGame extends FlameGame { + bool pressed = false; + + @override + Future? onLoad() async { + await super.onLoad(); + + await add( + KeyboardInputController( + keyUp: { + LogicalKeyboardKey.enter: () { + pressed = true; + return true; + }, + LogicalKeyboardKey.escape: () { + return false; + }, + }, + ), + ); + } +} + abstract class _KeyCall { bool onCall(); } @@ -75,4 +100,15 @@ void main() { }, ); }); + group('VirtualKeyEvents', () { + final flameTester = FlameTester(_TestGame.new); + + group('onVirtualKeyUp', () { + flameTester.test('triggers the event', (game) async { + await game.ready(); + game.triggerVirtualKeyUp(LogicalKeyboardKey.enter); + expect(game.pressed, isTrue); + }); + }); + }); } diff --git a/packages/pinball_ui/assets/images/button/dpad_down.png b/packages/pinball_ui/assets/images/button/dpad_down.png new file mode 100644 index 00000000..11bbb26f Binary files /dev/null and b/packages/pinball_ui/assets/images/button/dpad_down.png differ diff --git a/packages/pinball_ui/assets/images/button/dpad_left.png b/packages/pinball_ui/assets/images/button/dpad_left.png new file mode 100644 index 00000000..943cacc4 Binary files /dev/null and b/packages/pinball_ui/assets/images/button/dpad_left.png differ diff --git a/packages/pinball_ui/assets/images/button/dpad_right.png b/packages/pinball_ui/assets/images/button/dpad_right.png new file mode 100644 index 00000000..724b9f3e Binary files /dev/null and b/packages/pinball_ui/assets/images/button/dpad_right.png differ diff --git a/packages/pinball_ui/assets/images/button/dpad_up.png b/packages/pinball_ui/assets/images/button/dpad_up.png new file mode 100644 index 00000000..d1175d57 Binary files /dev/null and b/packages/pinball_ui/assets/images/button/dpad_up.png differ diff --git a/packages/pinball_ui/lib/gen/assets.gen.dart b/packages/pinball_ui/lib/gen/assets.gen.dart index 8972e8e0..9b09b254 100644 --- a/packages/pinball_ui/lib/gen/assets.gen.dart +++ b/packages/pinball_ui/lib/gen/assets.gen.dart @@ -3,8 +3,6 @@ /// FlutterGen /// ***************************************************** -// ignore_for_file: directives_ordering,unnecessary_import - import 'package:flutter/widgets.dart'; class $AssetsImagesGen { @@ -17,7 +15,14 @@ class $AssetsImagesGen { class $AssetsImagesButtonGen { const $AssetsImagesButtonGen(); - /// File path: assets/images/button/pinball_button.png + AssetGenImage get dpadDown => + const AssetGenImage('assets/images/button/dpad_down.png'); + AssetGenImage get dpadLeft => + const AssetGenImage('assets/images/button/dpad_left.png'); + AssetGenImage get dpadRight => + const AssetGenImage('assets/images/button/dpad_right.png'); + AssetGenImage get dpadUp => + const AssetGenImage('assets/images/button/dpad_up.png'); AssetGenImage get pinballButton => const AssetGenImage('assets/images/button/pinball_button.png'); } @@ -25,7 +30,6 @@ class $AssetsImagesButtonGen { class $AssetsImagesDialogGen { const $AssetsImagesDialogGen(); - /// File path: assets/images/dialog/background.png AssetGenImage get background => const AssetGenImage('assets/images/dialog/background.png'); } diff --git a/packages/pinball_ui/lib/gen/fonts.gen.dart b/packages/pinball_ui/lib/gen/fonts.gen.dart index 5f77da16..b15f2dd0 100644 --- a/packages/pinball_ui/lib/gen/fonts.gen.dart +++ b/packages/pinball_ui/lib/gen/fonts.gen.dart @@ -3,14 +3,9 @@ /// 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/src/widgets/pinball_dpad_button.dart b/packages/pinball_ui/lib/src/widgets/pinball_dpad_button.dart new file mode 100644 index 00000000..6d929f53 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/pinball_dpad_button.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/gen/gen.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// Enum with all possibile directions of a [PinballDpadButton]. +enum PinballDpadDirection { + /// Up + up, + + /// Down + down, + + /// Left + left, + + /// Right + right, +} + +extension _PinballDpadDirectionX on PinballDpadDirection { + String toAsset() { + switch (this) { + case PinballDpadDirection.up: + return Assets.images.button.dpadUp.keyName; + case PinballDpadDirection.down: + return Assets.images.button.dpadDown.keyName; + case PinballDpadDirection.left: + return Assets.images.button.dpadLeft.keyName; + case PinballDpadDirection.right: + return Assets.images.button.dpadRight.keyName; + } + } +} + +/// {@template pinball_dpad_button} +/// Widget that renders a Dpad button with a given direction. +/// {@endtemplate} +class PinballDpadButton extends StatelessWidget { + /// {@macro pinball_dpad_button} + const PinballDpadButton({ + Key? key, + required this.direction, + required this.onTap, + }) : super(key: key); + + /// Which [PinballDpadDirection] this button is. + final PinballDpadDirection direction; + + /// The function executed when the button is pressed. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: PinballColors.transparent, + child: InkWell( + onTap: onTap, + child: Image.asset( + direction.toAsset(), + width: 60, + height: 60, + ), + ), + ); + } +} diff --git a/packages/pinball_ui/lib/src/widgets/widgets.dart b/packages/pinball_ui/lib/src/widgets/widgets.dart index 3aa96c3e..45a6daad 100644 --- a/packages/pinball_ui/lib/src/widgets/widgets.dart +++ b/packages/pinball_ui/lib/src/widgets/widgets.dart @@ -1,4 +1,5 @@ export 'animated_ellipsis_text.dart'; export 'crt_background.dart'; export 'pinball_button.dart'; +export 'pinball_dpad_button.dart'; export 'pinball_loading_indicator.dart'; diff --git a/packages/pinball_ui/test/src/widgets/pinball_dpad_button_test.dart b/packages/pinball_ui/test/src/widgets/pinball_dpad_button_test.dart new file mode 100644 index 00000000..a7e89534 --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/pinball_dpad_button_test.dart @@ -0,0 +1,122 @@ +// ignore_for_file: one_member_abstracts + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_ui/gen/gen.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +extension _WidgetTesterX on WidgetTester { + Future pumpButton({ + required PinballDpadDirection direction, + required VoidCallback onTap, + }) async { + await pumpWidget( + MaterialApp( + home: Scaffold( + body: PinballDpadButton( + direction: direction, + onTap: onTap, + ), + ), + ), + ); + } +} + +extension _CommonFindersX on CommonFinders { + Finder byImagePath(String path) { + return find.byWidgetPredicate( + (widget) { + if (widget is Image) { + final image = widget.image; + + if (image is AssetImage) { + return image.keyName == path; + } + return false; + } + + return false; + }, + ); + } +} + +abstract class _VoidCallbackStubBase { + void onCall(); +} + +class _VoidCallbackStub extends Mock implements _VoidCallbackStubBase {} + +void main() { + group('PinballDpadButton', () { + testWidgets('can be tapped', (tester) async { + final stub = _VoidCallbackStub(); + await tester.pumpButton( + direction: PinballDpadDirection.up, + onTap: stub.onCall, + ); + + await tester.tap(find.byType(Image)); + + verify(stub.onCall).called(1); + }); + + group('up', () { + testWidgets('renders the correct image', (tester) async { + await tester.pumpButton( + direction: PinballDpadDirection.up, + onTap: () {}, + ); + + expect( + find.byImagePath(Assets.images.button.dpadUp.keyName), + findsOneWidget, + ); + }); + }); + + group('down', () { + testWidgets('renders the correct image', (tester) async { + await tester.pumpButton( + direction: PinballDpadDirection.down, + onTap: () {}, + ); + + expect( + find.byImagePath(Assets.images.button.dpadDown.keyName), + findsOneWidget, + ); + }); + }); + + group('left', () { + testWidgets('renders the correct image', (tester) async { + await tester.pumpButton( + direction: PinballDpadDirection.left, + onTap: () {}, + ); + + expect( + find.byImagePath(Assets.images.button.dpadLeft.keyName), + findsOneWidget, + ); + }); + }); + + group('right', () { + testWidgets('renders the correct image', (tester) async { + await tester.pumpButton( + direction: PinballDpadDirection.right, + onTap: () {}, + ); + + expect( + find.byImagePath(Assets.images.button.dpadRight.keyName), + findsOneWidget, + ); + }); + }); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 84554502..1b71a25f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,7 @@ flutter: - assets/images/bonus_animation/ - assets/images/score/ - assets/images/link_box/ + - assets/images/loading_game/ flutter_gen: line_length: 80 diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 4f04a89d..0f34b66e 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -35,6 +35,7 @@ void main() { pinballPlayer: pinballPlayer, ), ); + await tester.pump(const Duration(milliseconds: 400)); expect(find.byType(PinballGamePage), findsOneWidget); }); }); diff --git a/test/assets_manager/cubit/assets_manager_cubit_test.dart b/test/assets_manager/cubit/assets_manager_cubit_test.dart deleted file mode 100644 index 27d9cedb..00000000 --- a/test/assets_manager/cubit/assets_manager_cubit_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:async'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/assets_manager/assets_manager.dart'; - -void main() { - group('AssetsManagerCubit', () { - final completer1 = Completer(); - final completer2 = Completer(); - - final future1 = completer1.future; - final future2 = completer2.future; - - blocTest( - 'emits the loaded on the order that they load', - build: () => AssetsManagerCubit([future1, future2]), - act: (cubit) { - cubit.load(); - completer2.complete(); - completer1.complete(); - }, - expect: () => [ - AssetsManagerState( - loadables: [future1, future2], - loaded: [future2], - ), - AssetsManagerState( - loadables: [future1, future2], - loaded: [future2, future1], - ), - ], - ); - }); -} diff --git a/test/assets_manager/cubit/assets_manager_state_test.dart b/test/assets_manager/cubit/assets_manager_state_test.dart index 4882f880..41e94add 100644 --- a/test/assets_manager/cubit/assets_manager_state_test.dart +++ b/test/assets_manager/cubit/assets_manager_state_test.dart @@ -13,12 +13,11 @@ void main() { }); test('has the correct initial state', () { - final future = Future.value(); expect( - AssetsManagerState.initial(loadables: [future]), + AssetsManagerState.initial(), equals( AssetsManagerState( - loadables: [future], + loadables: const [], loaded: const [], ), ), diff --git a/test/game/behaviors/bonus_ball_spawning_behavior_test.dart b/test/game/behaviors/bonus_ball_spawning_behavior_test.dart new file mode 100644 index 00000000..1aacf506 --- /dev/null +++ b/test/game/behaviors/bonus_ball_spawning_behavior_test.dart @@ -0,0 +1,61 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:flame_forge2d/forge2d_game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + images.prefix = ''; + await images.loadAll([ + theme.Assets.images.dash.ball.keyName, + ]); + } + + Future pump(BonusBallSpawningBehavior child) async { + await ensureAdd( + FlameBlocProvider.value( + value: CharacterThemeCubit(), + children: [ + ZCanvasComponent( + children: [child], + ), + ], + ), + ); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('FlutterForestBonusBehavior', () { + final flameTester = FlameTester(_TestGame.new); + + flameTester.test( + 'adds a ball with a BallImpulsingBehavior to the game onTick ' + 'resulting in a -40 x impulse', + (game) async { + await game.onLoad(); + final behavior = BonusBallSpawningBehavior(); + + await game.pump(behavior); + + game.update(behavior.timer.limit); + await game.ready(); + + final ball = game.descendants().whereType().single; + + expect(ball.body.linearVelocity.x, equals(-40)); + expect(ball.body.linearVelocity.y, equals(0)); + }, + ); + }); +} diff --git a/test/game/behaviors/bonus_noise_behavior_test.dart b/test/game/behaviors/bonus_noise_behavior_test.dart index 12f62545..fada7d1e 100644 --- a/test/game/behaviors/bonus_noise_behavior_test.dart +++ b/test/game/behaviors/bonus_noise_behavior_test.dart @@ -126,7 +126,7 @@ void main() { await game.pump(behavior, player: player, bloc: bloc); }, verify: (_, __) async { - verifyNever(() => player.play(any())); + verify(() => player.play(PinballAudio.dino)).called(1); }, ); @@ -151,7 +151,7 @@ void main() { await game.pump(behavior, player: player, bloc: bloc); }, verify: (_, __) async { - verifyNever(() => player.play(any())); + verify(() => player.play(PinballAudio.android)).called(1); }, ); @@ -176,7 +176,7 @@ void main() { await game.pump(behavior, player: player, bloc: bloc); }, verify: (_, __) async { - verifyNever(() => player.play(any())); + verify(() => player.play(PinballAudio.dash)).called(1); }, ); }); diff --git a/test/game/components/android_acres/android_acres_test.dart b/test/game/components/android_acres/android_acres_test.dart index e88d1608..5bf22da9 100644 --- a/test/game/components/android_acres/android_acres_test.dart +++ b/test/game/components/android_acres/android_acres_test.dart @@ -131,11 +131,28 @@ void main() { ); }); + flameTester.test('adds a FlameBlocProvider', (game) async { + final androidAcres = AndroidAcres(); + await game.pump(androidAcres); + expect( + androidAcres.children + .whereType< + FlameBlocProvider>() + .single, + isNotNull, + ); + }); + flameTester.test('adds an AndroidSpaceshipBonusBehavior', (game) async { final androidAcres = AndroidAcres(); await game.pump(androidAcres); + final provider = androidAcres.children + .whereType< + FlameBlocProvider>() + .single; expect( - androidAcres.children.whereType().single, + provider.children.whereType().single, isNotNull, ); }); diff --git a/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart b/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart index e6b03c5f..e8ef68f8 100644 --- a/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart +++ b/test/game/components/android_acres/behaviors/android_spaceship_bonus_behavior_test.dart @@ -1,5 +1,8 @@ // ignore_for_file: cascade_invocations +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; @@ -41,13 +44,21 @@ class _TestGame extends Forge2DGame { Future pump( AndroidAcres child, { required GameBloc gameBloc, + required AndroidSpaceshipCubit androidSpaceshipCubit, }) async { // Not needed once https://github.com/flame-engine/flame/issues/1607 // is fixed await onLoad(); await ensureAdd( - FlameBlocProvider.value( - value: gameBloc, + FlameMultiBlocProvider( + providers: [ + FlameBlocProvider.value( + value: gameBloc, + ), + FlameBlocProvider.value( + value: androidSpaceshipCubit, + ), + ], children: [child], ), ); @@ -56,6 +67,9 @@ class _TestGame extends Forge2DGame { class _MockGameBloc extends Mock implements GameBloc {} +class _MockAndroidSpaceshipCubit extends Mock implements AndroidSpaceshipCubit { +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -70,20 +84,30 @@ void main() { flameTester.testGameWidget( 'adds GameBonus.androidSpaceship to the game ' - 'when android spacehship has a bonus', + 'when android spaceship has a bonus', setUp: (game, tester) async { final behavior = AndroidSpaceshipBonusBehavior(); final parent = AndroidAcres.test(); final androidSpaceship = AndroidSpaceship(position: Vector2.zero()); + final androidSpaceshipCubit = _MockAndroidSpaceshipCubit(); + final streamController = StreamController(); + + whenListen( + androidSpaceshipCubit, + streamController.stream, + initialState: AndroidSpaceshipState.withoutBonus, + ); await parent.add(androidSpaceship); await game.pump( parent, + androidSpaceshipCubit: androidSpaceshipCubit, gameBloc: gameBloc, ); await parent.ensureAdd(behavior); - androidSpaceship.bloc.onBallEntered(); + streamController.add(AndroidSpaceshipState.withBonus); + await tester.pump(); verify( diff --git a/test/game/components/backbox/backbox_test.dart b/test/game/components/backbox/backbox_test.dart index 100a54e0..773b163f 100644 --- a/test/game/components/backbox/backbox_test.dart +++ b/test/game/components/backbox/backbox_test.dart @@ -20,6 +20,7 @@ import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; +import 'package:platform_helper/platform_helper.dart'; class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents, HasTappables { @@ -67,6 +68,8 @@ RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) { return event; } +class _MockPlatformHelper extends Mock implements PlatformHelper {} + class _MockBackboxBloc extends Mock implements BackboxBloc {} class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { @@ -130,36 +133,32 @@ void main() { final flameTester = FlameTester(_TestGame.new); late BackboxBloc bloc; + late PlatformHelper platformHelper; setUp(() { bloc = _MockBackboxBloc(); + platformHelper = _MockPlatformHelper(); whenListen( bloc, Stream.empty(), initialState: LoadingState(), ); + when(() => platformHelper.isMobile).thenReturn(false); }); group('Backbox', () { flameTester.test( 'loads correctly', (game) async { - final backbox = Backbox.test(bloc: bloc); + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); await game.pump(backbox); expect(game.descendants(), contains(backbox)); }, ); - flameTester.test( - 'adds LeaderboardRequested when loaded', - (game) async { - final backbox = Backbox.test(bloc: bloc); - await game.pump(backbox); - - verify(() => bloc.add(LeaderboardRequested())).called(1); - }, - ); - flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { @@ -168,7 +167,10 @@ void main() { ..followVector2(Vector2(0, -130)) ..zoom = 6; await game.pump( - Backbox.test(bloc: bloc), + Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ), ); await tester.pump(); }, @@ -186,7 +188,9 @@ void main() { final backbox = Backbox.test( bloc: BackboxBloc( leaderboardRepository: _MockLeaderboardRepository(), + initialEntries: [LeaderboardEntryData.empty], ), + platformHelper: platformHelper, ); await game.pump(backbox); backbox.requestInitials( @@ -215,7 +219,10 @@ void main() { Stream.empty(), initialState: state, ); - final backbox = Backbox.test(bloc: bloc); + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); await game.pump(backbox); game.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.enter), {}); @@ -232,7 +239,7 @@ void main() { ); flameTester.test( - 'added GameOverInfoDisplay on InitialsSuccessState', + 'adds GameOverInfoDisplay when InitialsSuccessState', (game) async { final state = InitialsSuccessState(score: 100); whenListen( @@ -240,7 +247,60 @@ void main() { const Stream.empty(), initialState: state, ); - final backbox = Backbox.test(bloc: bloc); + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); + await game.pump(backbox); + + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'adds the mobile controls overlay when platform is mobile', + (game) async { + final bloc = _MockBackboxBloc(); + final platformHelper = _MockPlatformHelper(); + final state = InitialsFormState( + score: 10, + character: game.character, + ); + whenListen( + bloc, + Stream.empty(), + initialState: state, + ); + when(() => platformHelper.isMobile).thenReturn(true); + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); + await game.pump(backbox); + + expect( + game.overlays.value, + contains(PinballGame.mobileControlsOverlay), + ); + }, + ); + + flameTester.test( + 'adds InitialsSubmissionSuccessDisplay on InitialsSuccessState', + (game) async { + final state = InitialsSuccessState(score: 100); + whenListen( + bloc, + const Stream.empty(), + initialState: state, + ); + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); await game.pump(backbox); expect( @@ -259,7 +319,10 @@ void main() { Stream.value(state), initialState: state, ); - final backbox = Backbox.test(bloc: bloc); + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); await game.pump(backbox); final shareLink = @@ -282,7 +345,10 @@ void main() { Stream.empty(), initialState: InitialsFailureState(), ); - final backbox = Backbox.test(bloc: bloc); + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); await game.pump(backbox); expect( @@ -304,7 +370,10 @@ void main() { initialState: LeaderboardSuccessState(entries: const []), ); - final backbox = Backbox.test(bloc: bloc); + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); await game.pump(backbox); expect( @@ -324,7 +393,10 @@ void main() { initialState: LoadingState(), ); - final backbox = Backbox.test(bloc: bloc); + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); await game.pump(backbox); backbox.removeFromParent(); diff --git a/test/game/components/backbox/bloc/backbox_bloc_test.dart b/test/game/components/backbox/bloc/backbox_bloc_test.dart index 8b50b289..9d36714b 100644 --- a/test/game/components/backbox/bloc/backbox_bloc_test.dart +++ b/test/game/components/backbox/bloc/backbox_bloc_test.dart @@ -12,14 +12,37 @@ class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { void main() { late LeaderboardRepository leaderboardRepository; + const emptyEntries = []; + const filledEntries = [LeaderboardEntryData.empty]; group('BackboxBloc', () { + test('inits state with LeaderboardSuccessState when has entries', () { + leaderboardRepository = _MockLeaderboardRepository(); + final bloc = BackboxBloc( + leaderboardRepository: leaderboardRepository, + initialEntries: filledEntries, + ); + expect(bloc.state, isA()); + }); + + test('inits state with LeaderboardFailureState when has no entries', () { + leaderboardRepository = _MockLeaderboardRepository(); + final bloc = BackboxBloc( + leaderboardRepository: leaderboardRepository, + initialEntries: null, + ); + expect(bloc.state, isA()); + }); + blocTest( 'adds InitialsFormState on PlayerInitialsRequested', setUp: () { leaderboardRepository = _MockLeaderboardRepository(); }, - build: () => BackboxBloc(leaderboardRepository: leaderboardRepository), + build: () => BackboxBloc( + leaderboardRepository: leaderboardRepository, + initialEntries: emptyEntries, + ), act: (bloc) => bloc.add( PlayerInitialsRequested( score: 100, @@ -46,7 +69,10 @@ void main() { ), ).thenAnswer((_) async {}); }, - build: () => BackboxBloc(leaderboardRepository: leaderboardRepository), + build: () => BackboxBloc( + leaderboardRepository: leaderboardRepository, + initialEntries: emptyEntries, + ), act: (bloc) => bloc.add( PlayerInitialsSubmitted( score: 10, @@ -74,7 +100,10 @@ void main() { ), ).thenThrow(Exception('Error')); }, - build: () => BackboxBloc(leaderboardRepository: leaderboardRepository), + build: () => BackboxBloc( + leaderboardRepository: leaderboardRepository, + initialEntries: emptyEntries, + ), act: (bloc) => bloc.add( PlayerInitialsSubmitted( score: 10, @@ -95,7 +124,10 @@ void main() { setUp: () { leaderboardRepository = _MockLeaderboardRepository(); }, - build: () => BackboxBloc(leaderboardRepository: leaderboardRepository), + build: () => BackboxBloc( + leaderboardRepository: leaderboardRepository, + initialEntries: emptyEntries, + ), act: (bloc) => bloc.add( ShareScoreRequested(score: 100), ), @@ -116,7 +148,10 @@ void main() { (_) async => [LeaderboardEntryData.empty], ); }, - build: () => BackboxBloc(leaderboardRepository: leaderboardRepository), + build: () => BackboxBloc( + leaderboardRepository: leaderboardRepository, + initialEntries: emptyEntries, + ), act: (bloc) => bloc.add(LeaderboardRequested()), expect: () => [ LoadingState(), @@ -132,7 +167,10 @@ void main() { () => leaderboardRepository.fetchTop10Leaderboard(), ).thenThrow(Exception('Error')); }, - build: () => BackboxBloc(leaderboardRepository: leaderboardRepository), + build: () => BackboxBloc( + leaderboardRepository: leaderboardRepository, + initialEntries: emptyEntries, + ), act: (bloc) => bloc.add(LeaderboardRequested()), expect: () => [ LoadingState(), diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart deleted file mode 100644 index 00a69f9e..00000000 --- a/test/game/components/controlled_flipper_test.dart +++ /dev/null @@ -1,260 +0,0 @@ -import 'dart:collection'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/input.dart'; -import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter/services.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'; - -class _TestGame extends Forge2DGame with HasKeyboardHandlerComponents { - @override - Future onLoad() async { - images.prefix = ''; - await images.loadAll([ - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - ]); - } - - Future pump(Flipper flipper, {required GameBloc gameBloc}) { - return ensureAdd( - FlameBlocProvider.value( - value: gameBloc, - children: [flipper], - ), - ); - } -} - -class _MockGameBloc extends Mock implements GameBloc {} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(_TestGame.new); - - group('FlipperController', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = _MockGameBloc(); - }); - - group('onKeyEvent', () { - final leftKeys = UnmodifiableListView([ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.keyA, - ]); - final rightKeys = UnmodifiableListView([ - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.keyD, - ]); - - group('and Flipper is left', () { - late Flipper flipper; - late FlipperController controller; - - setUp(() { - flipper = Flipper(side: BoardSide.left); - controller = FlipperController(flipper); - flipper.add(controller); - }); - - testRawKeyDownEvents(leftKeys, (event) { - flameTester.test( - 'moves upwards ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isNegative); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyDownEvents(leftKeys, (event) { - flameTester.test( - 'does nothing when is game over', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.gameOver, - ), - ); - - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(leftKeys, (event) { - flameTester.test( - 'moves downwards ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isPositive); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(rightKeys, (event) { - flameTester.test( - 'does nothing ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - }); - - group('and Flipper is right', () { - late Flipper flipper; - late FlipperController controller; - - setUp(() { - flipper = Flipper(side: BoardSide.right); - controller = FlipperController(flipper); - flipper.add(controller); - }); - - testRawKeyDownEvents(rightKeys, (event) { - flameTester.test( - 'moves upwards ' - 'when ${event.logicalKey.keyLabel} is pressed', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isNegative); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(rightKeys, (event) { - flameTester.test( - 'moves downwards ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isPositive); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyDownEvents(rightKeys, (event) { - flameTester.test( - 'does nothing when is game over', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.gameOver, - ), - ); - - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - - testRawKeyUpEvents(leftKeys, (event) { - flameTester.test( - 'does nothing ' - 'when ${event.logicalKey.keyLabel} is released', - (game) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial().copyWith( - status: GameStatus.playing, - ), - ); - - await game.ready(); - await game.pump(flipper, gameBloc: gameBloc); - controller.onKeyEvent(event, {}); - - expect(flipper.body.linearVelocity.y, isZero); - expect(flipper.body.linearVelocity.x, isZero); - }, - ); - }); - }); - }); - }); -} 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 index 0d058c70..7fc1946b 100644 --- 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 @@ -5,9 +5,9 @@ import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/components/flutter_forest/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_theme/pinball_theme.dart' as theme; @@ -27,13 +27,8 @@ class _TestGame extends Forge2DGame { required GameBloc gameBloc, }) async { await ensureAdd( - FlameMultiBlocProvider( - providers: [ - FlameBlocProvider.value(value: gameBloc), - FlameBlocProvider.value( - value: CharacterThemeCubit(), - ), - ], + FlameBlocProvider.value( + value: gameBloc, children: [ ZCanvasComponent( children: [child], @@ -58,8 +53,7 @@ void main() { final flameTester = FlameTester(_TestGame.new); - void _contactedBumper(DashNestBumper bumper) => - bumper.bloc.onBallContacted(); + void _contactedBumper(DashBumper bumper) => bumper.bloc.onBallContacted(); flameTester.testGameWidget( 'adds GameBonus.dashNest to the game ' @@ -69,9 +63,9 @@ void main() { final behavior = FlutterForestBonusBehavior(); final parent = FlutterForest.test(); final bumpers = [ - DashNestBumper.test(bloc: DashNestBumperCubit()), - DashNestBumper.test(bloc: DashNestBumperCubit()), - DashNestBumper.test(bloc: DashNestBumperCubit()), + DashBumper.test(bloc: DashBumperCubit()), + DashBumper.test(bloc: DashBumperCubit()), + DashBumper.test(bloc: DashBumperCubit()), ]; final animatronic = DashAnimatronic(); final signpost = Signpost.test(bloc: SignpostCubit()); @@ -79,7 +73,7 @@ void main() { await parent.ensureAddAll([...bumpers, animatronic, signpost]); await parent.ensureAdd(behavior); - expect(game.descendants().whereType(), equals(bumpers)); + expect(game.descendants().whereType(), equals(bumpers)); bumpers.forEach(_contactedBumper); await tester.pump(); bumpers.forEach(_contactedBumper); @@ -94,16 +88,16 @@ void main() { ); flameTester.testGameWidget( - 'adds a new Ball to the game ' + 'adds BonusBallSpawningBehavior to the game ' 'when bumpers are activated three times', setUp: (game, tester) async { await game.onLoad(); final behavior = FlutterForestBonusBehavior(); final parent = FlutterForest.test(); final bumpers = [ - DashNestBumper.test(bloc: DashNestBumperCubit()), - DashNestBumper.test(bloc: DashNestBumperCubit()), - DashNestBumper.test(bloc: DashNestBumperCubit()), + DashBumper.test(bloc: DashBumperCubit()), + DashBumper.test(bloc: DashBumperCubit()), + DashBumper.test(bloc: DashBumperCubit()), ]; final animatronic = DashAnimatronic(); final signpost = Signpost.test(bloc: SignpostCubit()); @@ -111,7 +105,7 @@ void main() { await parent.ensureAddAll([...bumpers, animatronic, signpost]); await parent.ensureAdd(behavior); - expect(game.descendants().whereType(), equals(bumpers)); + expect(game.descendants().whereType(), equals(bumpers)); bumpers.forEach(_contactedBumper); await tester.pump(); bumpers.forEach(_contactedBumper); @@ -121,7 +115,7 @@ void main() { await game.ready(); expect( - game.descendants().whereType().length, + game.descendants().whereType().length, equals(1), ); }, @@ -135,9 +129,9 @@ void main() { final behavior = FlutterForestBonusBehavior(); final parent = FlutterForest.test(); final bumpers = [ - DashNestBumper.test(bloc: DashNestBumperCubit()), - DashNestBumper.test(bloc: DashNestBumperCubit()), - DashNestBumper.test(bloc: DashNestBumperCubit()), + DashBumper.test(bloc: DashBumperCubit()), + DashBumper.test(bloc: DashBumperCubit()), + DashBumper.test(bloc: DashBumperCubit()), ]; final animatronic = DashAnimatronic(); final signpost = Signpost.test(bloc: SignpostCubit()); @@ -145,7 +139,7 @@ void main() { await parent.ensureAddAll([...bumpers, animatronic, signpost]); await parent.ensureAdd(behavior); - expect(game.descendants().whereType(), equals(bumpers)); + expect(game.descendants().whereType(), equals(bumpers)); bumpers.forEach(_contactedBumper); await tester.pump(); diff --git a/test/game/components/flutter_forest/flutter_forest_test.dart b/test/game/components/flutter_forest/flutter_forest_test.dart index 470719d8..bce4b6ac 100644 --- a/test/game/components/flutter_forest/flutter_forest_test.dart +++ b/test/game/components/flutter_forest/flutter_forest_test.dart @@ -91,23 +91,23 @@ void main() { ); flameTester.test( - 'three DashNestBumper', + 'three DashBumper', (game) async { final component = FlutterForest(); await game.pump(component); expect( - game.descendants().whereType().length, + game.descendants().whereType().length, equals(3), ); }, ); flameTester.test( - 'three DashNestBumpers with BumperNoiseBehavior', + 'three DashBumpers with BumperNoiseBehavior', (game) async { final component = FlutterForest(); await game.pump(component); - final bumpers = game.descendants().whereType(); + final bumpers = game.descendants().whereType(); for (final bumper in bumpers) { expect( bumper.firstChild(), diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart index a371b276..e8685c3e 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -8,16 +8,24 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' as theme; class _TestGame extends Forge2DGame { @override Future onLoad() async { images.prefix = ''; - await images.load(Assets.images.backbox.marquee.keyName); + await images.loadAll( + [ + const theme.DashTheme().leaderboardIcon.keyName, + Assets.images.backbox.marquee.keyName, + Assets.images.backbox.displayDivider.keyName, + ], + ); } Future pump( @@ -35,8 +43,15 @@ class _TestGame extends Forge2DGame { ), ], children: [ - FlameProvider.value( - pinballPlayer ?? _MockPinballPlayer(), + MultiFlameProvider( + providers: [ + FlameProvider.value( + pinballPlayer ?? _MockPinballPlayer(), + ), + FlameProvider.value( + _MockAppLocalizations(), + ), + ], children: children, ), ], @@ -50,6 +65,35 @@ class _MockPinballPlayer extends Mock implements PinballPlayer {} class _MockLeaderboardRepository extends Mock implements LeaderboardRepository { } +class _MockAppLocalizations extends Mock implements AppLocalizations { + @override + String get score => ''; + + @override + String get name => ''; + + @override + String get rank => ''; + + @override + String get enterInitials => ''; + + @override + String get arrows => ''; + + @override + String get andPress => ''; + + @override + String get enterReturn => ''; + + @override + String get toSubmit => ''; + + @override + String get loading => ''; +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -92,90 +136,125 @@ void main() { }); group('onNewState', () { - flameTester.test( - 'changes the backbox display when the game is over', - (game) async { - final component = GameBlocStatusListener(); - final repository = _MockLeaderboardRepository(); - final backbox = Backbox(leaderboardRepository: repository); - final state = const GameState.initial() - ..copyWith( - status: GameStatus.gameOver, + group('on game over', () { + late GameState state; + + setUp(() { + state = const GameState.initial().copyWith( + status: GameStatus.gameOver, + ); + }); + + flameTester.test( + 'changes the backbox display', + (game) async { + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox( + leaderboardRepository: repository, + entries: const [], ); - await game.pump([component, backbox]); + await game.pump([component, backbox]); - expect(() => component.onNewState(state), returnsNormally); - }, - ); + expect(() => component.onNewState(state), returnsNormally); + }, + ); - flameTester.test( - 'plays the background music on start', - (game) async { - final player = _MockPinballPlayer(); - final component = GameBlocStatusListener(); - await game.pump([component], pinballPlayer: player); + flameTester.test( + 'removes FlipperKeyControllingBehavior from Fipper', + (game) async { + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox( + leaderboardRepository: repository, + entries: const [], + ); + final flipper = Flipper.test(side: BoardSide.left); + final behavior = FlipperKeyControllingBehavior(); - component.onNewState( - const GameState.initial().copyWith(status: GameStatus.playing), - ); + await game.pump([component, backbox, flipper]); + await flipper.ensureAdd(behavior); - verify(() => player.play(PinballAudio.backgroundMusic)).called(1); - }, - ); + expect(state.status, GameStatus.gameOver); - flameTester.test( - 'plays the game over voice over when it is game over', - (game) async { - final player = _MockPinballPlayer(); - final component = GameBlocStatusListener(); - final repository = _MockLeaderboardRepository(); - final backbox = Backbox(leaderboardRepository: repository); - await game.pump([component, backbox], pinballPlayer: player); - - component.onNewState( - const GameState.initial().copyWith(status: GameStatus.gameOver), - ); + component.onNewState(state); + await game.ready(); - verify(() => player.play(PinballAudio.gameOverVoiceOver)).called(1); - }, - ); + expect( + flipper.children.whereType(), + isEmpty, + ); + }, + ); + + flameTester.test( + 'plays the game over voice over', + (game) async { + final player = _MockPinballPlayer(); + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox( + leaderboardRepository: repository, + entries: const [], + ); + await game.pump([component, backbox], pinballPlayer: player); + + component.onNewState(state); - flameTester.test( - 'removes Play button on start', - (game) async { - final player = _MockPinballPlayer(); - final component = GameBlocStatusListener(); + verify(() => player.play(PinballAudio.gameOverVoiceOver)).called(1); + }, + ); + }); - await game.pump([component], pinballPlayer: player); + group('on playing', () { + late GameState state; - component.onNewState( - const GameState.initial().copyWith(status: GameStatus.playing), + setUp(() { + state = const GameState.initial().copyWith( + status: GameStatus.playing, ); - await game.ready(); + }); - expect(game.overlays.isActive(PinballGame.playButtonOverlay), false); - }, - ); + flameTester.test( + 'plays the background music on start', + (game) async { + final player = _MockPinballPlayer(); + final component = GameBlocStatusListener(); + await game.pump([component], pinballPlayer: player); - flameTester.test( - 'removes Replay button on replay', - (game) async { - final player = _MockPinballPlayer(); - final component = GameBlocStatusListener(); + expect(state.status, equals(GameStatus.playing)); + component.onNewState(state); - await game.pump([component], pinballPlayer: player); + verify(() => player.play(PinballAudio.backgroundMusic)).called(1); + }, + ); - component.onNewState( - const GameState.initial().copyWith(status: GameStatus.playing), - ); + flameTester.test( + 'adds key controlling behavior to Fippers when the game is started', + (game) async { + final component = GameBlocStatusListener(); + final repository = _MockLeaderboardRepository(); + final backbox = Backbox( + leaderboardRepository: repository, + entries: const [], + ); + final flipper = Flipper.test(side: BoardSide.left); - expect( - game.overlays.isActive(PinballGame.replayButtonOverlay), - false, - ); - }, - ); + await game.pump([component, backbox, flipper]); + + component.onNewState(state); + await game.ready(); + + expect( + flipper.children + .whereType() + .length, + equals(1), + ); + }, + ); + }); }); }); } diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 0e23e54d..8c6d3042 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -32,7 +32,10 @@ class _TestPinballGame extends PinballGame { @override Future onLoad() async { images.prefix = ''; - final futures = preLoadAssets(); + final futures = [ + ...preLoadAssets(), + preFetchLeaderboard(), + ]; await Future.wait(futures); return super.onLoad(); @@ -79,14 +82,26 @@ void main() { ); }); - testWidgets('renders PinballGameView', (tester) async { - await tester.pumpApp( - PinballGamePage(), - characterThemeCubit: characterThemeCubit, - gameBloc: gameBloc, - ); + group('renders PinballGameView', () { + testWidgets('with debug mode turned on', (tester) async { + await tester.pumpApp( + PinballGamePage(), + characterThemeCubit: characterThemeCubit, + gameBloc: gameBloc, + ); + + expect(find.byType(PinballGameView), findsOneWidget); + }); + + testWidgets('with debug mode turned off', (tester) async { + await tester.pumpApp( + PinballGamePage(isDebugMode: false), + characterThemeCubit: characterThemeCubit, + gameBloc: gameBloc, + ); - expect(find.byType(PinballGameView), findsOneWidget); + expect(find.byType(PinballGameView), findsOneWidget); + }); }); testWidgets( @@ -103,9 +118,7 @@ void main() { initialState: initialAssetsState, ); await tester.pumpApp( - PinballGameView( - game: game, - ), + PinballGameView(game), assetsManagerCubit: assetsManagerCubit, characterThemeCubit: characterThemeCubit, ); @@ -135,9 +148,7 @@ void main() { ); await tester.pumpApp( - PinballGameView( - game: game, - ), + PinballGameView(game), assetsManagerCubit: assetsManagerCubit, characterThemeCubit: characterThemeCubit, gameBloc: gameBloc, @@ -148,61 +159,6 @@ void main() { expect(find.byType(PinballGameLoadedView), findsOneWidget); }); - - group('route', () { - Future pumpRoute({ - required WidgetTester tester, - required bool isDebugMode, - }) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context).push( - PinballGamePage.route( - isDebugMode: isDebugMode, - ), - ); - }, - child: const Text('Tap me'), - ); - }, - ), - ), - characterThemeCubit: characterThemeCubit, - gameBloc: gameBloc, - ); - - await tester.tap(find.text('Tap me')); - - // We can't use pumpAndSettle here because the page renders a Flame game - // which is an infinity animation, so it will timeout - await tester.pump(); // Runs the button action - await tester.pump(); // Runs the navigation - } - - testWidgets('route creates the correct non debug game', (tester) async { - await pumpRoute(tester: tester, isDebugMode: false); - expect( - find.byWidgetPredicate( - (w) => w is PinballGameView && w.game is! DebugPinballGame, - ), - findsOneWidget, - ); - }); - - testWidgets('route creates the correct debug game', (tester) async { - await pumpRoute(tester: tester, isDebugMode: true); - expect( - find.byWidgetPredicate( - (w) => w is PinballGameView && w.game is DebugPinballGame, - ), - findsOneWidget, - ); - }); - }); }); group('PinballGameView', () { @@ -227,7 +183,7 @@ void main() { testWidgets('renders game', (tester) async { await tester.pumpApp( - PinballGameView(game: game), + PinballGameView(game), gameBloc: gameBloc, startGameBloc: startGameBloc, ); @@ -255,7 +211,7 @@ void main() { ); await tester.pumpApp( - PinballGameView(game: game), + PinballGameView(game), gameBloc: gameBloc, startGameBloc: startGameBloc, ); @@ -273,7 +229,6 @@ void main() { final gameState = GameState.initial().copyWith( status: GameStatus.gameOver, ); - whenListen( startGameBloc, Stream.value(startGameState), @@ -284,17 +239,12 @@ void main() { Stream.value(gameState), initialState: gameState, ); - await tester.pumpApp( - PinballGameView(game: game), + Material(child: PinballGameView(game)), gameBloc: gameBloc, startGameBloc: startGameBloc, ); - - expect( - find.byType(GameHud), - findsNothing, - ); + expect(find.byType(GameHud), findsNothing); }); testWidgets('keep focus on game when mouse hovers over it', (tester) async { @@ -304,7 +254,6 @@ void main() { final gameState = GameState.initial().copyWith( status: GameStatus.gameOver, ); - whenListen( startGameBloc, Stream.value(startGameState), @@ -316,47 +265,51 @@ void main() { initialState: gameState, ); await tester.pumpApp( - PinballGameView(game: game), + Material(child: PinballGameView(game)), gameBloc: gameBloc, startGameBloc: startGameBloc, ); - game.focusNode.unfocus(); await tester.pump(); - expect(game.focusNode.hasFocus, isFalse); - final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); addTearDown(gesture.removePointer); await gesture.moveTo((game.size / 2).toOffset()); await tester.pump(); - expect(game.focusNode.hasFocus, isTrue); }); + testWidgets('mobile controls when the overlay is added', (tester) async { + await tester.pumpApp( + PinballGameView(game), + gameBloc: gameBloc, + startGameBloc: startGameBloc, + ); + + game.overlays.add(PinballGame.mobileControlsOverlay); + + await tester.pump(); + + expect(find.byType(MobileControls), findsOneWidget); + }); + group('info icon', () { testWidgets('renders on game over', (tester) async { final gameState = GameState.initial().copyWith( status: GameStatus.gameOver, ); - whenListen( gameBloc, Stream.value(gameState), initialState: gameState, ); - await tester.pumpApp( - PinballGameView(game: game), + Material(child: PinballGameView(game)), gameBloc: gameBloc, startGameBloc: startGameBloc, ); - - expect( - find.image(Assets.images.linkBox.infoIcon), - findsOneWidget, - ); + expect(find.image(Assets.images.linkBox.infoIcon), findsOneWidget); }); testWidgets('opens MoreInformationDialog when tapped', (tester) async { @@ -369,16 +322,13 @@ void main() { initialState: gameState, ); await tester.pumpApp( - PinballGameView(game: game), + Material(child: PinballGameView(game)), gameBloc: gameBloc, startGameBloc: startGameBloc, ); await tester.tap(find.byType(IconButton)); await tester.pump(); - expect( - find.byType(MoreInformationDialog), - findsOneWidget, - ); + expect(find.byType(MoreInformationDialog), findsOneWidget); }); }); }); diff --git a/test/game/view/widgets/mobile_controls_test.dart b/test/game/view/widgets/mobile_controls_test.dart new file mode 100644 index 00000000..ab9c0b76 --- /dev/null +++ b/test/game/view/widgets/mobile_controls_test.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.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_flame/pinball_flame.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +class _MockPinballGame extends Mock implements PinballGame {} + +extension _WidgetTesterX on WidgetTester { + Future pumpMobileControls(PinballGame game) async { + await pumpWidget( + MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + home: Scaffold( + body: MobileControls(game: game), + ), + ), + ); + } +} + +extension _CommonFindersX on CommonFinders { + Finder byPinballDpadDirection(PinballDpadDirection direction) { + return byWidgetPredicate((widget) { + return widget is PinballDpadButton && widget.direction == direction; + }); + } +} + +void main() { + group('MobileControls', () { + testWidgets('renders', (tester) async { + await tester.pumpMobileControls(_MockPinballGame()); + + expect(find.byType(PinballButton), findsOneWidget); + expect(find.byType(MobileDpad), findsOneWidget); + }); + + testWidgets('correctly triggers the arrow up', (tester) async { + var pressed = false; + final component = KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowUp: () => pressed = true, + }, + ); + final game = _MockPinballGame(); + when(game.descendants).thenReturn([component]); + + await tester.pumpMobileControls(game); + await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.up)); + await tester.pump(); + + expect(pressed, isTrue); + }); + + testWidgets('correctly triggers the arrow down', (tester) async { + var pressed = false; + final component = KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowDown: () => pressed = true, + }, + ); + final game = _MockPinballGame(); + when(game.descendants).thenReturn([component]); + + await tester.pumpMobileControls(game); + await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.down)); + await tester.pump(); + + expect(pressed, isTrue); + }); + + testWidgets('correctly triggers the arrow right', (tester) async { + var pressed = false; + final component = KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowRight: () => pressed = true, + }, + ); + final game = _MockPinballGame(); + when(game.descendants).thenReturn([component]); + + await tester.pumpMobileControls(game); + await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.right)); + await tester.pump(); + + expect(pressed, isTrue); + }); + + testWidgets('correctly triggers the arrow left', (tester) async { + var pressed = false; + final component = KeyboardInputController( + keyUp: { + LogicalKeyboardKey.arrowLeft: () => pressed = true, + }, + ); + final game = _MockPinballGame(); + when(game.descendants).thenReturn([component]); + + await tester.pumpMobileControls(game); + await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.left)); + await tester.pump(); + + expect(pressed, isTrue); + }); + + testWidgets('correctly triggers the enter', (tester) async { + var pressed = false; + final component = KeyboardInputController( + keyUp: { + LogicalKeyboardKey.enter: () => pressed = true, + }, + ); + final game = _MockPinballGame(); + when(game.descendants).thenReturn([component]); + + await tester.pumpMobileControls(game); + await tester.tap(find.byType(PinballButton)); + await tester.pump(); + + expect(pressed, isTrue); + }); + }); +} diff --git a/test/game/view/widgets/mobile_dpad_test.dart b/test/game/view/widgets/mobile_dpad_test.dart new file mode 100644 index 00000000..2a8d0b02 --- /dev/null +++ b/test/game/view/widgets/mobile_dpad_test.dart @@ -0,0 +1,113 @@ +// ignore_for_file: one_member_abstracts + +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_ui/pinball_ui.dart'; + +extension _WidgetTesterX on WidgetTester { + Future pumpDpad({ + required VoidCallback onTapUp, + required VoidCallback onTapDown, + required VoidCallback onTapLeft, + required VoidCallback onTapRight, + }) async { + await pumpWidget( + MaterialApp( + home: Scaffold( + body: MobileDpad( + onTapUp: onTapUp, + onTapDown: onTapDown, + onTapLeft: onTapLeft, + onTapRight: onTapRight, + ), + ), + ), + ); + } +} + +extension _CommonFindersX on CommonFinders { + Finder byPinballDpadDirection(PinballDpadDirection direction) { + return byWidgetPredicate((widget) { + return widget is PinballDpadButton && widget.direction == direction; + }); + } +} + +abstract class _VoidCallbackStubBase { + void onCall(); +} + +class _VoidCallbackStub extends Mock implements _VoidCallbackStubBase {} + +void main() { + group('MobileDpad', () { + testWidgets('renders correctly', (tester) async { + await tester.pumpDpad( + onTapUp: () {}, + onTapDown: () {}, + onTapLeft: () {}, + onTapRight: () {}, + ); + + expect( + find.byType(PinballDpadButton), + findsNWidgets(4), + ); + }); + + testWidgets('can tap up', (tester) async { + final stub = _VoidCallbackStub(); + await tester.pumpDpad( + onTapUp: stub.onCall, + onTapDown: () {}, + onTapLeft: () {}, + onTapRight: () {}, + ); + + await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.up)); + verify(stub.onCall).called(1); + }); + + testWidgets('can tap down', (tester) async { + final stub = _VoidCallbackStub(); + await tester.pumpDpad( + onTapUp: () {}, + onTapDown: stub.onCall, + onTapLeft: () {}, + onTapRight: () {}, + ); + + await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.down)); + verify(stub.onCall).called(1); + }); + + testWidgets('can tap left', (tester) async { + final stub = _VoidCallbackStub(); + await tester.pumpDpad( + onTapUp: () {}, + onTapDown: () {}, + onTapLeft: stub.onCall, + onTapRight: () {}, + ); + + await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.left)); + verify(stub.onCall).called(1); + }); + + testWidgets('can tap left', (tester) async { + final stub = _VoidCallbackStub(); + await tester.pumpDpad( + onTapUp: () {}, + onTapDown: () {}, + onTapLeft: () {}, + onTapRight: stub.onCall, + ); + + await tester.tap(find.byPinballDpadDirection(PinballDpadDirection.right)); + verify(stub.onCall).called(1); + }); + }); +} diff --git a/web/index.html b/web/index.html index f60ae7ce..30eb9080 100644 --- a/web/index.html +++ b/web/index.html @@ -76,6 +76,10 @@ application. For more information, see: https://developers.google.com/web/fundamentals/primers/service-workers -->