feat: implementing the new game start flow (#160)

* feat: implementing the new game start flow

* fix: coverage

* fix: lint

* fix: lint

* Apply suggestions from code review

Co-authored-by: Alejandro Santiago <dev@alestiago.com>
Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* feat: PR suggestions

* feat: improve test docs

* feaT: pr suggestions

* feat: adding backboard assets to the pre fetch

* feat: pr suggestions

* feat: pr suggestions

* fix: lint

* fix: ci

* fix: tests

Co-authored-by: Alejandro Santiago <dev@alestiago.com>
Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
pull/171/head
Erick 2 years ago committed by GitHub
parent 31afa08fc1
commit bfba65823f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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<FlameGame> {
/// {@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));
}
}

@ -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_flow_controller.dart';
export 'plunger.dart';
export 'score_points.dart';
export 'sparky_fire_zone.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<PinballGame>
with BlocComponent<GameBloc, GameState> {
/// {@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<Backboard>()?.gameOverMode();
component.firstChild<CameraController>()?.focusOnBackboard();
}
/// Puts the game on a playing state
void start() {
component.firstChild<Backboard>()?.waitingMode();
component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay);
}
}

@ -46,6 +46,8 @@ 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),
];
}

@ -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<void> 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()));

@ -51,52 +51,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<GameBloc, GameState>(
listenWhen: (previous, current) =>
previous.isGameOver != current.isGameOver,
listener: (context, state) {
if (state.isGameOver) {
showDialog<void>(
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<AssetsManagerCubit>().state.progress;
@ -114,7 +80,20 @@ class _GameView extends StatelessWidget {
return Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(game: _game),
child: GameWidget<PinballGame>(
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,

@ -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<LeaderboardRepository>(),
),
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<GameOverDialogView> createState() => _GameOverDialogViewState();
}
class _GameOverDialogViewState extends State<GameOverDialogView> {
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<LeaderboardBloc, LeaderboardState>(
builder: (context, state) {
switch (state.status) {
case LeaderboardStatus.loading:
return TextButton(
onPressed: () {
context.read<LeaderboardBloc>().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<void>(
LeaderboardPage.route(theme: theme),
),
child: Text(l10n.leaderboard),
);
case LeaderboardStatus.error:
return Text(l10n.error);
}
},
);
}
}

@ -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),
),
);
}
}

@ -1,2 +1,2 @@
export 'game_hud.dart';
export 'game_over_dialog.dart';
export 'play_button_overlay.dart';

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

@ -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');
}

@ -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<void> onLoad() async {
await waitingMode();
}
/// Puts the Backboard in waiting mode, where the scoreboard is shown.
Future<void> 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<void> gameOverMode() async {
final sprite = await gameRef.loadSprite(
Assets.images.backboard.backboardGameOver.keyName,
);
size = sprite.originalSize / 10;
this.sprite = sprite;
}
}

@ -1,3 +1,4 @@
export 'backboard.dart';
export 'ball.dart';
export 'baseboard.dart';
export 'board_dimensions.dart';

@ -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

@ -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<TestGame>(),
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<TestGame>(),
matchesGoldenFile('golden/backboard/game_over.png'),
);
},
);
});
});
}

@ -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));
},
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 KiB

@ -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:

@ -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:

@ -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<CameraController>(), 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<CameraZoom>();
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<CameraZoom>()!;
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<CameraZoom>();
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<CameraZoom>()!;
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));
});
});
});
}

@ -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<Backboard>).thenReturn(backboard);
when(game.firstChild<CameraController>).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);
},
);
});
});
}

@ -137,7 +137,7 @@ void main() {
);
await tester.pumpApp(
PinballGameView(theme: theme, game: game),
PinballGameView(game: game),
gameBloc: gameBloc,
);
@ -150,33 +150,5 @@ void main() {
findsOneWidget,
);
});
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);
},
);
});
}

@ -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);
});
});
}

@ -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<LeaderboardState>.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<LeaderboardState>.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<LeaderboardState>.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<LeaderboardState>.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<void>(any())).thenAnswer((_) async {});
whenListen(
leaderboardBloc,
const Stream<LeaderboardState>.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<void>(any())).called(1);
});
});
});
}

@ -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 MockGameFlowController extends Mock implements GameFlowController {}

Loading…
Cancel
Save