fix: fixed merge conflicts with trace sandbox and spritecomponents at ramps

pull/168/head
RuiAlonso 4 years ago
commit a9d8127355

@ -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,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<AssetsManagerState> {
/// {@macro assets_manager_cubit}
AssetsManagerCubit(List<Future> loadables)
: super(
AssetsManagerState.initial(
loadables: loadables,
),
);
/// Loads the assets
Future<void> load() async {
final all = state.loadables.map((loadable) async {
await loadable;
emit(state.copyWith(loaded: [...state.loaded, loadable]));
}).toList();
await Future.wait(all);
}
}

@ -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<Future> loadables,
}) : this(loadables: loadables, loaded: const []);
/// List of futures to load
final List<Future> loadables;
/// List of loaded futures
final List<Future> 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<Future>? loadables,
List<Future>? loaded,
}) {
return AssetsManagerState(
loadables: loadables ?? this.loadables,
loaded: loaded ?? this.loaded,
);
}
@override
List<Object> get props => [loaded, loadables];
}

@ -12,11 +12,14 @@ class Board extends Component {
@override
Future<void> 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,

@ -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,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';

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

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

@ -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<SparkyBumper>
with HasGameRef<PinballGame> {
/// {@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;
}
}

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

@ -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<void> preLoadAssets() async {
await Future.wait([
/// Returns a list of assets to be loaded
List<Future> 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),
]);
];
}
}

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

@ -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<void>(
builder: (_) {
return BlocProvider(
create: (_) => GameBloc(),
child: PinballGamePage(theme: theme),
builder: (context) {
final audio = context.read<PinballAudio>();
final game = isDebugMode
? DebugPinballGame(theme: theme, audio: audio)
: PinballGame(theme: theme, audio: audio);
final pinballAudio = context.read<PinballAudio>();
final loadables = [
...game.preLoadAssets(),
pinballAudio.load(),
];
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => GameBloc()),
BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
),
],
child: PinballGamePage(theme: theme, game: game),
);
},
);
@ -26,74 +51,49 @@ 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;
required this.game,
}) : super(key: key);
@override
State<PinballGameView> createState() => _PinballGameViewState();
}
class _PinballGameViewState extends State<PinballGameView> {
late PinballGame _game;
final PinballGame game;
@override
void initState() {
super.initState();
final audio = context.read<PinballAudio>();
_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<AssetsManagerCubit>().state.progress;
Future<void> _fetchAssets() async {
final pinballAudio = context.read<PinballAudio>();
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<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: widget.theme.characterTheme,
return Stack(
children: [
Positioned.fill(
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),
);
},
);
}
},
child: Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(game: _game),
),
),
const Positioned(
top: 8,
@ -101,7 +101,6 @@ class _PinballGameViewState extends State<PinballGameView> {
child: GameHud(),
),
],
),
);
}
}

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 18 KiB

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

@ -22,36 +22,31 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
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<void> 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<T extends Forge2DGame> extends BodyComponent<T>
/// 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<T extends Forge2DGame> extends BodyComponent<T>
_spriteComponent.scale = Vector2.all(scaleFactor);
}
}
class _BallSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
Assets.images.ball.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
}
}

@ -82,23 +82,8 @@ class Baseboard extends BodyComponent with InitialPosition {
@override
Future<void> 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<void> 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;
}
}

@ -30,9 +30,9 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
final bottomLeftCurve = BezierCurveShape(
controlPoints: [
Vector2(-43.6, -44.4),
Vector2(-31, -43.4),
Vector2(-18.7, -52.1),
Vector2(-43.9, -41.8),
Vector2(-35.7, -43),
Vector2(-19.9, -51),
],
);
final bottomLeftCurveFixtureDef = FixtureDef(bottomLeftCurve);
@ -40,9 +40,9 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
final bottomRightCurve = BezierCurveShape(
controlPoints: [
Vector2(31.8, -44.1),
Vector2(21.95, -47),
Vector2(12.3, -51.4),
Vector2(31.8, -44.8),
Vector2(21.95, -47.7),
Vector2(12.3, -52.1),
],
);
final bottomRightCurveFixtureDef = FixtureDef(bottomRightCurve);
@ -63,23 +63,22 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_BottomBoundarySpriteComponent());
}
}
Future<void> _loadSprite() async {
class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> 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, 55.8);
}
}
@ -115,7 +114,7 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
final leftWall = EdgeShape()
..set(
Vector2(-32.3, 57.2),
Vector2(-44.1, -44.4),
Vector2(-43.9, -41.8),
);
final leftWallFixtureDef = FixtureDef(leftWall);
fixturesDefs.add(leftWallFixtureDef);
@ -135,22 +134,21 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_OuterBoundarySpriteComponent());
}
}
Future<void> _loadSprite() async {
class _OuterBoundarySpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> 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);
}
}

