diff --git a/.github/workflows/pinball_components.yaml b/.github/workflows/pinball_components.yaml index e4154059..d75553f5 100644 --- a/.github/workflows/pinball_components.yaml +++ b/.github/workflows/pinball_components.yaml @@ -13,7 +13,8 @@ on: jobs: build: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@b075749771679a5baa4c90d36ad2e8580bbf273b + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: packages/pinball_components coverage_excludes: "lib/gen/*.dart" + test_optimization: false diff --git a/lib/game/assets_manager/cubit/assets_manager_cubit.dart b/lib/game/assets_manager/cubit/assets_manager_cubit.dart new file mode 100644 index 00000000..b97483d4 --- /dev/null +++ b/lib/game/assets_manager/cubit/assets_manager_cubit.dart @@ -0,0 +1,27 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.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, + ), + ); + + /// Loads the assets + Future load() async { + 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/game/assets_manager/cubit/assets_manager_state.dart b/lib/game/assets_manager/cubit/assets_manager_state.dart new file mode 100644 index 00000000..8ef1e874 --- /dev/null +++ b/lib/game/assets_manager/cubit/assets_manager_state.dart @@ -0,0 +1,41 @@ +part of 'assets_manager_cubit.dart'; + +/// {@template assets_manager_state} +/// State used to load the game assets +/// {@endtemplate} +class AssetsManagerState extends Equatable { + /// {@macro assets_manager_state} + const AssetsManagerState({ + required this.loadables, + required this.loaded, + }); + + /// {@macro assets_manager_state} + const AssetsManagerState.initial({ + required List loadables, + }) : this(loadables: loadables, loaded: const []); + + /// List of futures to load + final List loadables; + + /// List of loaded futures + final List loaded; + + /// Returns a value between 0 and 1 to indicate the loading progress + double get progress => loaded.length / loadables.length; + + /// Returns a copy of this instance with the given parameters + /// updated + AssetsManagerState copyWith({ + List? loadables, + List? loaded, + }) { + return AssetsManagerState( + loadables: loadables ?? this.loadables, + loaded: loaded ?? this.loaded, + ); + } + + @override + List get props => [loaded, loadables]; +} diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index a312daee..41581cc3 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -12,11 +12,14 @@ class Board extends Component { @override Future onLoad() async { + // TODO(allisonryan0002): add bottom group and flutter forest to pinball + // game directly. Then remove board. final bottomGroup = _BottomGroup(); final flutterForest = FlutterForest(); // TODO(alestiago): adjust positioning to real design. + // TODO(alestiago): add dino in pinball game. final dino = ChromeDino() ..initialPosition = Vector2( BoardDimensions.bounds.center.dx + 25, @@ -74,7 +77,7 @@ class _BottomGroupSide extends Component { final flipper = ControlledFlipper( side: _side, - )..initialPosition = Vector2((11.0 * direction) + centerXAdjustment, -42.4); + )..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, -43.6); final baseboard = Baseboard(side: _side) ..initialPosition = Vector2( (25.58 * direction) + centerXAdjustment, diff --git a/lib/game/components/camera_controller.dart b/lib/game/components/camera_controller.dart new file mode 100644 index 00000000..aa963e9a --- /dev/null +++ b/lib/game/components/camera_controller.dart @@ -0,0 +1,80 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:pinball/flame/flame.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// Adds helpers methods to Flame's [Camera] +extension CameraX on Camera { + /// Instantly apply the point of focus to the [Camera] + void snapToFocus(FocusData data) { + followVector2(data.position); + zoom = data.zoom; + } + + /// Returns a [CameraZoom] that can be added to a [FlameGame] + CameraZoom focusToCameraZoom(FocusData data) { + final zoom = CameraZoom(value: data.zoom); + zoom.completed.then((_) { + moveTo(data.position); + }); + return zoom; + } +} + +/// {@template focus_data} +/// Model class that defines a focus point of the camera +/// {@endtemplate} +class FocusData { + /// {@template focus_data} + FocusData({ + required this.zoom, + required this.position, + }); + + /// The amount of zoom + final double zoom; + + /// The position of the camera + final Vector2 position; +} + +/// {@template camera_controller} +/// A [Component] that controls its game camera focus +/// {@endtemplate} +class CameraController extends ComponentController { + /// {@macro camera_controller} + CameraController(FlameGame component) : super(component) { + final gameZoom = component.size.y / 16; + final backboardZoom = component.size.y / 18; + + gameFocus = FocusData( + zoom: gameZoom, + position: Vector2(0, -7.8), + ); + backboardFocus = FocusData( + zoom: backboardZoom, + position: Vector2(0, -100.8), + ); + + // Game starts with the camera focused on the panel + component.camera + ..speed = 100 + ..snapToFocus(backboardFocus); + } + + /// Holds the data for the game focus point + late final FocusData gameFocus; + + /// Holds the data for the backboard focus point + late final FocusData backboardFocus; + + /// Move the camera focus to the game board + void focusOnGame() { + component.add(component.camera.focusToCameraZoom(gameFocus)); + } + + /// Move the camera focus to the backboard + void focusOnBackboard() { + component.add(component.camera.focusToCameraZoom(backboardFocus)); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 61d0f3ca..6bc65a89 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,8 +1,11 @@ export 'board.dart'; export 'bonus_word.dart'; +export 'camera_controller.dart'; export 'controlled_ball.dart'; export 'controlled_flipper.dart'; export 'flutter_forest.dart'; +export 'game_flow_controller.dart'; export 'plunger.dart'; export 'score_points.dart'; +export 'sparky_fire_zone.dart'; export 'wall.dart'; diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart new file mode 100644 index 00000000..b0f6f514 --- /dev/null +++ b/lib/game/components/game_flow_controller.dart @@ -0,0 +1,41 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/flame/flame.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template game_flow_controller} +/// A [Component] that controls the game over and game restart logic +/// {@endtemplate} +class GameFlowController extends ComponentController + with BlocComponent { + /// {@macro game_flow_controller} + GameFlowController(PinballGame component) : super(component); + + @override + bool listenWhen(GameState? previousState, GameState newState) { + return previousState?.isGameOver != newState.isGameOver; + } + + @override + void onNewState(GameState state) { + if (state.isGameOver) { + gameOver(); + } else { + start(); + } + } + + /// Puts the game on a game over state + void gameOver() { + component.firstChild()?.gameOverMode(); + component.firstChild()?.focusOnBackboard(); + } + + /// Puts the game on a playing state + void start() { + component.firstChild()?.waitingMode(); + component.firstChild()?.focusOnGame(); + component.overlays.remove(PinballGame.playButtonOverlay); + } +} diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 1911be02..b8c079b5 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -136,12 +136,10 @@ class PlungerAnchor extends JointAnchor { @override Body createBody() { - final shape = CircleShape()..radius = 0.5; - final fixtureDef = FixtureDef(shape); final bodyDef = BodyDef() ..position = initialPosition ..type = BodyType.static; - return world.createBody(bodyDef)..createFixture(fixtureDef); + return world.createBody(bodyDef); } } diff --git a/lib/game/components/sparky_fire_zone.dart b/lib/game/components/sparky_fire_zone.dart new file mode 100644 index 00000000..9d88f0f5 --- /dev/null +++ b/lib/game/components/sparky_fire_zone.dart @@ -0,0 +1,44 @@ +// ignore_for_file: avoid_renaming_method_parameters + +import 'package:flame/components.dart'; +import 'package:pinball/flame/flame.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +// TODO(ruimiguel): create and add SparkyFireZone component here in other PR. + +// TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done +// ignore: public_member_api_docs +class ControlledSparkyBumper extends SparkyBumper + with Controls<_SparkyBumperController> { + // TODO(ruimiguel): make private and remove ignore once SparkyFireZone is done + // ignore: public_member_api_docs + ControlledSparkyBumper() : super.a() { + controller = _SparkyBumperController(this); + } +} + +/// {@template sparky_bumper_controller} +/// Controls a [SparkyBumper]. +/// {@endtemplate} +class _SparkyBumperController extends ComponentController + with HasGameRef { + /// {@macro sparky_bumper_controller} + _SparkyBumperController(ControlledSparkyBumper controlledSparkyBumper) + : super(controlledSparkyBumper); + + /// Flag for activated state of the [SparkyBumper]. + /// + /// Used to toggle [SparkyBumper]s' state between activated and deactivated. + bool isActivated = false; + + /// Registers when a [SparkyBumper] is hit by a [Ball]. + void hit() { + if (isActivated) { + component.deactivate(); + } else { + component.activate(); + } + isActivated = !isActivated; + } +} diff --git a/lib/game/game.dart b/lib/game/game.dart index ad02533d..7de964eb 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -1,3 +1,4 @@ +export 'assets_manager/cubit/assets_manager_cubit.dart'; export 'bloc/game_bloc.dart'; export 'components/components.dart'; export 'game_assets.dart'; diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 050b2cd3..a4eedeb4 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -4,9 +4,9 @@ import 'package:pinball_components/pinball_components.dart' as components; /// Add methods to help loading and caching game assets. extension PinballGameAssetsX on PinballGame { - /// Pre load the initial assets of the game. - Future preLoadAssets() async { - await Future.wait([ + /// Returns a list of assets to be loaded + List preLoadAssets() { + return [ images.load(components.Assets.images.ball.keyName), images.load(components.Assets.images.flutterSignPost.keyName), images.load(components.Assets.images.flipper.left.keyName), @@ -46,7 +46,9 @@ extension PinballGameAssetsX on PinballGame { images.load(components.Assets.images.spaceship.rail.foreground.keyName), images.load(components.Assets.images.chromeDino.mouth.keyName), images.load(components.Assets.images.chromeDino.head.keyName), + images.load(components.Assets.images.backboard.backboardScores.keyName), + images.load(components.Assets.images.backboard.backboardGameOver.keyName), images.load(Assets.images.components.background.path), - ]); + ]; } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 2ccf8fe8..4ccad9db 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -25,17 +25,22 @@ class PinballGame extends Forge2DGame controller = _GameBallsController(this); } + /// Identifier of the play button overlay + static const playButtonOverlay = 'play_button'; + final PinballTheme theme; final PinballAudio audio; + late final GameFlowController gameFlowController; + @override Future onLoad() async { _addContactCallbacks(); - // Fix camera on the center of the board. - camera - ..followVector2(Vector2(0, -7.8)) - ..zoom = size.y / 16; + + unawaited(add(gameFlowController = GameFlowController(this))); + unawaited(add(CameraController(this))); + unawaited(add(Backboard(position: Vector2(0, -88)))); await _addGameBoundaries(); unawaited(addFromBlueprint(Boundaries())); diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index e50eb2d7..f6b7ee81 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -9,16 +9,41 @@ import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_theme/pinball_theme.dart'; class PinballGamePage extends StatelessWidget { - const PinballGamePage({Key? key, required this.theme}) : super(key: key); + const PinballGamePage({ + Key? key, + required this.theme, + required this.game, + }) : super(key: key); final PinballTheme theme; + final PinballGame game; - static Route route({required PinballTheme theme}) { + static Route route({ + required PinballTheme theme, + bool isDebugMode = kDebugMode, + }) { return MaterialPageRoute( - builder: (_) { - return BlocProvider( - create: (_) => GameBloc(), - child: PinballGamePage(theme: theme), + builder: (context) { + final audio = context.read(); + + final game = isDebugMode + ? DebugPinballGame(theme: theme, audio: audio) + : PinballGame(theme: theme, audio: audio); + + final pinballAudio = context.read(); + final loadables = [ + ...game.preLoadAssets(), + pinballAudio.load(), + ]; + + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => GameBloc()), + BlocProvider( + create: (_) => AssetsManagerCubit(loadables)..load(), + ), + ], + child: PinballGamePage(theme: theme, game: game), ); }, ); @@ -26,82 +51,56 @@ class PinballGamePage extends StatelessWidget { @override Widget build(BuildContext context) { - return PinballGameView(theme: theme); + return PinballGameView(game: game); } } -class PinballGameView extends StatefulWidget { +class PinballGameView extends StatelessWidget { const PinballGameView({ Key? key, - required this.theme, - bool isDebugMode = kDebugMode, - }) : _isDebugMode = isDebugMode, - super(key: key); - - final PinballTheme theme; - final bool _isDebugMode; - - @override - State createState() => _PinballGameViewState(); -} + required this.game, + }) : super(key: key); -class _PinballGameViewState extends State { - late PinballGame _game; + final PinballGame game; @override - void initState() { - super.initState(); - - final audio = context.read(); - - _game = widget._isDebugMode - ? DebugPinballGame(theme: widget.theme, audio: audio) - : PinballGame(theme: widget.theme, audio: audio); - - // TODO(erickzanardo): Revisit this when we start to have more assets - // this could expose a Stream (maybe even a cubit?) so we could show the - // the loading progress with some fancy widgets. - _fetchAssets(); - } + Widget build(BuildContext context) { + final loadingProgress = context.watch().state.progress; - Future _fetchAssets() async { - final pinballAudio = context.read(); - await Future.wait([ - _game.preLoadAssets(), - pinballAudio.load(), - ]); - } + if (loadingProgress != 1) { + return Scaffold( + body: Center( + child: Text( + loadingProgress.toString(), + ), + ), + ); + } - @override - Widget build(BuildContext context) { - return BlocListener( - listenWhen: (previous, current) => - previous.isGameOver != current.isGameOver, - listener: (context, state) { - if (state.isGameOver) { - showDialog( - context: context, - builder: (_) { - return GameOverDialog( - score: state.score, - theme: widget.theme.characterTheme, - ); + return Stack( + children: [ + Positioned.fill( + child: GameWidget( + game: game, + initialActiveOverlays: const [PinballGame.playButtonOverlay], + overlayBuilderMap: { + PinballGame.playButtonOverlay: (context, game) { + return Positioned( + bottom: 20, + right: 0, + left: 0, + child: PlayButtonOverlay(game: game), + ); + }, }, - ); - } - }, - child: Stack( - children: [ - Positioned.fill( - child: GameWidget(game: _game), - ), - const Positioned( - top: 8, - left: 8, - child: GameHud(), ), - ], - ), + ), + const Positioned( + top: 8, + left: 8, + child: GameHud(), + ), + ], ); } } diff --git a/lib/game/view/widgets/game_over_dialog.dart b/lib/game/view/widgets/game_over_dialog.dart deleted file mode 100644 index e3c5a1e1..00000000 --- a/lib/game/view/widgets/game_over_dialog.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -/// {@template game_over_dialog} -/// [Dialog] displayed when the [PinballGame] is over. -/// {@endtemplate} -class GameOverDialog extends StatelessWidget { - /// {@macro game_over_dialog} - const GameOverDialog({Key? key, required this.score, required this.theme}) - : super(key: key); - - /// Score achieved by the current user. - final int score; - - /// Theme of the current user. - final CharacterTheme theme; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => LeaderboardBloc( - context.read(), - ), - child: GameOverDialogView(score: score, theme: theme), - ); - } -} - -/// {@template game_over_dialog_view} -/// View for showing final score when the game is finished. -/// {@endtemplate} -@visibleForTesting -class GameOverDialogView extends StatefulWidget { - /// {@macro game_over_dialog_view} - const GameOverDialogView({ - Key? key, - required this.score, - required this.theme, - }) : super(key: key); - - /// Score achieved by the current user. - final int score; - - /// Theme of the current user. - final CharacterTheme theme; - - @override - State createState() => _GameOverDialogViewState(); -} - -class _GameOverDialogViewState extends State { - final playerInitialsInputController = TextEditingController(); - - @override - void dispose() { - playerInitialsInputController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - // TODO(ruimiguel): refactor this view once UI design finished. - return Dialog( - child: SizedBox( - width: 200, - height: 250, - child: Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - l10n.gameOver, - style: Theme.of(context).textTheme.headline4, - ), - const SizedBox( - height: 20, - ), - Text( - '${l10n.yourScore} ${widget.score}', - style: Theme.of(context).textTheme.headline6, - ), - const SizedBox( - height: 15, - ), - TextField( - key: const Key('player_initials_text_field'), - controller: playerInitialsInputController, - textCapitalization: TextCapitalization.characters, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: l10n.enterInitials, - ), - maxLength: 3, - ), - const SizedBox( - height: 10, - ), - _GameOverDialogActions( - score: widget.score, - theme: widget.theme, - playerInitialsInputController: - playerInitialsInputController, - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -class _GameOverDialogActions extends StatelessWidget { - const _GameOverDialogActions({ - Key? key, - required this.score, - required this.theme, - required this.playerInitialsInputController, - }) : super(key: key); - - final int score; - final CharacterTheme theme; - final TextEditingController playerInitialsInputController; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return BlocBuilder( - builder: (context, state) { - switch (state.status) { - case LeaderboardStatus.loading: - return TextButton( - onPressed: () { - context.read().add( - LeaderboardEntryAdded( - entry: LeaderboardEntryData( - playerInitials: - playerInitialsInputController.text.toUpperCase(), - score: score, - character: theme.toType, - ), - ), - ); - }, - child: Text(l10n.addUser), - ); - case LeaderboardStatus.success: - return TextButton( - onPressed: () => Navigator.of(context).push( - LeaderboardPage.route(theme: theme), - ), - child: Text(l10n.leaderboard), - ); - case LeaderboardStatus.error: - return Text(l10n.error); - } - }, - ); - } -} diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart new file mode 100644 index 00000000..6f039124 --- /dev/null +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:pinball/game/pinball_game.dart'; +import 'package:pinball/l10n/l10n.dart'; + +/// {@template play_button_overlay} +/// [Widget] that renders the button responsible to starting the game +/// {@endtemplate} +class PlayButtonOverlay extends StatelessWidget { + /// {@macro play_button_overlay} + const PlayButtonOverlay({ + Key? key, + required PinballGame game, + }) : _game = game, + super(key: key); + + final PinballGame _game; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Center( + child: ElevatedButton( + onPressed: _game.gameFlowController.start, + child: Text(l10n.play), + ), + ); + } +} diff --git a/lib/game/view/widgets/widgets.dart b/lib/game/view/widgets/widgets.dart index aa473c64..7e9db5c3 100644 --- a/lib/game/view/widgets/widgets.dart +++ b/lib/game/view/widgets/widgets.dart @@ -1,2 +1,2 @@ export 'game_hud.dart'; -export 'game_over_dialog.dart'; +export 'play_button_overlay.dart'; diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 5c2a87c2..90013646 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -3,6 +3,8 @@ /// FlutterGen /// ***************************************************** +// ignore_for_file: directives_ordering,unnecessary_import + import 'package:flutter/widgets.dart'; class $AssetsImagesGen { @@ -15,6 +17,7 @@ class $AssetsImagesGen { class $AssetsImagesComponentsGen { const $AssetsImagesComponentsGen(); + /// File path: assets/images/components/background.png AssetGenImage get background => const AssetGenImage('assets/images/components/background.png'); diff --git a/packages/pinball_components/assets/images/backboard/backboard_game_over.png b/packages/pinball_components/assets/images/backboard/backboard_game_over.png new file mode 100644 index 00000000..70bd4544 Binary files /dev/null and b/packages/pinball_components/assets/images/backboard/backboard_game_over.png differ diff --git a/packages/pinball_components/assets/images/backboard/backboard_scores.png b/packages/pinball_components/assets/images/backboard/backboard_scores.png new file mode 100644 index 00000000..bb591b14 Binary files /dev/null and b/packages/pinball_components/assets/images/backboard/backboard_scores.png differ diff --git a/packages/pinball_components/lib/gen/assets.gen.dart b/packages/pinball_components/lib/gen/assets.gen.dart index 518d3237..4273ffb8 100644 --- a/packages/pinball_components/lib/gen/assets.gen.dart +++ b/packages/pinball_components/lib/gen/assets.gen.dart @@ -3,16 +3,13 @@ /// FlutterGen /// ***************************************************** -// ignore_for_file: directives_ordering,unnecessary_import - import 'package:flutter/widgets.dart'; class $AssetsImagesGen { const $AssetsImagesGen(); - /// File path: assets/images/ball.png + $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); - $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesChromeDinoGen get chromeDino => @@ -21,11 +18,8 @@ class $AssetsImagesGen { const $AssetsImagesDashBumperGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); - - /// File path: assets/images/flutter_sign_post.png AssetGenImage get flutterSignPost => const AssetGenImage('assets/images/flutter_sign_post.png'); - $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesLaunchRampGen get launchRamp => const $AssetsImagesLaunchRampGen(); @@ -35,14 +29,20 @@ class $AssetsImagesGen { const $AssetsImagesSparkyBumperGen(); } +class $AssetsImagesBackboardGen { + const $AssetsImagesBackboardGen(); + + AssetGenImage get backboardGameOver => + const AssetGenImage('assets/images/backboard/backboard_game_over.png'); + AssetGenImage get backboardScores => + const AssetGenImage('assets/images/backboard/backboard_scores.png'); +} + class $AssetsImagesBaseboardGen { const $AssetsImagesBaseboardGen(); - /// File path: assets/images/baseboard/left.png AssetGenImage get left => const AssetGenImage('assets/images/baseboard/left.png'); - - /// File path: assets/images/baseboard/right.png AssetGenImage get right => const AssetGenImage('assets/images/baseboard/right.png'); } @@ -50,11 +50,8 @@ class $AssetsImagesBaseboardGen { class $AssetsImagesBoundaryGen { const $AssetsImagesBoundaryGen(); - /// File path: assets/images/boundary/bottom.png AssetGenImage get bottom => const AssetGenImage('assets/images/boundary/bottom.png'); - - /// File path: assets/images/boundary/outer.png AssetGenImage get outer => const AssetGenImage('assets/images/boundary/outer.png'); } @@ -62,11 +59,8 @@ class $AssetsImagesBoundaryGen { class $AssetsImagesChromeDinoGen { const $AssetsImagesChromeDinoGen(); - /// File path: assets/images/chrome_dino/head.png AssetGenImage get head => const AssetGenImage('assets/images/chrome_dino/head.png'); - - /// File path: assets/images/chrome_dino/mouth.png AssetGenImage get mouth => const AssetGenImage('assets/images/chrome_dino/mouth.png'); } @@ -83,11 +77,8 @@ class $AssetsImagesDashBumperGen { class $AssetsImagesDinoGen { const $AssetsImagesDinoGen(); - /// File path: assets/images/dino/dino-land-bottom.png AssetGenImage get dinoLandBottom => const AssetGenImage('assets/images/dino/dino-land-bottom.png'); - - /// File path: assets/images/dino/dino-land-top.png AssetGenImage get dinoLandTop => const AssetGenImage('assets/images/dino/dino-land-top.png'); } @@ -95,11 +86,8 @@ class $AssetsImagesDinoGen { class $AssetsImagesFlipperGen { const $AssetsImagesFlipperGen(); - /// File path: assets/images/flipper/left.png AssetGenImage get left => const AssetGenImage('assets/images/flipper/left.png'); - - /// File path: assets/images/flipper/right.png AssetGenImage get right => const AssetGenImage('assets/images/flipper/right.png'); } @@ -107,11 +95,8 @@ class $AssetsImagesFlipperGen { class $AssetsImagesKickerGen { const $AssetsImagesKickerGen(); - /// File path: assets/images/kicker/left.png AssetGenImage get left => const AssetGenImage('assets/images/kicker/left.png'); - - /// File path: assets/images/kicker/right.png AssetGenImage get right => const AssetGenImage('assets/images/kicker/right.png'); } @@ -119,11 +104,8 @@ class $AssetsImagesKickerGen { class $AssetsImagesLaunchRampGen { const $AssetsImagesLaunchRampGen(); - /// File path: assets/images/launch_ramp/foreground-railing.png AssetGenImage get foregroundRailing => const AssetGenImage('assets/images/launch_ramp/foreground-railing.png'); - - /// File path: assets/images/launch_ramp/ramp.png AssetGenImage get ramp => const AssetGenImage('assets/images/launch_ramp/ramp.png'); } @@ -131,19 +113,12 @@ class $AssetsImagesLaunchRampGen { class $AssetsImagesSlingshotGen { const $AssetsImagesSlingshotGen(); - /// File path: assets/images/slingshot/left_lower.png AssetGenImage get leftLower => const AssetGenImage('assets/images/slingshot/left_lower.png'); - - /// File path: assets/images/slingshot/left_upper.png AssetGenImage get leftUpper => const AssetGenImage('assets/images/slingshot/left_upper.png'); - - /// File path: assets/images/slingshot/right_lower.png AssetGenImage get rightLower => const AssetGenImage('assets/images/slingshot/right_lower.png'); - - /// File path: assets/images/slingshot/right_upper.png AssetGenImage get rightUpper => const AssetGenImage('assets/images/slingshot/right_upper.png'); } @@ -151,16 +126,12 @@ class $AssetsImagesSlingshotGen { class $AssetsImagesSpaceshipGen { const $AssetsImagesSpaceshipGen(); - /// File path: assets/images/spaceship/bridge.png AssetGenImage get bridge => const AssetGenImage('assets/images/spaceship/bridge.png'); - $AssetsImagesSpaceshipRailGen get rail => const $AssetsImagesSpaceshipRailGen(); $AssetsImagesSpaceshipRampGen get ramp => const $AssetsImagesSpaceshipRampGen(); - - /// File path: assets/images/spaceship/saucer.png AssetGenImage get saucer => const AssetGenImage('assets/images/spaceship/saucer.png'); } @@ -176,11 +147,8 @@ class $AssetsImagesSparkyBumperGen { class $AssetsImagesDashBumperAGen { const $AssetsImagesDashBumperAGen(); - /// File path: assets/images/dash_bumper/a/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/a/active.png'); - - /// File path: assets/images/dash_bumper/a/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/a/inactive.png'); } @@ -188,11 +156,8 @@ class $AssetsImagesDashBumperAGen { class $AssetsImagesDashBumperBGen { const $AssetsImagesDashBumperBGen(); - /// File path: assets/images/dash_bumper/b/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/b/active.png'); - - /// File path: assets/images/dash_bumper/b/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/b/inactive.png'); } @@ -200,11 +165,8 @@ class $AssetsImagesDashBumperBGen { class $AssetsImagesDashBumperMainGen { const $AssetsImagesDashBumperMainGen(); - /// File path: assets/images/dash_bumper/main/active.png AssetGenImage get active => const AssetGenImage('assets/images/dash_bumper/main/active.png'); - - /// File path: assets/images/dash_bumper/main/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/dash_bumper/main/inactive.png'); } @@ -212,11 +174,8 @@ class $AssetsImagesDashBumperMainGen { class $AssetsImagesSpaceshipRailGen { const $AssetsImagesSpaceshipRailGen(); - /// File path: assets/images/spaceship/rail/foreground.png AssetGenImage get foreground => const AssetGenImage('assets/images/spaceship/rail/foreground.png'); - - /// File path: assets/images/spaceship/rail/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/rail/main.png'); } @@ -224,15 +183,10 @@ class $AssetsImagesSpaceshipRailGen { class $AssetsImagesSpaceshipRampGen { const $AssetsImagesSpaceshipRampGen(); - /// File path: assets/images/spaceship/ramp/main.png AssetGenImage get main => const AssetGenImage('assets/images/spaceship/ramp/main.png'); - - /// File path: assets/images/spaceship/ramp/railing-background.png AssetGenImage get railingBackground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-background.png'); - - /// File path: assets/images/spaceship/ramp/railing-foreground.png AssetGenImage get railingForeground => const AssetGenImage( 'assets/images/spaceship/ramp/railing-foreground.png'); } @@ -240,11 +194,8 @@ class $AssetsImagesSpaceshipRampGen { class $AssetsImagesSparkyBumperAGen { const $AssetsImagesSparkyBumperAGen(); - /// File path: assets/images/sparky_bumper/a/active.png AssetGenImage get active => const AssetGenImage('assets/images/sparky_bumper/a/active.png'); - - /// File path: assets/images/sparky_bumper/a/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/sparky_bumper/a/inactive.png'); } @@ -252,11 +203,8 @@ class $AssetsImagesSparkyBumperAGen { class $AssetsImagesSparkyBumperBGen { const $AssetsImagesSparkyBumperBGen(); - /// File path: assets/images/sparky_bumper/b/active.png AssetGenImage get active => const AssetGenImage('assets/images/sparky_bumper/b/active.png'); - - /// File path: assets/images/sparky_bumper/b/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/sparky_bumper/b/inactive.png'); } @@ -264,11 +212,8 @@ class $AssetsImagesSparkyBumperBGen { class $AssetsImagesSparkyBumperCGen { const $AssetsImagesSparkyBumperCGen(); - /// File path: assets/images/sparky_bumper/c/active.png AssetGenImage get active => const AssetGenImage('assets/images/sparky_bumper/c/active.png'); - - /// File path: assets/images/sparky_bumper/c/inactive.png AssetGenImage get inactive => const AssetGenImage('assets/images/sparky_bumper/c/inactive.png'); } diff --git a/packages/pinball_components/lib/src/components/backboard.dart b/packages/pinball_components/lib/src/components/backboard.dart new file mode 100644 index 00000000..613cbc05 --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard.dart @@ -0,0 +1,40 @@ +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// {@template backboard} +/// The [Backboard] of the pinball machine. +/// {@endtemplate} +class Backboard extends SpriteComponent with HasGameRef { + /// {@macro backboard} + Backboard({ + required Vector2 position, + }) : super( + // TODO(erickzanardo): remove multiply after + // https://github.com/flame-engine/flame/pull/1506 is merged + position: position..clone().multiply(Vector2(1, -1)), + anchor: Anchor.bottomCenter, + ); + + @override + Future onLoad() async { + await waitingMode(); + } + + /// Puts the Backboard in waiting mode, where the scoreboard is shown. + Future waitingMode() async { + final sprite = await gameRef.loadSprite( + Assets.images.backboard.backboardScores.keyName, + ); + size = sprite.originalSize / 10; + this.sprite = sprite; + } + + /// Puts the Backboard in game over mode, where the score input is shown. + Future gameOverMode() async { + final sprite = await gameRef.loadSprite( + Assets.images.backboard.backboardGameOver.keyName, + ); + size = sprite.originalSize / 10; + this.sprite = sprite; + } +} diff --git a/packages/pinball_components/lib/src/components/ball.dart b/packages/pinball_components/lib/src/components/ball.dart index 892936f9..6aaf88de 100644 --- a/packages/pinball_components/lib/src/components/ball.dart +++ b/packages/pinball_components/lib/src/components/ball.dart @@ -22,36 +22,31 @@ class Ball extends BodyComponent layer = Layer.board; } - /// The size of the [Ball] - static final Vector2 size = Vector2.all(4.5); + /// The size of the [Ball]. + static final Vector2 size = Vector2.all(4.13); - /// The base [Color] used to tint this [Ball] + /// The base [Color] used to tint this [Ball]. final Color baseColor; double _boostTimer = 0; static const _boostDuration = 2.0; - late SpriteComponent _spriteComponent; + + final _BallSpriteComponent _spriteComponent = _BallSpriteComponent(); @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite(Assets.images.ball.keyName); - final tint = baseColor.withOpacity(0.5); + renderBody = false; + await add( - _spriteComponent = SpriteComponent( - sprite: sprite, - size: size * 1.15, - anchor: Anchor.center, - )..tint(tint), + _spriteComponent..tint(baseColor.withOpacity(0.5)), ); } @override Body createBody() { final shape = CircleShape()..radius = size.x / 2; - final fixtureDef = FixtureDef(shape)..density = 1; - final bodyDef = BodyDef() ..position = initialPosition ..userData = this @@ -70,7 +65,7 @@ class Ball extends BodyComponent /// Allows the [Ball] to be affected by forces. /// - /// If previously [stop]ed, the previous ball's velocity is not kept. + /// If previously [stop]ped, the previous ball's velocity is not kept. void resume() { body.setType(BodyType.dynamic); } @@ -114,3 +109,16 @@ class Ball extends BodyComponent _spriteComponent.scale = Vector2.all(scaleFactor); } } + +class _BallSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + Assets.images.ball.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + } +} diff --git a/packages/pinball_components/lib/src/components/baseboard.dart b/packages/pinball_components/lib/src/components/baseboard.dart index 0a6bcc91..56b7c978 100644 --- a/packages/pinball_components/lib/src/components/baseboard.dart +++ b/packages/pinball_components/lib/src/components/baseboard.dart @@ -82,23 +82,8 @@ class Baseboard extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - - final sprite = await gameRef.loadSprite( - (_side.isLeft) - ? Assets.images.baseboard.left.keyName - : Assets.images.baseboard.right.keyName, - ); - - await add( - SpriteComponent( - sprite: sprite, - size: Vector2(27.5, 17.9), - anchor: Anchor.center, - position: Vector2(_side.isLeft ? 0.4 : -0.4, 0), - ), - ); - renderBody = false; + await add(_BaseboardSpriteComponent(side: _side)); } @override @@ -115,3 +100,23 @@ class Baseboard extends BodyComponent with InitialPosition { return body; } } + +class _BaseboardSpriteComponent extends SpriteComponent with HasGameRef { + _BaseboardSpriteComponent({required BoardSide side}) : _side = side; + + final BoardSide _side; + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + (_side.isLeft) + ? Assets.images.baseboard.left.keyName + : Assets.images.baseboard.right.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + position = Vector2(0.4 * -_side.direction, 0); + anchor = Anchor.center; + } +} diff --git a/packages/pinball_components/lib/src/components/boundaries.dart b/packages/pinball_components/lib/src/components/boundaries.dart index 79b3f909..a542cd2a 100644 --- a/packages/pinball_components/lib/src/components/boundaries.dart +++ b/packages/pinball_components/lib/src/components/boundaries.dart @@ -63,23 +63,22 @@ class _BottomBoundary extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); renderBody = false; + await add(_BottomBoundarySpriteComponent()); } +} - Future _loadSprite() async { +class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( Assets.images.boundary.bottom.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.center, - position: Vector2(-5.4, 57.4), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(-5.4, 57.4); } } @@ -135,22 +134,21 @@ class _OuterBoundary extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); renderBody = false; + await add(_OuterBoundarySpriteComponent()); } +} - Future _loadSprite() async { +class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( Assets.images.boundary.outer.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.center, - position: Vector2(-0.2, -1.4), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(-0.2, -1.4); } } diff --git a/packages/pinball_components/lib/src/components/components.dart b/packages/pinball_components/lib/src/components/components.dart index 8ac1a0f9..b71f7c13 100644 --- a/packages/pinball_components/lib/src/components/components.dart +++ b/packages/pinball_components/lib/src/components/components.dart @@ -1,3 +1,4 @@ +export 'backboard.dart'; export 'ball.dart'; export 'baseboard.dart'; export 'board_dimensions.dart'; diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index daf83850..dc5b7a26 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -81,13 +81,10 @@ class _DinoTopWall extends BodyComponent with InitialPosition { @override Body createBody() { - renderBody = false; - final bodyDef = BodyDef() ..userData = this ..position = initialPosition ..type = BodyType.static; - final body = world.createBody(bodyDef); _createFixtureDefs().forEach( (fixture) => body.createFixture( @@ -103,21 +100,22 @@ class _DinoTopWall extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); + renderBody = false; + + await add(_DinoTopWallSpriteComponent()); } +} - Future _loadSprite() async { +class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( Assets.images.dino.dinoLandTop.keyName, ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: Vector2(10.6, 27.7), - anchor: Anchor.center, - position: Vector2(27, -28.2), - ); - - await add(spriteComponent); + this.sprite = sprite; + size = sprite.originalSize / 10; + position = Vector2(22, -41.8); } } @@ -182,8 +180,6 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { @override Body createBody() { - renderBody = false; - final bodyDef = BodyDef() ..userData = this ..position = initialPosition @@ -204,19 +200,21 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); + renderBody = false; + + await add(_DinoBottomWallSpriteComponent()); } +} - Future _loadSprite() async { +class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( Assets.images.dino.dinoLandBottom.keyName, ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: Vector2(15.6, 54.8), - anchor: Anchor.center, - )..position = Vector2(31.7, 18); - - await add(spriteComponent); + this.sprite = sprite; + size = sprite.originalSize / 10; + position = Vector2(23.8, -9.5); } } diff --git a/packages/pinball_components/lib/src/components/flipper.dart b/packages/pinball_components/lib/src/components/flipper.dart index 64a82269..c9580510 100644 --- a/packages/pinball_components/lib/src/components/flipper.dart +++ b/packages/pinball_components/lib/src/components/flipper.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; @@ -42,22 +41,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { body.linearVelocity = Vector2(0, _speed); } - /// Loads the sprite that renders with the [Flipper]. - Future _loadSprite() async { - final sprite = await gameRef.loadSprite( - (side.isLeft) - ? Assets.images.flipper.left.keyName - : Assets.images.flipper.right.keyName, - ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: size, - anchor: Anchor.center, - ); - - await add(spriteComponent); - } - /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. Future _anchorToJoint() async { final anchor = _FlipperAnchor(flipper: this); @@ -129,10 +112,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { await super.onLoad(); renderBody = false; - await Future.wait([ - _loadSprite(), - _anchorToJoint(), - ]); + await _anchorToJoint(); + await add(_FlipperSpriteComponent(side: side)); } @override @@ -148,6 +129,25 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { } } +class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { + _FlipperSpriteComponent({required BoardSide side}) : _side = side; + + final BoardSide _side; + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + (_side.isLeft) + ? Assets.images.flipper.left.keyName + : Assets.images.flipper.right.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + } +} + /// {@template flipper_anchor} /// [JointAnchor] positioned at the end of a [Flipper]. /// @@ -197,8 +197,8 @@ class _FlipperJoint extends RevoluteJoint { lock(); } - /// The total angle of the arc motion. - static const _sweepingAngle = math.pi / 3.5; + /// Half the angle of the arc motion. + static const _halfSweepingAngle = 0.611; final BoardSide side; @@ -207,7 +207,7 @@ class _FlipperJoint extends RevoluteJoint { /// The joint is locked when initialized in order to force the [Flipper] /// at its resting position. void lock() { - const angle = _sweepingAngle / 2; + const angle = _halfSweepingAngle; setLimits( -angle * side.direction, -angle * side.direction, @@ -216,7 +216,7 @@ class _FlipperJoint extends RevoluteJoint { /// Unlocks the [Flipper] from its resting position. void unlock() { - const angle = _sweepingAngle / 2; + const angle = _halfSweepingAngle; setLimits(-angle, angle); } } diff --git a/packages/pinball_components/lib/src/components/flutter_sign_post.dart b/packages/pinball_components/lib/src/components/flutter_sign_post.dart index deaceb76..070fa316 100644 --- a/packages/pinball_components/lib/src/components/flutter_sign_post.dart +++ b/packages/pinball_components/lib/src/components/flutter_sign_post.dart @@ -1,33 +1,17 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; /// {@template flutter_sign_post} -/// A sign, found in the FlutterForest. +/// A sign, found in the Flutter Forest. /// {@endtemplate} -// TODO(alestiago): Revisit doc comment if FlutterForest is moved to package. class FlutterSignPost extends BodyComponent with InitialPosition { - Future _loadSprite() async { - final sprite = await gameRef.loadSprite( - Assets.images.flutterSignPost.keyName, - ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.bottomCenter, - position: Vector2(0.65, 0.45), - ); - await add(spriteComponent); - } - @override Future onLoad() async { await super.onLoad(); - paint = Paint() - ..color = Colors.blue.withOpacity(0.5) - ..style = PaintingStyle.fill; - await _loadSprite(); + renderBody = false; + + await add(_FlutterSignPostSpriteComponent()); } @override @@ -39,3 +23,18 @@ class FlutterSignPost extends BodyComponent with InitialPosition { return world.createBody(bodyDef)..createFixture(fixtureDef); } } + +class _FlutterSignPostSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + + final sprite = await gameRef.loadSprite( + Assets.images.flutterSignPost.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.bottomCenter; + position = Vector2(0.65, 0.45); + } +} diff --git a/packages/pinball_components/lib/src/components/kicker.dart b/packages/pinball_components/lib/src/components/kicker.dart index 442f4200..de009595 100644 --- a/packages/pinball_components/lib/src/components/kicker.dart +++ b/packages/pinball_components/lib/src/components/kicker.dart @@ -18,6 +18,9 @@ class Kicker extends BodyComponent with InitialPosition { required BoardSide side, }) : _side = side; + /// The size of the [Kicker] body. + static final Vector2 size = Vector2(4.4, 15); + /// Whether the [Kicker] is on the left or right side of the board. /// /// A [Kicker] with [BoardSide.left] propels the [Ball] to the right, @@ -25,9 +28,6 @@ class Kicker extends BodyComponent with InitialPosition { /// left. final BoardSide _side; - /// The size of the [Kicker] body. - static final Vector2 size = Vector2(4.4, 15); - List _createFixtureDefs() { final fixturesDefs = []; final direction = _side.direction; @@ -122,21 +122,28 @@ class Kicker extends BodyComponent with InitialPosition { Future onLoad() async { await super.onLoad(); renderBody = false; + await add(_KickerSpriteComponent(side: _side)); + } +} + +class _KickerSpriteComponent extends SpriteComponent with HasGameRef { + _KickerSpriteComponent({required BoardSide side}) : _side = side; + + final BoardSide _side; + + @override + Future onLoad() async { + await super.onLoad(); final sprite = await gameRef.loadSprite( (_side.isLeft) ? Assets.images.kicker.left.keyName : Assets.images.kicker.right.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: Vector2(8.7, 19), - anchor: Anchor.center, - position: Vector2(0.7 * -_side.direction, -2.2), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(0.7 * -_side.direction, -2.2); } } diff --git a/packages/pinball_components/lib/src/components/launch_ramp.dart b/packages/pinball_components/lib/src/components/launch_ramp.dart index 3268cc46..2eea7a91 100644 --- a/packages/pinball_components/lib/src/components/launch_ramp.dart +++ b/packages/pinball_components/lib/src/components/launch_ramp.dart @@ -115,23 +115,24 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); renderBody = false; + + await add(_LaunchRampBaseSpriteComponent()); } +} + +class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); - Future _loadSprite() async { final sprite = await gameRef.loadSprite( Assets.images.launchRamp.ramp.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.center, - position: Vector2(25.65, 0), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(25.65, 0); } } @@ -192,23 +193,25 @@ class _LaunchRampForegroundRailing extends BodyComponent @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); renderBody = false; + + await add(_LaunchRampForegroundRailingSpriteComponent()); } +} + +class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); - Future _loadSprite() async { final sprite = await gameRef.loadSprite( Assets.images.launchRamp.foregroundRailing.keyName, ); - - await add( - SpriteComponent( - sprite: sprite, - size: sprite.originalSize / 10, - anchor: Anchor.center, - position: Vector2(22.8, 0), - ), - ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(22.8, 0); } } diff --git a/packages/pinball_components/lib/src/components/spaceship.dart b/packages/pinball_components/lib/src/components/spaceship.dart index f1b58db0..10144eef 100644 --- a/packages/pinball_components/lib/src/components/spaceship.dart +++ b/packages/pinball_components/lib/src/components/spaceship.dart @@ -102,27 +102,9 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered { @override Future onLoad() async { await super.onLoad(); - renderBody = false; - final sprite = await gameRef.images.load( - Assets.images.spaceship.bridge.keyName, - ); - - await add( - SpriteAnimationComponent.fromFrameData( - sprite, - SpriteAnimationData.sequenced( - amount: 72, - amountPerRow: 24, - stepTime: 0.05, - textureSize: Vector2(82, 100), - ), - size: Vector2(8.2, 10), - position: Vector2(0, -2), - anchor: Anchor.center, - ), - ); + await add(_AndroidHeadSpriteAnimation()); } @override @@ -141,6 +123,29 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered { } } +class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + + final image = await gameRef.images.load( + Assets.images.spaceship.bridge.keyName, + ); + size = Vector2(8.2, 10); + position = Vector2(0, -2); + anchor = Anchor.center; + + final data = SpriteAnimationData.sequenced( + amount: 72, + amountPerRow: 24, + stepTime: 0.05, + textureSize: size * 10, + ); + animation = SpriteAnimation.fromFrameData(image, data); + } +} + /// {@template spaceship_entrance} /// A sensor [BodyComponent] used to detect when the ball enters the /// the spaceship area in order to modify its filter data so the ball @@ -228,8 +233,11 @@ class _SpaceshipWallShape extends ChainShape { /// {@template spaceship_wall} /// A [BodyComponent] that provides the collision for the wall -/// surrounding the spaceship, with a small opening to allow the -/// [Ball] to get inside the spaceship saucer. +/// surrounding the spaceship. +/// +/// It has a small opening to allow the [Ball] to get inside the spaceship +/// saucer. +/// /// It also contains the [SpriteComponent] for the lower wall /// {@endtemplate} class SpaceshipWall extends BodyComponent with InitialPosition, Layered { diff --git a/packages/pinball_components/lib/src/components/spaceship_rail.dart b/packages/pinball_components/lib/src/components/spaceship_rail.dart index ace12e61..2cc8bccc 100644 --- a/packages/pinball_components/lib/src/components/spaceship_rail.dart +++ b/packages/pinball_components/lib/src/components/spaceship_rail.dart @@ -139,21 +139,23 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { @override Future onLoad() async { await super.onLoad(); - await _loadSprite(); + await add(_SpaceshipRailRampSpriteComponent()); } +} + +class _SpaceshipRailRampSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); - Future _loadSprite() async { final sprite = await gameRef.loadSprite( Assets.images.spaceship.rail.main.keyName, ); - final spriteComponent = SpriteComponent( - sprite: sprite, - size: Vector2(17.5, 55.7), - anchor: Anchor.center, - position: Vector2(-29.4, -5.7), - ); - - await add(spriteComponent); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(-29.4, -5.7); } } diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp.dart index 6d5b205e..252c98bf 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp.dart @@ -102,8 +102,6 @@ class _SpaceshipRampBackground extends BodyComponent @override Body createBody() { - renderBody = false; - final bodyDef = BodyDef() ..userData = this ..position = initialPosition; @@ -117,35 +115,40 @@ class _SpaceshipRampBackground extends BodyComponent @override Future onLoad() async { await super.onLoad(); - await _loadSprites(); - } - - Future _loadSprites() async { - final spriteRamp = await gameRef.loadSprite( - Assets.images.spaceship.ramp.main.keyName, - ); + renderBody = false; - final spriteRampComponent = SpriteComponent( - sprite: spriteRamp, - size: spriteRamp.originalSize / 10, - anchor: Anchor.center, - position: Vector2(-10.6, -53.6), - ); + await add(_SpaceshipRampBackgroundRailingSpriteComponent()); + await add(_SpaceshipRampBackgroundRampSpriteComponent()); + } +} - final spriteRailingBg = await gameRef.loadSprite( +class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( Assets.images.spaceship.ramp.railingBackground.keyName, ); - final spriteRailingBgComponent = SpriteComponent( - sprite: spriteRailingBg, - size: spriteRailingBg.originalSize / 10, - anchor: Anchor.center, - position: spriteRampComponent.position + Vector2(-1.1, -0.7), - ); + this.sprite = sprite; + size = Vector2(38.3, 35.1); + anchor = Anchor.center; + position = Vector2(-11.7, -54.3); + } +} - await addAll([ - spriteRampComponent, - spriteRailingBgComponent, - ]); +class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( + Assets.images.spaceship.ramp.main.keyName, + ); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(-10.6, -53.6); } } @@ -209,21 +212,22 @@ class _SpaceshipRampForegroundRailing extends BodyComponent @override Future onLoad() async { await super.onLoad(); - await _loadSprites(); + await add(_SpaceshipRampForegroundRalingSpriteComponent()); } +} - Future _loadSprites() async { - final spriteRailingFg = await gameRef.loadSprite( +class _SpaceshipRampForegroundRalingSpriteComponent extends SpriteComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await gameRef.loadSprite( Assets.images.spaceship.ramp.railingForeground.keyName, ); - final spriteRailingFgComponent = SpriteComponent( - sprite: spriteRailingFg, - size: spriteRailingFg.originalSize / 10, - anchor: Anchor.center, - position: Vector2(-12.3, -52.5), - ); - - await add(spriteRailingFgComponent); + this.sprite = sprite; + size = sprite.originalSize / 10; + anchor = Anchor.center; + position = Vector2(-12.3, -52.5); } } diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml index b6f71b8b..b93f98a2 100644 --- a/packages/pinball_components/pubspec.yaml +++ b/packages/pinball_components/pubspec.yaml @@ -43,6 +43,7 @@ flutter: - assets/images/sparky_bumper/a/ - assets/images/sparky_bumper/b/ - assets/images/sparky_bumper/c/ + - assets/images/backboard/ flutter_gen: line_length: 80 diff --git a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart index 46cfb154..ee9fa88c 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/basic_ball_game.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; -class BasicBallGame extends BasicGame with TapDetector { +class BasicBallGame extends BasicGame with TapDetector, Traceable { BasicBallGame({required this.color}); static const info = ''' @@ -19,5 +19,6 @@ class BasicBallGame extends BasicGame with TapDetector { add( Ball(baseColor: color)..initialPosition = info.eventPosition.game, ); + traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart index 2e945c47..64892d22 100644 --- a/packages/pinball_components/sandbox/lib/stories/ball/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/ball/stories.dart @@ -12,7 +12,7 @@ void addBallStories(Dashbook dashbook) { (context) => GameWidget( game: BasicBallGame( color: context.colorProperty('color', Colors.blue), - ), + )..trace = context.boolProperty('Trace', true), ), codeLink: buildSourceLink('ball/basic.dart'), info: BasicBallGame.info, diff --git a/packages/pinball_components/sandbox/lib/stories/dash_nest_bumper/stories.dart b/packages/pinball_components/sandbox/lib/stories/dash_nest_bumper/stories.dart deleted file mode 100644 index 22f792bc..00000000 --- a/packages/pinball_components/sandbox/lib/stories/dash_nest_bumper/stories.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:dashbook/dashbook.dart'; -import 'package:flame/game.dart'; -import 'package:sandbox/common/common.dart'; -import 'package:sandbox/stories/ball/basic_ball_game.dart'; -import 'package:sandbox/stories/dash_nest_bumper/big_dash_nest_bumper_game.dart'; - -void addDashNestBumperStories(Dashbook dashbook) { - dashbook.storiesOf('Dash Nest Bumpers').add( - 'Big', - (context) => GameWidget( - game: BigDashNestBumperGame() - ..trace = context.boolProperty('Trace', true), - ), - codeLink: buildSourceLink('dash_nest_bumper/big.dart'), - info: BasicBallGame.info, - ); -} diff --git a/packages/pinball_components/sandbox/lib/stories/dash_nest_bumper/big_dash_nest_bumper_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart similarity index 66% rename from packages/pinball_components/sandbox/lib/stories/dash_nest_bumper/big_dash_nest_bumper_game.dart rename to packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart index 649256d8..c1407819 100644 --- a/packages/pinball_components/sandbox/lib/stories/dash_nest_bumper/big_dash_nest_bumper_game.dart +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/big_dash_nest_bumper_game.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ui'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball_components/pinball_components.dart'; import 'package:sandbox/common/common.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart'; @@ -11,19 +12,15 @@ class BigDashNestBumperGame extends BasicBallGame with Traceable { static const info = ''' Shows how a BigDashNestBumper is rendered. - Activate the "trace" parameter to overlay the body. + - Activate the "trace" parameter to overlay the body. '''; @override Future onLoad() async { await super.onLoad(); - - final center = screenToWorld(camera.viewport.canvasSize! / 2); - final bigDashNestBumper = BigDashNestBumper() - ..initialPosition = center - ..priority = 1; - await add(bigDashNestBumper); - + camera.followVector2(Vector2.zero()); + await add(BigDashNestBumper()..priority = 1); + await traceAllBodies(); await traceAllBodies(); } } diff --git a/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart b/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart new file mode 100644 index 00000000..f3ba7bda --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/flutter_sign_post_game.dart @@ -0,0 +1,25 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class FlutterSignPostGame extends BasicBallGame with Traceable { + FlutterSignPostGame() : super(color: const Color(0xFF0000FF)); + + static const info = ''' + Shows how a FlutterSignPost is rendered. + + - Activate the "trace" parameter to overlay the body. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + camera.followVector2(Vector2.zero()); + await add(FlutterSignPost()..priority = 1); + await traceAllBodies(); + } +} 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/small_dash_nest_bumper_a_game.dart new file mode 100644 index 00000000..a8499581 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_a_game.dart @@ -0,0 +1,26 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SmallDashNestBumperAGame extends BasicBallGame with Traceable { + SmallDashNestBumperAGame() : super(color: const Color(0xFF0000FF)); + + static const info = ''' + Shows how a SmallDashNestBumper ("a") is rendered. + + - Activate the "trace" parameter to overlay the body. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + camera.followVector2(Vector2.zero()); + await add(SmallDashNestBumper.a()..priority = 1); + await traceAllBodies(); + 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/small_dash_nest_bumper_b_game.dart new file mode 100644 index 00000000..91b2a383 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/small_dash_nest_bumper_b_game.dart @@ -0,0 +1,26 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class SmallDashNestBumperBGame extends BasicBallGame with Traceable { + SmallDashNestBumperBGame() : super(color: const Color(0xFF0000FF)); + + static const info = ''' + Shows how a SmallDashNestBumper ("b") is rendered. + + - Activate the "trace" parameter to overlay the body. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + camera.followVector2(Vector2.zero()); + await add(SmallDashNestBumper.b()..priority = 1); + await traceAllBodies(); + 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 new file mode 100644 index 00000000..a625d174 --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/flutter_forest/stories.dart @@ -0,0 +1,47 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/game.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/flutter_sign_post_game.dart'; +import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart'; +import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart'; + +void addDashNestBumperStories(Dashbook dashbook) { + dashbook.storiesOf('Flutter Forest') + ..add( + 'Flutter Sign Post', + (context) => GameWidget( + game: FlutterSignPostGame() + ..trace = context.boolProperty('Trace', true), + ), + codeLink: buildSourceLink('flutter_forest/flutter_sign_post.dart'), + info: FlutterSignPostGame.info, + ) + ..add( + 'Big Dash Nest Bumper', + (context) => GameWidget( + game: BigDashNestBumperGame() + ..trace = context.boolProperty('Trace', true), + ), + codeLink: buildSourceLink('flutter_forest/big_dash_nest_bumper.dart'), + info: BigDashNestBumperGame.info, + ) + ..add( + 'Small Dash Nest Bumper A', + (context) => GameWidget( + game: SmallDashNestBumperAGame() + ..trace = context.boolProperty('Trace', true), + ), + codeLink: buildSourceLink('flutter_forest/small_dash_nest_bumper_a.dart'), + info: SmallDashNestBumperAGame.info, + ) + ..add( + 'Small Dash Nest Bumper B', + (context) => GameWidget( + game: SmallDashNestBumperBGame() + ..trace = context.boolProperty('Trace', true), + ), + codeLink: buildSourceLink('flutter_forest/small_dash_nest_bumper_b.dart'), + info: SmallDashNestBumperBGame.info, + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 5ff6e309..e2a8ad18 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -1,9 +1,9 @@ export 'ball/stories.dart'; export 'baseboard/stories.dart'; export 'chrome_dino/stories.dart'; -export 'dash_nest_bumper/stories.dart'; export 'effects/stories.dart'; export 'flipper/stories.dart'; +export 'flutter_forest/stories.dart'; export 'layer/stories.dart'; export 'slingshot/stories.dart'; export 'spaceship/stories.dart'; diff --git a/packages/pinball_components/test/src/components/backboard_test.dart b/packages/pinball_components/test/src/components/backboard_test.dart new file mode 100644 index 00000000..2d95cc47 --- /dev/null +++ b/packages/pinball_components/test/src/components/backboard_test.dart @@ -0,0 +1,53 @@ +// ignore_for_file: unawaited_futures + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('Backboard', () { + final tester = FlameTester(TestGame.new); + + group('on waitingMode', () { + tester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + game.camera.zoom = 2; + game.camera.followVector2(Vector2.zero()); + await game.ensureAdd(Backboard(position: Vector2(0, 15))); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/backboard/waiting.png'), + ); + }, + ); + }); + + group('on gameOverMode', () { + tester.testGameWidget( + 'renders correctly', + setUp: (game, tester) async { + game.camera.zoom = 2; + game.camera.followVector2(Vector2.zero()); + final backboard = Backboard(position: Vector2(0, 15)); + await game.ensureAdd(backboard); + + await backboard.gameOverMode(); + await game.ready(); + await tester.pump(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('golden/backboard/game_over.png'), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/ball_test.dart b/packages/pinball_components/test/src/components/ball_test.dart index f2a54c68..248216f4 100644 --- a/packages/pinball_components/test/src/components/ball_test.dart +++ b/packages/pinball_components/test/src/components/ball_test.dart @@ -86,7 +86,7 @@ void main() { final fixture = ball.body.fixtures[0]; expect(fixture.shape.shapeType, equals(ShapeType.circle)); - expect(fixture.shape.radius, equals(2.25)); + expect(fixture.shape.radius, equals(2.065)); }, ); diff --git a/packages/pinball_components/test/src/components/golden/backboard/game_over.png b/packages/pinball_components/test/src/components/golden/backboard/game_over.png new file mode 100644 index 00000000..04a8e3ad Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/backboard/game_over.png differ diff --git a/packages/pinball_components/test/src/components/golden/backboard/waiting.png b/packages/pinball_components/test/src/components/golden/backboard/waiting.png new file mode 100644 index 00000000..25e24a6b Binary files /dev/null and b/packages/pinball_components/test/src/components/golden/backboard/waiting.png differ diff --git a/pubspec.lock b/pubspec.lock index 240c5a9f..fc1e96a6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -377,14 +377,14 @@ packages: name: mockingjay url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.3.0" mocktail: dependency: "direct dev" description: name: mocktail url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.3.0" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 161afb85..d497e561 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,8 +35,8 @@ dev_dependencies: flame_test: ^1.3.0 flutter_test: sdk: flutter - mockingjay: ^0.2.0 - mocktail: ^0.2.0 + mockingjay: ^0.3.0 + mocktail: ^0.3.0 very_good_analysis: ^2.4.0 flutter: diff --git a/test/game/assets_manager/cubit/assets_manager_cubit_test.dart b/test/game/assets_manager/cubit/assets_manager_cubit_test.dart new file mode 100644 index 00000000..d0afee34 --- /dev/null +++ b/test/game/assets_manager/cubit/assets_manager_cubit_test.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.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/game/assets_manager/cubit/assets_manager_state_test.dart b/test/game/assets_manager/cubit/assets_manager_state_test.dart new file mode 100644 index 00000000..12a42485 --- /dev/null +++ b/test/game/assets_manager/cubit/assets_manager_state_test.dart @@ -0,0 +1,145 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + group('AssetsManagerState', () { + test('can be instantiated', () { + expect( + AssetsManagerState(loadables: const [], loaded: const []), + isNotNull, + ); + }); + + test('has the correct initial state', () { + final future = Future.value(); + expect( + AssetsManagerState.initial(loadables: [future]), + equals( + AssetsManagerState( + loadables: [future], + loaded: const [], + ), + ), + ); + }); + + group('progress', () { + final future1 = Future.value(); + final future2 = Future.value(); + + test('returns 0 when no future is loaded', () { + expect( + AssetsManagerState( + loadables: [future1, future2], + loaded: const [], + ).progress, + equals(0), + ); + }); + + test('returns the correct value when some of the futures are loaded', () { + expect( + AssetsManagerState( + loadables: [future1, future2], + loaded: [future1], + ).progress, + equals(0.5), + ); + }); + + test('returns the 1 when all futures are loaded', () { + expect( + AssetsManagerState( + loadables: [future1, future2], + loaded: [future1, future2], + ).progress, + equals(1), + ); + }); + }); + + group('copyWith', () { + final future = Future.value(); + + test('returns a copy with the updated loadables', () { + expect( + AssetsManagerState( + loadables: const [], + loaded: const [], + ).copyWith(loadables: [future]), + equals( + AssetsManagerState( + loadables: [future], + loaded: const [], + ), + ), + ); + }); + + test('returns a copy with the updated loaded', () { + expect( + AssetsManagerState( + loadables: const [], + loaded: const [], + ).copyWith(loaded: [future]), + equals( + AssetsManagerState( + loadables: const [], + loaded: [future], + ), + ), + ); + }); + }); + + test('supports value comparison', () { + final future1 = Future.value(); + final future2 = Future.value(); + + expect( + AssetsManagerState( + loadables: const [], + loaded: const [], + ), + equals( + AssetsManagerState( + loadables: const [], + loaded: const [], + ), + ), + ); + + expect( + AssetsManagerState( + loadables: [future1], + loaded: const [], + ), + isNot( + equals( + AssetsManagerState( + loadables: [future2], + loaded: const [], + ), + ), + ), + ); + + expect( + AssetsManagerState( + loadables: const [], + loaded: [future1], + ), + isNot( + equals( + AssetsManagerState( + loadables: const [], + loaded: [future2], + ), + ), + ), + ); + }); + }); +} diff --git a/test/game/components/camera_controller_test.dart b/test/game/components/camera_controller_test.dart new file mode 100644 index 00000000..6af3f594 --- /dev/null +++ b/test/game/components/camera_controller_test.dart @@ -0,0 +1,85 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/components/camera_controller.dart'; +import 'package:pinball_components/pinball_components.dart'; + +void main() { + group('CameraController', () { + late FlameGame game; + late CameraController controller; + + setUp(() async { + game = FlameGame()..onGameResize(Vector2(100, 200)); + + controller = CameraController(game); + await game.ensureAdd(controller); + }); + + test('loads correctly', () async { + expect(game.firstChild(), isNotNull); + }); + + test('correctly calculates the zooms', () async { + expect(controller.gameFocus.zoom.toInt(), equals(12)); + expect(controller.backboardFocus.zoom.toInt(), equals(11)); + }); + + test('correctly sets the initial zoom and position', () async { + expect(game.camera.zoom, equals(controller.backboardFocus.zoom)); + expect(game.camera.follow, equals(controller.backboardFocus.position)); + }); + + group('focusOnBoard', () { + test('changes the zoom', () async { + controller.focusOnGame(); + + await game.ready(); + final zoom = game.firstChild(); + expect(zoom, isNotNull); + expect(zoom?.value, equals(controller.gameFocus.zoom)); + }); + + test('moves the camera after the zoom is completed', () async { + controller.focusOnGame(); + await game.ready(); + final cameraZoom = game.firstChild()!; + final future = cameraZoom.completed; + + game.update(10); + game.update(0); // Ensure that the component was removed + + await future; + + expect(game.camera.position, Vector2(-4, -108.8)); + }); + }); + + group('focusOnBackboard', () { + test('changes the zoom', () async { + controller.focusOnBackboard(); + + await game.ready(); + final zoom = game.firstChild(); + expect(zoom, isNotNull); + expect(zoom?.value, equals(controller.backboardFocus.zoom)); + }); + + test('moves the camera after the zoom is completed', () async { + controller.focusOnBackboard(); + await game.ready(); + final cameraZoom = game.firstChild()!; + final future = cameraZoom.completed; + + game.update(10); + game.update(0); // Ensure that the component was removed + + await future; + + expect(game.camera.position, Vector2(-4.5, -109.8)); + }); + }); + }); +} diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart new file mode 100644 index 00000000..dc1d9ab8 --- /dev/null +++ b/test/game/components/game_flow_controller_test.dart @@ -0,0 +1,88 @@ +// ignore_for_file: type_annotate_public_apis, prefer_const_constructors + +import 'package:flame/game.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('GameFlowController', () { + group('listenWhen', () { + test('is true when the game over state has changed', () { + final state = GameState( + score: 10, + balls: 0, + activatedBonusLetters: const [], + bonusHistory: const [], + activatedDashNests: const {}, + ); + + final previous = GameState.initial(); + expect( + GameFlowController(MockPinballGame()).listenWhen(previous, state), + isTrue, + ); + }); + }); + + group('onNewState', () { + late PinballGame game; + late Backboard backboard; + late CameraController cameraController; + late GameFlowController gameFlowController; + late ActiveOverlaysNotifier overlays; + + setUp(() { + game = MockPinballGame(); + backboard = MockBackboard(); + cameraController = MockCameraController(); + gameFlowController = GameFlowController(game); + overlays = MockActiveOverlaysNotifier(); + + when(backboard.gameOverMode).thenAnswer((_) async {}); + when(backboard.waitingMode).thenAnswer((_) async {}); + when(cameraController.focusOnBackboard).thenAnswer((_) async {}); + when(cameraController.focusOnGame).thenAnswer((_) async {}); + + when(() => overlays.remove(any())).thenAnswer((_) => true); + + when(game.firstChild).thenReturn(backboard); + when(game.firstChild).thenReturn(cameraController); + when(() => game.overlays).thenReturn(overlays); + }); + + test( + 'changes the backboard and camera correctly when it is a game over', + () { + gameFlowController.onNewState( + GameState( + score: 10, + balls: 0, + activatedBonusLetters: const [], + bonusHistory: const [], + activatedDashNests: const {}, + ), + ); + + verify(backboard.gameOverMode).called(1); + verify(cameraController.focusOnBackboard).called(1); + }, + ); + + test( + 'changes the backboard and camera correctly when it is not a game over', + () { + gameFlowController.onNewState(GameState.initial()); + + verify(backboard.waitingMode).called(1); + verify(cameraController.focusOnGame).called(1); + verify(() => overlays.remove(PinballGame.playButtonOverlay)) + .called(1); + }, + ); + }); + }); +} diff --git a/test/game/components/sparky_fire_zone_test.dart b/test/game/components/sparky_fire_zone_test.dart new file mode 100644 index 00000000..dceaa9cc --- /dev/null +++ b/test/game/components/sparky_fire_zone_test.dart @@ -0,0 +1,45 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(EmptyPinballGameTest.new); + + group('SparkyFireZone', () { + group('bumpers', () { + late ControlledSparkyBumper controlledSparkyBumper; + + flameTester.testGameWidget( + 'activate when deactivated bumper is hit', + setUp: (game, tester) async { + controlledSparkyBumper = ControlledSparkyBumper(); + await game.ensureAdd(controlledSparkyBumper); + + controlledSparkyBumper.controller.hit(); + }, + verify: (game, tester) async { + expect(controlledSparkyBumper.controller.isActivated, isTrue); + }, + ); + + flameTester.testGameWidget( + 'deactivate when activated bumper is hit', + setUp: (game, tester) async { + controlledSparkyBumper = ControlledSparkyBumper(); + await game.ensureAdd(controlledSparkyBumper); + + controlledSparkyBumper.controller.hit(); + controlledSparkyBumper.controller.hit(); + }, + verify: (game, tester) async { + expect(controlledSparkyBumper.controller.isActivated, isFalse); + }, + ); + }); + }); +} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index f16b8ef1..7a1419fb 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -11,6 +11,7 @@ import '../../helpers/helpers.dart'; void main() { const theme = PinballTheme(characterTheme: DashTheme()); + final game = PinballGameTest(); group('PinballGamePage', () { testWidgets('renders PinballGameView', (tester) async { @@ -22,113 +23,112 @@ void main() { ); await tester.pumpApp( - PinballGamePage(theme: theme), + PinballGamePage(theme: theme, game: game), gameBloc: gameBloc, ); expect(find.byType(PinballGameView), findsOneWidget); }); - testWidgets('route returns a valid navigation route', (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context) - .push(PinballGamePage.route(theme: theme)); - }, - child: const Text('Tap me'), - ); - }, - ), - ), - ); - - 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 - - expect(find.byType(PinballGamePage), findsOneWidget); - }); - }); - - group('PinballGameView', () { - testWidgets('renders game and a hud', (tester) async { - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(const GameState.initial()), - initialState: const GameState.initial(), - ); - - await tester.pumpApp( - PinballGameView(theme: theme), - gameBloc: gameBloc, - ); - - expect( - find.byWidgetPredicate((w) => w is GameWidget), - findsOneWidget, - ); - expect( - find.byType(GameHud), - findsOneWidget, - ); - }); - testWidgets( - 'renders a game over dialog when the user has lost', + 'renders the loading indicator while the assets load', (tester) async { final gameBloc = MockGameBloc(); - const state = GameState( - score: 0, - balls: 0, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), ); + final assetsManagerCubit = MockAssetsManagerCubit(); + final initialAssetsState = AssetsManagerState( + loadables: [Future.value()], + loaded: const [], + ); whenListen( - gameBloc, - Stream.value(state), - initialState: GameState.initial(), + assetsManagerCubit, + Stream.value(initialAssetsState), + initialState: initialAssetsState, ); await tester.pumpApp( - const PinballGameView(theme: theme), + PinballGamePage(theme: theme, game: game), gameBloc: gameBloc, + assetsManagerCubit: assetsManagerCubit, ); - await tester.pump(); + expect(find.text('0.0'), findsOneWidget); - expect(find.byType(GameOverDialog), findsOneWidget); + final loadedAssetsState = AssetsManagerState( + loadables: [Future.value()], + loaded: [Future.value()], + ); + whenListen( + assetsManagerCubit, + Stream.value(loadedAssetsState), + initialState: loadedAssetsState, + ); + + await tester.pump(); + expect(find.byType(PinballGameView), findsOneWidget); }, ); - testWidgets('renders the real game when not in debug mode', (tester) async { - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(const GameState.initial()), - initialState: const GameState.initial(), - ); + 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( + theme: theme, + isDebugMode: isDebugMode, + ), + ); + }, + child: const Text('Tap me'), + ); + }, + ), + ), + ); - await tester.pumpApp( - const PinballGameView(theme: theme, isDebugMode: false), - gameBloc: gameBloc, - ); - expect( - find.byWidgetPredicate( - (w) => w is GameWidget && w.game is! DebugPinballGame, - ), - findsOneWidget, - ); + 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, + ); + }); }); + }); - testWidgets('renders the debug game when on debug mode', (tester) async { + group('PinballGameView', () { + testWidgets('renders game and a hud', (tester) async { final gameBloc = MockGameBloc(); whenListen( gameBloc, @@ -137,13 +137,16 @@ void main() { ); await tester.pumpApp( - const PinballGameView(theme: theme), + PinballGameView(game: game), gameBloc: gameBloc, ); + expect( - find.byWidgetPredicate( - (w) => w is GameWidget && w.game is DebugPinballGame, - ), + find.byWidgetPredicate((w) => w is GameWidget), + findsOneWidget, + ); + expect( + find.byType(GameHud), findsOneWidget, ); }); diff --git a/test/game/view/play_button_overlay_test.dart b/test/game/view/play_button_overlay_test.dart new file mode 100644 index 00000000..020998d4 --- /dev/null +++ b/test/game/view/play_button_overlay_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('PlayButtonOverlay', () { + late PinballGame game; + late GameFlowController gameFlowController; + + setUp(() { + game = MockPinballGame(); + gameFlowController = MockGameFlowController(); + + when(() => game.gameFlowController).thenReturn(gameFlowController); + when(gameFlowController.start).thenAnswer((_) {}); + }); + + testWidgets('renders correctly', (tester) async { + await tester.pumpApp(PlayButtonOverlay(game: game)); + + expect(find.text('Play'), findsOneWidget); + }); + + testWidgets('calls gameFlowController.start when taped', (tester) async { + await tester.pumpApp(PlayButtonOverlay(game: game)); + + await tester.tap(find.text('Play')); + await tester.pump(); + + verify(gameFlowController.start).called(1); + }); + }); +} diff --git a/test/game/view/widgets/game_over_dialog_test.dart b/test/game/view/widgets/game_over_dialog_test.dart deleted file mode 100644 index 814a7a45..00000000 --- a/test/game/view/widgets/game_over_dialog_test.dart +++ /dev/null @@ -1,195 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'dart:async'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/leaderboard/leaderboard.dart'; -import 'package:pinball_theme/pinball_theme.dart'; - -import '../../../helpers/helpers.dart'; - -void main() { - group('GameOverDialog', () { - testWidgets('renders GameOverDialogView', (tester) async { - await tester.pumpApp( - GameOverDialog( - score: 1000, - theme: DashTheme(), - ), - ); - - expect(find.byType(GameOverDialogView), findsOneWidget); - }); - - group('GameOverDialogView', () { - late LeaderboardBloc leaderboardBloc; - - final leaderboard = [ - LeaderboardEntry( - rank: '1', - playerInitials: 'ABC', - score: 5000, - character: DashTheme().characterAsset, - ), - ]; - final entryData = LeaderboardEntryData( - playerInitials: 'VGV', - score: 10000, - character: CharacterType.dash, - ); - - setUp(() { - leaderboardBloc = MockLeaderboardBloc(); - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: const LeaderboardState.initial(), - ); - }); - - testWidgets('renders input text view when bloc emits [loading]', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: GameOverDialogView( - score: entryData.score, - theme: entryData.character.toTheme, - ), - ), - ); - - expect(find.widgetWithText(TextButton, l10n.addUser), findsOneWidget); - }); - - testWidgets('renders error view when bloc emits [error]', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial() - .copyWith(status: LeaderboardStatus.error), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: GameOverDialogView( - score: entryData.score, - theme: entryData.character.toTheme, - ), - ), - ); - - expect(find.text(l10n.error), findsOneWidget); - }); - - testWidgets('renders success view when bloc emits [success]', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState( - status: LeaderboardStatus.success, - ranking: LeaderboardRanking(ranking: 1, outOf: 2), - leaderboard: leaderboard, - ), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: GameOverDialogView( - score: entryData.score, - theme: entryData.character.toTheme, - ), - ), - ); - - expect( - find.widgetWithText(TextButton, l10n.leaderboard), - findsOneWidget, - ); - }); - - testWidgets('adds LeaderboardEntryAdded when tap on add user button', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState.initial(), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: GameOverDialogView( - score: entryData.score, - theme: entryData.character.toTheme, - ), - ), - ); - - await tester.enterText( - find.byKey(const Key('player_initials_text_field')), - entryData.playerInitials, - ); - - final button = find.widgetWithText(TextButton, l10n.addUser); - await tester.ensureVisible(button); - await tester.tap(button); - - verify( - () => leaderboardBloc.add(LeaderboardEntryAdded(entry: entryData)), - ).called(1); - }); - - testWidgets('navigates to LeaderboardPage when tap on leaderboard button', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); - whenListen( - leaderboardBloc, - const Stream.empty(), - initialState: LeaderboardState( - status: LeaderboardStatus.success, - ranking: LeaderboardRanking(ranking: 1, outOf: 2), - leaderboard: leaderboard, - ), - ); - - await tester.pumpApp( - BlocProvider.value( - value: leaderboardBloc, - child: GameOverDialogView( - score: entryData.score, - theme: entryData.character.toTheme, - ), - ), - navigator: navigator, - ); - - final button = find.widgetWithText(TextButton, l10n.leaderboard); - await tester.ensureVisible(button); - await tester.tap(button); - - verify(() => navigator.push(any())).called(1); - }); - }); - }); -} diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 748b48f3..941da872 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -1,4 +1,5 @@ import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/foundation.dart'; @@ -74,3 +75,14 @@ class MockComponentSet extends Mock implements ComponentSet {} class MockDashNestBumper extends Mock implements DashNestBumper {} class MockPinballAudio extends Mock implements PinballAudio {} + +class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} + +class MockBackboard extends Mock implements Backboard {} + +class MockCameraController extends Mock implements CameraController {} + +class MockActiveOverlaysNotifier extends Mock + implements ActiveOverlaysNotifier {} + +class MockGameFlowController extends Mock implements GameFlowController {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 722dc44c..92e2c042 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -5,6 +5,7 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -26,11 +27,31 @@ PinballAudio _buildDefaultPinballAudio() { return audio; } +MockAssetsManagerCubit _buildDefaultAssetsManagerCubit() { + final cubit = MockAssetsManagerCubit(); + + final state = AssetsManagerState( + loadables: [Future.value()], + loaded: [ + Future.value(), + ], + ); + + whenListen( + cubit, + Stream.value(state), + initialState: state, + ); + + return cubit; +} + extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { MockNavigator? navigator, GameBloc? gameBloc, + AssetsManagerCubit? assetsManagerCubit, ThemeCubit? themeCubit, LeaderboardRepository? leaderboardRepository, PinballAudio? pinballAudio, @@ -54,6 +75,9 @@ extension PumpApp on WidgetTester { BlocProvider.value( value: gameBloc ?? MockGameBloc(), ), + BlocProvider.value( + value: assetsManagerCubit ?? _buildDefaultAssetsManagerCubit(), + ), ], child: MaterialApp( localizationsDelegates: const [