diff --git a/lib/game/components/camera_controller.dart b/lib/game/components/camera_controller.dart new file mode 100644 index 00000000..a4dc34d8 --- /dev/null +++ b/lib/game/components/camera_controller.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// A [Component] that controls its game camera focus +class CameraController extends Component with HasGameRef, KeyboardHandler { + /// The camera position for the board + static final gamePosition = Vector2(0, -7.8); + + /// The camera position for the pinball panel + static final backboardPosition = Vector2(0, -100.8); + + /// The zoom value for the game mode + late final double gameZoom; + + /// The zoom value for the panel mode + late final double backboardZoom; + + @override + Future onLoad() async { + await super.onLoad(); + + gameZoom = gameRef.size.y / 16; + backboardZoom = gameRef.size.y / 18; + + // Game starts with the camera focused on the panel + gameRef.camera + ..speed = 100 + ..followVector2(backboardPosition) + ..zoom = backboardZoom; + } + + /// Move the camera focus to the game board + Future focusOnGame() async { + final zoom = CameraZoom(value: gameZoom); + unawaited(gameRef.add(zoom)); + await zoom.completed; + gameRef.camera.moveTo(gamePosition); + } + + /// Move the camera focus to the backboard + Future focusOnBackboard() async { + final zoom = CameraZoom(value: backboardZoom); + unawaited(gameRef.add(zoom)); + await zoom.completed; + gameRef.camera.moveTo(backboardPosition); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 9dd7eed4..fe2e610f 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,8 +1,10 @@ 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_controller.dart'; export 'plunger.dart'; export 'score_points.dart'; export 'sparky_fire_zone.dart'; diff --git a/lib/game/components/game_controller.dart b/lib/game/components/game_controller.dart new file mode 100644 index 00000000..d7e3186f --- /dev/null +++ b/lib/game/components/game_controller.dart @@ -0,0 +1,35 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +/// A [Component] that controls the game over and game restart logic +class GameController extends Component + with BlocComponent, HasGameRef { + @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() { + gameRef.firstChild()?.gameOverMode(); + gameRef.firstChild()?.focusOnBackboard(); + } + + /// Puts the game on a playing state + void start() { + gameRef.firstChild()?.waitingMode(); + gameRef.firstChild()?.focusOnGame(); + gameRef.overlays.remove(PinballGame.playButtonOverlay); + } +} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 2ccf8fe8..705b5eee 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 GameController gameController; + @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(gameController = GameController())); + unawaited(add(CameraController())); + 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 b1f031c7..ba131a1c 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -51,52 +52,18 @@ class PinballGamePage extends StatelessWidget { @override Widget build(BuildContext context) { - return PinballGameView(theme: theme, game: game); + return PinballGameView(game: game); } } class PinballGameView extends StatelessWidget { const PinballGameView({ Key? key, - required this.theme, required this.game, }) : super(key: key); - final PinballTheme theme; final PinballGame game; - @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: theme.characterTheme, - ); - }, - ); - } - }, - child: _GameView(game: game), - ); - } -} - -class _GameView extends StatelessWidget { - const _GameView({ - Key? key, - required PinballGame game, - }) : _game = game, - super(key: key); - - final PinballGame _game; - @override Widget build(BuildContext context) { final loadingProgress = context.watch().state.progress; @@ -114,7 +81,20 @@ class _GameView extends StatelessWidget { return Stack( children: [ Positioned.fill( - child: GameWidget(game: _game), + 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), + ); + }, + }, + ), ), const Positioned( top: 8, 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..771e1c32 --- /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.gameController.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/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..6a4a3bcf --- /dev/null +++ b/packages/pinball_components/lib/src/components/backboard.dart @@ -0,0 +1,36 @@ +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( + position: position..clone().multiply(Vector2(1, -1)), + anchor: Anchor.bottomCenter, + ); + + @override + Future onLoad() async { + await waitingMode(); + } + + /// Sets the Backboard on the waiting mode, where the scoreboard is show + Future waitingMode() async { + size = Vector2(120, 100); + sprite = await gameRef.loadSprite( + Assets.images.backboard.backboardScores.keyName, + ); + } + + /// Sets the Backboard on the game over mode, where the score input is show + Future gameOverMode() async { + size = Vector2(100, 100); + sprite = await gameRef.loadSprite( + Assets.images.backboard.backboardGameOver.keyName, + ); + } +} 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/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/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/components/camera_controller_test.dart b/test/game/components/camera_controller_test.dart new file mode 100644 index 00000000..3ad17a88 --- /dev/null +++ b/test/game/components/camera_controller_test.dart @@ -0,0 +1,83 @@ +// ignore_for_file: cascade_invocations + +import 'dart:async'; + +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(); + await game.ensureAdd(controller); + }); + + test('loads correctly', () async { + expect(game.firstChild(), isNotNull); + }); + + test('correctly calculates the zooms', () async { + expect(controller.gameZoom.toInt(), equals(12)); + expect(controller.backboardZoom.toInt(), equals(11)); + }); + + test('correctly sets the initial zoom and position', () async { + expect(game.camera.zoom, equals(controller.backboardZoom)); + expect(game.camera.follow, equals(CameraController.backboardPosition)); + }); + + group('focusOnBoard', () { + test('changes the zoom', () async { + unawaited(controller.focusOnGame()); + + await game.ready(); + final zoom = game.firstChild(); + expect(zoom, isNotNull); + expect(zoom?.value, equals(controller.gameZoom)); + }); + + test('moves the camera after the zoom is completed', () async { + final future = controller.focusOnGame(); + await game.ready(); + + game.update(10); + game.update(0); + + await future; + + expect(game.camera.position, Vector2(-4, -108.8)); + }); + }); + + group('focusOnBackboard', () { + test('changes the zoom', () async { + unawaited(controller.focusOnBackboard()); + + await game.ready(); + final zoom = game.firstChild(); + expect(zoom, isNotNull); + expect(zoom?.value, equals(controller.backboardZoom)); + }); + + test('moves the camera after the zoom is completed', () async { + final future = controller.focusOnBackboard(); + await game.ready(); + + game.update(10); + game.update(0); + + await future; + + expect(game.camera.position, Vector2(-4.5, -109.8)); + }); + }); + }); +} diff --git a/test/game/components/game_controller_test.dart b/test/game/components/game_controller_test.dart new file mode 100644 index 00000000..b29b3e2d --- /dev/null +++ b/test/game/components/game_controller_test.dart @@ -0,0 +1,99 @@ +// 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'; + +// TODO(erickzanardo: This will not be needed anymore when +// this issue is merged: https://github.com/flame-engine/flame/issues/1513 +class WrappedGameController extends GameController { + WrappedGameController(this._gameRef); + + final PinballGame _gameRef; + + @override + PinballGame get gameRef => _gameRef; +} + +void main() { + group('GameController', () { + 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( + GameController().listenWhen(previous, state), + isTrue, + ); + }); + }); + + group('onNewState', () { + late PinballGame game; + late Backboard backboard; + late CameraController cameraController; + late GameController gameController; + late ActiveOverlaysNotifier overlays; + + setUp(() { + game = MockPinballGame(); + backboard = MockBackboard(); + cameraController = MockCameraController(); + gameController = WrappedGameController(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', + () { + gameController.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', + () { + gameController.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/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 683b53e8..97728e3a 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -137,7 +137,7 @@ void main() { ); await tester.pumpApp( - PinballGameView(theme: theme, game: game), + PinballGameView(game: game), gameBloc: gameBloc, ); @@ -151,32 +151,5 @@ void main() { ); }); - testWidgets( - 'renders a game over dialog when the user has lost', - (tester) async { - final gameBloc = MockGameBloc(); - const state = GameState( - score: 0, - balls: 0, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ); - - whenListen( - gameBloc, - Stream.value(state), - initialState: GameState.initial(), - ); - - await tester.pumpApp( - PinballGameView(theme: theme, game: game), - gameBloc: gameBloc, - ); - await tester.pump(); - - expect(find.byType(GameOverDialog), 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..f07e4dc8 --- /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 GameController gameController; + + setUp(() { + game = MockPinballGame(); + gameController = MockGameController(); + + when(() => game.gameController).thenReturn(gameController); + when(gameController.start).thenAnswer((_) {}); + }); + + testWidgets('renders correctly', (tester) async { + await tester.pumpApp(PlayButtonOverlay(game: game)); + + expect(find.text('Play'), findsOneWidget); + }); + + testWidgets('calls gameController.start when taped', (tester) async { + await tester.pumpApp(PlayButtonOverlay(game: game)); + + await tester.tap(find.text('Play')); + await tester.pump(); + + verify(gameController.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 2da91a25..3d63ba8e 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'; @@ -76,3 +77,12 @@ 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 MockGameController extends Mock implements GameController {}