@ -1,3 +1,4 @@
export 'backboard.dart';
export 'ball.dart';
export 'baseboard.dart';
export 'board_dimensions.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<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_DinoTopWallSpriteComponent());
}
}
Future<void> _loadSprite() async {
class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> 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<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_DinoBottomWallSpriteComponent());
}
}
Future<void> _loadSprite() async {
class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> 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);
}
}

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

@ -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 Flutter Forest.
/// {@endtemplate}
// TODO(alestiago): Revisit doc comment if FlutterForest is moved to package.
class FlutterSignPost extends BodyComponent with InitialPosition {
Future<void> _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<void> 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<void> 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);
}
}

@ -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<FixtureDef> _createFixtureDefs() {
final fixturesDefs = <FixtureDef>[];
final direction = _side.direction;
@ -122,21 +122,28 @@ class Kicker extends BodyComponent with InitialPosition {
Future<void> 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<void> 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);
}
}

@ -122,22 +122,24 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_LaunchRampBaseSpriteComponent());
}
}
Future<void> _loadSprite() async {
class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
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);
}
}
@ -202,22 +204,25 @@ class _LaunchRampForegroundRailing extends BodyComponent
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
renderBody = false;
await add(_LaunchRampForegroundRailingSpriteComponent());
}
}
Future<void> _loadSprite() async {
class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
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);
}
}

@ -102,27 +102,9 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered {
@override
Future<void> 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<void> 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 {

@ -139,21 +139,23 @@ class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered {
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprite();
await add(_SpaceshipRailRampSpriteComponent());
}
}
class _SpaceshipRailRampSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
Future<void> _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);
}
}

@ -110,35 +110,40 @@ class _SpaceshipRampBackground extends BodyComponent
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
}
Future<void> _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(-12.2, -53.5),
);
await add(_SpaceshipRampBackgroundRailingSpriteComponent());
await add(_SpaceshipRampBackgroundRampSpriteComponent());
}
}
final spriteRailingBg = await gameRef.loadSprite(
class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> 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: Vector2(-12.2, -54.5),
);
this.sprite = sprite;
size = Vector2(38.3, 35.1);
anchor = Anchor.center;
position = Vector2(-12.2, -54.5);
}
}
await addAll([
spriteRailingBgComponent,
spriteRampComponent,
]);
class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> 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(-12.2, -53.5);
}
}
@ -194,21 +199,22 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
await add(_SpaceshipRampForegroundRalingSpriteComponent());
}
}
Future<void> _loadSprites() async {
final spriteRailingFg = await gameRef.loadSprite(
class _SpaceshipRampForegroundRalingSpriteComponent extends SpriteComponent
with HasGameRef {
@override
Future<void> 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.2, -52.5),
);
await add(spriteRailingFgComponent);
this.sprite = sprite;
size = Vector2(26.1, 28.3);
anchor = Anchor.center;
position = Vector2(-12.2, -52.5);
}
}

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

@ -12,8 +12,7 @@ extension BodyTrace on BodyComponent {
unawaited(
mounted.whenComplete(() {
final sprite = children.whereType<SpriteComponent>().first;
sprite.paint.color = sprite.paint.color.withOpacity(0.5);
children.whereType<SpriteComponent>().first.setOpacity(0.5);
descendants().whereType<JointAnchor>().forEach((anchor) {
final fixtureDef = FixtureDef(CircleShape()..radius = 0.5);

@ -24,6 +24,7 @@ void main() {
addSlingshotStories(dashbook);
addSparkyBumperStories(dashbook);
addZoomStories(dashbook);
addBoundariesStories(dashbook);
addSpaceshipRampStories(dashbook);
addSpaceshipRailStories(dashbook);
addLaunchRampStories(dashbook);

@ -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,
this.ballPriority = 0,
@ -28,5 +28,6 @@ class BasicBallGame extends BasicGame with TapDetector {
..layer = ballLayer
..priority = ballPriority,
);
traceAllBodies();
}
}

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

@ -0,0 +1,29 @@
import 'package:flame/extensions.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class BoundariesGame extends BasicBallGame with Traceable {
BoundariesGame() : super(color: const Color(0xFFFF0000));
static const info = '''
Shows how Boundaries are rendered.
- Activate the "trace" parameter to overlay the body.
- Tap anywhere on the screen to spawn a ball into the game.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
await addFromBlueprint(Boundaries());
await ready();
camera
..followVector2(Vector2.zero())
..zoom = 6;
await traceAllBodies();
}
}

@ -0,0 +1,15 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/boundaries/boundaries_game.dart';
void addBoundariesStories(Dashbook dashbook) {
dashbook.storiesOf('Boundaries').add(
'Basic',
(context) => GameWidget(
game: BoundariesGame()..trace = context.boolProperty('Trace', true),
),
codeLink: buildSourceLink('boundaries_game/basic.dart'),
info: BoundariesGame.info,
);
}

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

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

@ -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<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(FlutterSignPost()..priority = 1);
await traceAllBodies();
}
}

@ -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<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(SmallDashNestBumper.a()..priority = 1);
await traceAllBodies();
await traceAllBodies();
}
}

@ -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<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(SmallDashNestBumper.b()..priority = 1);
await traceAllBodies();
await traceAllBodies();
}
}

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

@ -3,10 +3,9 @@ import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class LaunchRampGame extends BasicBallGame with Traceable {
class LaunchRampGame extends BasicBallGame {
LaunchRampGame()
: super(
color: Colors.blue,

@ -3,10 +3,9 @@ import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SpaceshipRailGame extends BasicBallGame with Traceable {
class SpaceshipRailGame extends BasicBallGame {
SpaceshipRailGame()
: super(
color: Colors.blue,

@ -3,10 +3,9 @@ import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SpaceshipRampGame extends BasicBallGame with Traceable {
class SpaceshipRampGame extends BasicBallGame {
SpaceshipRampGame()
: super(
color: Colors.blue,

@ -1,9 +1,10 @@
export 'ball/stories.dart';
export 'baseboard/stories.dart';
export 'boundaries/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 'launch_ramp/stories.dart';
export 'layer/stories.dart';
export 'slingshot/stories.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<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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 153 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,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<void>();
final completer2 = Completer<void>();
final future1 = completer1.future;
final future2 = completer2.future;
blocTest<AssetsManagerCubit, AssetsManagerState>(
'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],
),
],
);
});
}

@ -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<void>.value();
expect(
AssetsManagerState.initial(loadables: [future]),
equals(
AssetsManagerState(
loadables: [future],
loaded: const [],
),
),
);
});
group('progress', () {
final future1 = Future<void>.value();
final future2 = Future<void>.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<void>.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<void>.value();
final future2 = Future<void>.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],
),
),
),
);
});
});
}

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

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

@ -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,21 +23,72 @@ 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 {
testWidgets(
'renders the loading indicator while the assets load',
(tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
final assetsManagerCubit = MockAssetsManagerCubit();
final initialAssetsState = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: const [],
);
whenListen(
assetsManagerCubit,
Stream.value(initialAssetsState),
initialState: initialAssetsState,
);
await tester.pumpApp(
PinballGamePage(theme: theme, game: game),
gameBloc: gameBloc,
assetsManagerCubit: assetsManagerCubit,
);
expect(find.text('0.0'), findsOneWidget);
final loadedAssetsState = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: [Future<void>.value()],
);
whenListen(
assetsManagerCubit,
Stream.value(loadedAssetsState),
initialState: loadedAssetsState,
);
await tester.pump();
expect(find.byType(PinballGameView), findsOneWidget);
},
);
group('route', () {
Future<void> pumpRoute({
required WidgetTester tester,
required bool isDebugMode,
}) async {
await tester.pumpApp(
Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context)
.push<void>(PinballGamePage.route(theme: theme));
Navigator.of(context).push<void>(
PinballGamePage.route(
theme: theme,
isDebugMode: isDebugMode,
),
);
},
child: const Text('Tap me'),
);
@ -51,84 +103,32 @@ void main() {
// 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,
);
testWidgets('route creates the correct non debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: false);
expect(
find.byWidgetPredicate((w) => w is GameWidget<PinballGame>),
findsOneWidget,
);
expect(
find.byType(GameHud),
find.byWidgetPredicate(
(w) => w is PinballGameView && w.game is! DebugPinballGame,
),
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(
const PinballGameView(theme: theme),
gameBloc: gameBloc,
);
await tester.pump();
expect(find.byType(GameOverDialog), 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(),
);
await tester.pumpApp(
const PinballGameView(theme: theme, isDebugMode: false),
gameBloc: gameBloc,
);
testWidgets('route creates the correct debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: true);
expect(
find.byWidgetPredicate(
(w) => w is GameWidget<PinballGame> && w.game is! DebugPinballGame,
(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<PinballGame> && w.game is DebugPinballGame,
),
find.byWidgetPredicate((w) => w is GameWidget<PinballGame>),
findsOneWidget,
);
expect(
find.byType(GameHud),
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';
@ -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 {}

@ -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<void>.value()],
loaded: [
Future<void>.value(),
],
);
whenListen(
cubit,
Stream.value(state),
initialState: state,
);
return cubit;
}
extension PumpApp on WidgetTester {
Future<void> 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 [

Loading…
Cancel
Save