Merge branch 'main' into chore/character-icon-by-input

pull/209/head
Allison Ryan 3 years ago
commit 5a6c1b9fee

3
.gitignore vendored

@ -131,6 +131,3 @@ app.*.map.json
test/.test_runner.dart test/.test_runner.dart
web/__/firebase/init.js web/__/firebase/init.js
# Application exceptions
!/packages/pinball_components/assets/images/flutter_sign_post.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

@ -11,8 +11,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/landing/landing.dart'; import 'package:pinball/theme/theme.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
class App extends StatelessWidget { class App extends StatelessWidget {
@ -34,20 +35,17 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _pinballAudio), RepositoryProvider.value(value: _pinballAudio),
], ],
child: MaterialApp( child: BlocProvider(
create: (context) => ThemeCubit(),
child: const MaterialApp(
title: 'I/O Pinball', title: 'I/O Pinball',
theme: ThemeData( localizationsDelegates: [
appBarTheme: const AppBarTheme(color: Color(0xFF13B9FF)),
colorScheme: ColorScheme.fromSwatch(
accentColor: const Color(0xFF13B9FF),
),
),
localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
], ],
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: const LandingPage(), home: PinballGamePage(),
),
), ),
); );
} }

@ -42,7 +42,7 @@ class Board extends Component {
// TODO(alestiago): Consider renaming once entire Board is defined. // TODO(alestiago): Consider renaming once entire Board is defined.
class _BottomGroup extends Component { class _BottomGroup extends Component {
/// {@macro bottom_group} /// {@macro bottom_group}
_BottomGroup(); _BottomGroup() : super(priority: RenderPriority.bottomGroup);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {

@ -18,7 +18,7 @@ class ControlledBall extends Ball with Controls<BallController> {
required PinballTheme theme, required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) { }) : super(baseColor: theme.characterTheme.ballColor) {
controller = BallController(this); controller = BallController(this);
priority = Ball.launchRampPriority; priority = RenderPriority.ballOnLaunchRamp;
layer = Layer.launcher; layer = Layer.launcher;
} }
@ -31,13 +31,13 @@ class ControlledBall extends Ball with Controls<BallController> {
required PinballTheme theme, required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) { }) : super(baseColor: theme.characterTheme.ballColor) {
controller = BallController(this); controller = BallController(this);
priority = Ball.boardPriority; priority = RenderPriority.ballOnBoard;
} }
/// [Ball] used in [DebugPinballGame]. /// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = DebugBallController(this); controller = DebugBallController(this);
priority = Ball.boardPriority; priority = RenderPriority.ballOnBoard;
} }
} }
@ -66,9 +66,8 @@ class BallController extends ComponentController<Ball>
// given animations. // given animations.
component.stop(); component.stop();
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
component component.resume();
..resume() await component.boost(Vector2(40, 110));
..boost(Vector2(200, 500));
} }
@override @override

@ -25,7 +25,7 @@ class FlutterForest extends Component
await super.onLoad(); await super.onLoad();
gameRef.addContactCallback(_DashNestBumperBallContactCallback()); gameRef.addContactCallback(_DashNestBumperBallContactCallback());
final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, -58.3); final signpost = Signpost()..initialPosition = Vector2(8.35, -58.3);
final bigNest = _BigDashNestBumper() final bigNest = _BigDashNestBumper()
..initialPosition = Vector2(18.55, -59.35); ..initialPosition = Vector2(18.55, -59.35);
@ -36,7 +36,7 @@ class FlutterForest extends Component
final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66); final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66);
await addAll([ await addAll([
signPost, signpost,
smallLeftNest, smallLeftNest,
smallRightNest, smallRightNest,
bigNest, bigNest,

@ -16,8 +16,8 @@ class Launcher extends Forge2DBlueprint {
@override @override
void build(Forge2DGame gameRef) { void build(Forge2DGame gameRef) {
plunger = ControlledPlunger(compressionDistance: 12.3) plunger = ControlledPlunger(compressionDistance: 14)
..initialPosition = Vector2(40.1, 38); ..initialPosition = Vector2(40.7, 38);
final _rocket = RocketSpriteComponent()..position = Vector2(43, 62); final _rocket = RocketSpriteComponent()..position = Vector2(43, 62);

@ -7,8 +7,12 @@ extension PinballGameAssetsX on PinballGame {
/// Returns a list of assets to be loaded /// Returns a list of assets to be loaded
List<Future> preLoadAssets() { List<Future> preLoadAssets() {
return [ return [
images.load(components.Assets.images.ball.keyName), images.load(components.Assets.images.ball.ball.keyName),
images.load(components.Assets.images.flutterSignPost.keyName), images.load(components.Assets.images.ball.flameEffect.keyName),
images.load(components.Assets.images.signpost.inactive.keyName),
images.load(components.Assets.images.signpost.active1.keyName),
images.load(components.Assets.images.signpost.active2.keyName),
images.load(components.Assets.images.signpost.active3.keyName),
images.load(components.Assets.images.flipper.left.keyName), images.load(components.Assets.images.flipper.left.keyName),
images.load(components.Assets.images.flipper.right.keyName), images.load(components.Assets.images.flipper.right.keyName),
images.load(components.Assets.images.baseboard.left.keyName), images.load(components.Assets.images.baseboard.left.keyName),
@ -21,6 +25,9 @@ extension PinballGameAssetsX on PinballGame {
images.load( images.load(
components.Assets.images.launchRamp.foregroundRailing.keyName, components.Assets.images.launchRamp.foregroundRailing.keyName,
), ),
images.load(
components.Assets.images.launchRamp.backgroundRailing.keyName,
),
images.load(components.Assets.images.dino.dinoLandTop.keyName), images.load(components.Assets.images.dino.dinoLandTop.keyName),
images.load(components.Assets.images.dino.dinoLandBottom.keyName), images.load(components.Assets.images.dino.dinoLandBottom.keyName),
images.load(components.Assets.images.dash.animatronic.keyName), images.load(components.Assets.images.dash.animatronic.keyName),

@ -5,6 +5,7 @@ import 'package:flame/components.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
@ -28,6 +29,9 @@ class PinballGame extends Forge2DGame
/// Identifier of the play button overlay /// Identifier of the play button overlay
static const playButtonOverlay = 'play_button'; static const playButtonOverlay = 'play_button';
@override
Color backgroundColor() => Colors.transparent;
final PinballTheme theme; final PinballTheme theme;
final PinballAudio audio; final PinballAudio audio;
@ -164,7 +168,7 @@ class DebugPinballGame extends PinballGame with TapDetector {
anchor: Anchor.center, anchor: Anchor.center,
) )
..position = Vector2(0, -7.8) ..position = Vector2(0, -7.8)
..priority = -4; ..priority = RenderPriority.background;
await add(spriteComponent); await add(spriteComponent);
} }

@ -5,32 +5,40 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGamePage extends StatelessWidget { class PinballGamePage extends StatelessWidget {
const PinballGamePage({ const PinballGamePage({
Key? key, Key? key,
required this.theme, this.isDebugMode = kDebugMode,
required this.game,
}) : super(key: key); }) : super(key: key);
final PinballTheme theme; final bool isDebugMode;
final PinballGame game;
static Route route({ static Route route({
required PinballTheme theme,
bool isDebugMode = kDebugMode, bool isDebugMode = kDebugMode,
}) { }) {
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (context) { builder: (context) {
return PinballGamePage(
isDebugMode: isDebugMode,
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = context.read<ThemeCubit>().state.theme;
final audio = context.read<PinballAudio>(); final audio = context.read<PinballAudio>();
final pinballAudio = context.read<PinballAudio>();
final game = isDebugMode final game = isDebugMode
? DebugPinballGame(theme: theme, audio: audio) ? DebugPinballGame(theme: theme, audio: audio)
: PinballGame(theme: theme, audio: audio); : PinballGame(theme: theme, audio: audio);
final pinballAudio = context.read<PinballAudio>();
final loadables = [ final loadables = [
...game.preLoadAssets(), ...game.preLoadAssets(),
pinballAudio.load(), pinballAudio.load(),
@ -38,21 +46,15 @@ class PinballGamePage extends StatelessWidget {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (_) => StartGameBloc(game: game)),
BlocProvider(create: (_) => GameBloc()), BlocProvider(create: (_) => GameBloc()),
BlocProvider( BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(), create: (_) => AssetsManagerCubit(loadables)..load(),
), ),
], ],
child: PinballGamePage(theme: theme, game: game), child: PinballGameView(game: game),
);
},
); );
} }
@override
Widget build(BuildContext context) {
return PinballGameView(game: game);
}
} }
class PinballGameView extends StatelessWidget { class PinballGameView extends StatelessWidget {
@ -65,18 +67,51 @@ class PinballGameView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loadingProgress = context.watch<AssetsManagerCubit>().state.progress; final isLoading = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress != 1,
);
if (loadingProgress != 1) {
return Scaffold( return Scaffold(
body: Center( backgroundColor: Colors.blue,
child: Text( body: isLoading
loadingProgress.toString(), ? const _PinballGameLoadingView()
: PinballGameLoadedView(game: game),
);
}
}
class _PinballGameLoadingView extends StatelessWidget {
const _PinballGameLoadingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final loadingProgress = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress,
);
return Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: LinearProgressIndicator(
color: Colors.white,
value: loadingProgress,
), ),
), ),
); );
} }
}
@visibleForTesting
class PinballGameLoadedView extends StatelessWidget {
const PinballGameLoadedView({
Key? key,
required this.game,
}) : super(key: key);
final PinballGame game;
@override
Widget build(BuildContext context) {
return Stack( return Stack(
children: [ children: [
Positioned.fill( Positioned.fill(

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/pinball_game.dart'; import 'package:pinball/game/pinball_game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
/// {@template play_button_overlay} /// {@template play_button_overlay}
/// [Widget] that renders the button responsible to starting the game /// [Widget] that renders the button responsible to starting the game
@ -18,9 +19,27 @@ class PlayButtonOverlay extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: _game.gameFlowController.start, onPressed: () {
_game.gameFlowController.start();
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) {
final height = MediaQuery.of(context).size.height * 0.5;
return Center(
child: SizedBox(
height: height,
width: height * 1.4,
child: const CharacterSelectionDialog(),
),
);
},
);
},
child: Text(l10n.play), child: Text(l10n.play),
), ),
); );

@ -0,0 +1 @@
export 'assets.gen.dart';

@ -1 +0,0 @@
export 'view/landing_page.dart';

@ -69,7 +69,7 @@ class LeaderboardView extends StatelessWidget {
const SizedBox(height: 20), const SizedBox(height: 20),
TextButton( TextButton(
onPressed: () => Navigator.of(context).push<void>( onPressed: () => Navigator.of(context).push<void>(
CharacterSelectionPage.route(), CharacterSelectionDialog.route(),
), ),
child: Text(l10n.retry), child: Text(l10n.retry),
), ),

@ -1 +1,2 @@
export 'bloc/start_game_bloc.dart'; export 'bloc/start_game_bloc.dart';
export 'widgets/widgets.dart';

@ -2,42 +2,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
class LandingPage extends StatelessWidget { class HowToPlayDialog extends StatelessWidget {
const LandingPage({Key? key}) : super(key: key); const HowToPlayDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () => Navigator.of(context).push<void>(
CharacterSelectionPage.route(),
),
child: Text(l10n.play),
),
TextButton(
onPressed: () => showDialog<void>(
context: context,
builder: (_) => const _HowToPlayDialog(),
),
child: Text(l10n.howToPlay),
),
],
),
),
);
}
}
class _HowToPlayDialog extends StatelessWidget {
const _HowToPlayDialog({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

@ -0,0 +1 @@
export 'how_to_play_dialog.dart';

@ -0,0 +1,15 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
abstract class AppColors {
static const Color white = Color(0xFFFFFFFF);
static const Color darkBlue = Color(0xFF0C32A4);
static const Color orange = Color(0xFFFFEE02);
static const Color blue = Color(0xFF4B94F6);
static const Color transparent = Color(0x00000000);
}

@ -0,0 +1,35 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/widgets.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_components/pinball_components.dart';
const _fontPackage = 'pinball_components';
const _primaryFontFamily = PinballFonts.pixeloidSans;
abstract class AppTextStyle {
static const headline1 = TextStyle(
fontSize: 28,
package: _fontPackage,
fontFamily: _primaryFontFamily,
);
static const headline2 = TextStyle(
fontSize: 24,
package: _fontPackage,
fontFamily: _primaryFontFamily,
);
static const headline3 = TextStyle(
color: AppColors.white,
fontSize: 20,
package: _fontPackage,
fontFamily: _primaryFontFamily,
);
static const subtitle1 = TextStyle(
fontSize: 10,
fontFamily: _primaryFontFamily,
package: _fontPackage,
);
}

@ -1,2 +1,4 @@
export 'app_colors.dart';
export 'app_text_style.dart';
export 'cubit/theme_cubit.dart'; export 'cubit/theme_cubit.dart';
export 'view/view.dart'; export 'view/view.dart';

@ -2,17 +2,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball/theme/theme.dart'; import 'package:pinball/theme/theme.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
class CharacterSelectionPage extends StatelessWidget { class CharacterSelectionDialog extends StatelessWidget {
const CharacterSelectionPage({Key? key}) : super(key: key); const CharacterSelectionDialog({Key? key}) : super(key: key);
static Route route() { static Route route() {
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (_) => const CharacterSelectionPage(), builder: (_) => const CharacterSelectionDialog(),
); );
} }
@ -46,11 +46,13 @@ class CharacterSelectionView extends StatelessWidget {
const _CharacterSelectionGridView(), const _CharacterSelectionGridView(),
const SizedBox(height: 20), const SizedBox(height: 20),
TextButton( TextButton(
onPressed: () => Navigator.of(context).push<void>( onPressed: () {
PinballGamePage.route( Navigator.of(context).pop();
theme: context.read<ThemeCubit>().state.theme, showDialog<void>(
), context: context,
), builder: (_) => const HowToPlayDialog(),
);
},
child: Text(l10n.start), child: Text(l10n.start),
), ),
], ],

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -13,10 +13,7 @@ class $AssetsImagesGen {
$AssetsImagesAlienBumperGen get alienBumper => $AssetsImagesAlienBumperGen get alienBumper =>
const $AssetsImagesAlienBumperGen(); const $AssetsImagesAlienBumperGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
/// File path: assets/images/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
$AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen();
$AssetsImagesChromeDinoGen get chromeDino => $AssetsImagesChromeDinoGen get chromeDino =>
@ -24,17 +21,13 @@ class $AssetsImagesGen {
$AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDashGen get dash => const $AssetsImagesDashGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
/// File path: assets/images/flutter_sign_post.png
AssetGenImage get flutterSignPost =>
const AssetGenImage('assets/images/flutter_sign_post.png');
$AssetsImagesGoogleWordGen get googleWord => $AssetsImagesGoogleWordGen get googleWord =>
const $AssetsImagesGoogleWordGen(); const $AssetsImagesGoogleWordGen();
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen(); $AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp => $AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen(); $AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
@ -57,10 +50,23 @@ class $AssetsImagesBackboardGen {
/// File path: assets/images/backboard/backboard_scores.png /// File path: assets/images/backboard/backboard_scores.png
AssetGenImage get backboardScores => AssetGenImage get backboardScores =>
const AssetGenImage('assets/images/backboard/backboard_scores.png'); const AssetGenImage('assets/images/backboard/backboard_scores.png');
/// File path: assets/images/backboard/display.png
AssetGenImage get display => AssetGenImage get display =>
const AssetGenImage('assets/images/backboard/display.png'); const AssetGenImage('assets/images/backboard/display.png');
} }
class $AssetsImagesBallGen {
const $AssetsImagesBallGen();
/// File path: assets/images/ball/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/ball/ball.png');
/// File path: assets/images/ball/flame_effect.png
AssetGenImage get flameEffect =>
const AssetGenImage('assets/images/ball/flame_effect.png');
}
class $AssetsImagesBaseboardGen { class $AssetsImagesBaseboardGen {
const $AssetsImagesBaseboardGen(); const $AssetsImagesBaseboardGen();
@ -174,6 +180,10 @@ class $AssetsImagesKickerGen {
class $AssetsImagesLaunchRampGen { class $AssetsImagesLaunchRampGen {
const $AssetsImagesLaunchRampGen(); const $AssetsImagesLaunchRampGen();
/// File path: assets/images/launch_ramp/background-railing.png
AssetGenImage get backgroundRailing =>
const AssetGenImage('assets/images/launch_ramp/background-railing.png');
/// File path: assets/images/launch_ramp/foreground-railing.png /// File path: assets/images/launch_ramp/foreground-railing.png
AssetGenImage get foregroundRailing => AssetGenImage get foregroundRailing =>
const AssetGenImage('assets/images/launch_ramp/foreground-railing.png'); const AssetGenImage('assets/images/launch_ramp/foreground-railing.png');
@ -195,6 +205,26 @@ class $AssetsImagesPlungerGen {
const AssetGenImage('assets/images/plunger/rocket.png'); const AssetGenImage('assets/images/plunger/rocket.png');
} }
class $AssetsImagesSignpostGen {
const $AssetsImagesSignpostGen();
/// File path: assets/images/signpost/active1.png
AssetGenImage get active1 =>
const AssetGenImage('assets/images/signpost/active1.png');
/// File path: assets/images/signpost/active2.png
AssetGenImage get active2 =>
const AssetGenImage('assets/images/signpost/active2.png');
/// File path: assets/images/signpost/active3.png
AssetGenImage get active3 =>
const AssetGenImage('assets/images/signpost/active3.png');
/// File path: assets/images/signpost/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/signpost/inactive.png');
}
class $AssetsImagesSlingshotGen { class $AssetsImagesSlingshotGen {
const $AssetsImagesSlingshotGen(); const $AssetsImagesSlingshotGen();
@ -309,8 +339,11 @@ class $AssetsImagesSparkyBumperGen {
class $AssetsImagesSparkyComputerGen { class $AssetsImagesSparkyComputerGen {
const $AssetsImagesSparkyComputerGen(); const $AssetsImagesSparkyComputerGen();
/// File path: assets/images/sparky/computer/base.png
AssetGenImage get base => AssetGenImage get base =>
const AssetGenImage('assets/images/sparky/computer/base.png'); const AssetGenImage('assets/images/sparky/computer/base.png');
/// File path: assets/images/sparky/computer/top.png
AssetGenImage get top => AssetGenImage get top =>
const AssetGenImage('assets/images/sparky/computer/top.png'); const AssetGenImage('assets/images/sparky/computer/top.png');
} }
@ -354,8 +387,11 @@ class $AssetsImagesDashBumperMainGen {
class $AssetsImagesSparkyBumperAGen { class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen(); const $AssetsImagesSparkyBumperAGen();
/// File path: assets/images/sparky/bumper/a/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/a/active.png'); const AssetGenImage('assets/images/sparky/bumper/a/active.png');
/// File path: assets/images/sparky/bumper/a/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/a/inactive.png'); const AssetGenImage('assets/images/sparky/bumper/a/inactive.png');
} }
@ -363,8 +399,11 @@ class $AssetsImagesSparkyBumperAGen {
class $AssetsImagesSparkyBumperBGen { class $AssetsImagesSparkyBumperBGen {
const $AssetsImagesSparkyBumperBGen(); const $AssetsImagesSparkyBumperBGen();
/// File path: assets/images/sparky/bumper/b/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/b/active.png'); const AssetGenImage('assets/images/sparky/bumper/b/active.png');
/// File path: assets/images/sparky/bumper/b/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/b/inactive.png'); const AssetGenImage('assets/images/sparky/bumper/b/inactive.png');
} }
@ -372,8 +411,11 @@ class $AssetsImagesSparkyBumperBGen {
class $AssetsImagesSparkyBumperCGen { class $AssetsImagesSparkyBumperCGen {
const $AssetsImagesSparkyBumperCGen(); const $AssetsImagesSparkyBumperCGen();
/// File path: assets/images/sparky/bumper/c/active.png
AssetGenImage get active => AssetGenImage get active =>
const AssetGenImage('assets/images/sparky/bumper/c/active.png'); const AssetGenImage('assets/images/sparky/bumper/c/active.png');
/// File path: assets/images/sparky/bumper/c/inactive.png
AssetGenImage get inactive => AssetGenImage get inactive =>
const AssetGenImage('assets/images/sparky/bumper/c/inactive.png'); const AssetGenImage('assets/images/sparky/bumper/c/inactive.png');
} }

@ -0,0 +1,2 @@
export 'assets.gen.dart';
export 'pinball_fonts.dart';

@ -1,16 +1,14 @@
import 'package:pinball_components/gen/fonts.gen.dart'; import 'package:pinball_components/gen/fonts.gen.dart';
String _prefixFont(String font) { const String _fontPath = 'packages/pinball_components/';
return 'packages/pinball_components/$font';
}
/// Class with the fonts available on the pinball game /// Class with the fonts available on the pinball game
class PinballFonts { class PinballFonts {
PinballFonts._(); PinballFonts._();
/// Mono variation of the Pixeloid font /// Mono variation of the Pixeloid font
static final String pixeloidMono = _prefixFont(FontFamily.pixeloidMono); static const String pixeloidMono = '$_fontPath/${FontFamily.pixeloidMono}';
/// Sans variation of the Pixeloid font /// Sans variation of the Pixeloid font
static final String pixeloidSans = _prefixFont(FontFamily.pixeloidSans); static const String pixeloidSans = '$_fontPath/${FontFamily.pixeloidSans}';
} }

@ -1,5 +1,4 @@
library pinball_components; library pinball_components;
export 'gen/assets.gen.dart'; export 'gen/gen.dart';
export 'gen/pinball_fonts.dart';
export 'src/pinball_components.dart'; export 'src/pinball_components.dart';

@ -18,7 +18,8 @@ class AlienBumper extends BodyComponent with InitialPosition {
_minorRadius = minorRadius, _minorRadius = minorRadius,
_activeAssetPath = activeAssetPath, _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath, _inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent; _spriteComponent = spriteComponent,
super(priority: RenderPriority.alienBumper);
/// {@macro alien_bumper} /// {@macro alien_bumper}
AlienBumper.a() AlienBumper.a()

@ -48,7 +48,7 @@ class Backboard extends PositionComponent with HasGameRef {
/// [TextPaint] used on the [Backboard] /// [TextPaint] used on the [Backboard]
static final textPaint = TextPaint( static final textPaint = TextPaint(
style: TextStyle( style: const TextStyle(
fontSize: 6, fontSize: 6,
color: Colors.white, color: Colors.white,
fontFamily: PinballFonts.pixeloidSans, fontFamily: PinballFonts.pixeloidSans,

@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/widgets.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template ball} /// {@template ball}
@ -28,30 +29,12 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
renderBody = false; renderBody = false;
} }
/// Render priority for the [Ball] while it's on the board.
static const int boardPriority = 0;
/// Render priority for the [Ball] while it's on the [SpaceshipRamp].
static const int spaceshipRampPriority = 4;
/// Render priority for the [Ball] while it's on the [Spaceship].
static const int spaceshipPriority = 4;
/// Render priority for the [Ball] while it's on the [SpaceshipRail].
static const int spaceshipRailPriority = 2;
/// Render priority for the [Ball] while it's on the [LaunchRamp].
static const int launchRampPriority = -2;
/// The size of the [Ball]. /// The size of the [Ball].
static final Vector2 size = Vector2.all(4.13); 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; final Color baseColor;
double _boostTimer = 0;
static const _boostDuration = 2.0;
@override @override
Body createBody() { Body createBody() {
final shape = CircleShape()..radius = size.x / 2; final shape = CircleShape()..radius = size.x / 2;
@ -87,32 +70,20 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
body.gravityScale = Vector2(0, 1); body.gravityScale = Vector2(0, 1);
} }
/// Applies a boost and [_TurboChargeSpriteAnimationComponent] on this [Ball].
Future<void> boost(Vector2 impulse) async {
body.linearVelocity = impulse;
await add(_TurboChargeSpriteAnimationComponent());
}
@override @override
void update(double dt) { void update(double dt) {
super.update(dt); super.update(dt);
if (_boostTimer > 0) {
_boostTimer -= dt;
final direction = body.linearVelocity.normalized();
final effect = FireEffect(
burstPower: _boostTimer,
direction: direction,
position: Vector2(body.position.x, body.position.y),
priority: priority - 1,
);
unawaited(gameRef.add(effect));
}
_rescaleSize(); _rescaleSize();
_setPositionalGravity(); _setPositionalGravity();
} }
/// Applies a boost on this [Ball].
void boost(Vector2 impulse) {
body.linearVelocity = impulse;
_boostTimer = _boostDuration;
}
void _rescaleSize() { void _rescaleSize() {
final boardHeight = BoardDimensions.bounds.height; final boardHeight = BoardDimensions.bounds.height;
const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
@ -153,10 +124,61 @@ class _BallSpriteComponent extends SpriteComponent with HasGameRef {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprite = await gameRef.loadSprite( final sprite = await gameRef.loadSprite(
Assets.images.ball.keyName, Assets.images.ball.ball.keyName,
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
} }
} }
class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
_TurboChargeSpriteAnimationComponent()
: super(
anchor: const Anchor(0.53, 0.72),
priority: RenderPriority.turboChargeFlame,
removeOnFinish: true,
);
late final Vector2 _textureSize;
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = await gameRef.images.load(
Assets.images.ball.flameEffect.keyName,
);
const amountPerRow = 8;
const amountPerColumn = 4;
_textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: _textureSize,
loop: false,
),
);
}
@override
void update(double dt) {
super.update(dt);
if (parent != null) {
final body = (parent! as BodyComponent).body;
final direction = -body.linearVelocity.normalized();
angle = math.atan2(direction.x, -direction.y);
size = (_textureSize / 45) * body.fixtures.first.shape.radius;
}
}
}

@ -26,7 +26,7 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
/// {@macro bottom_boundary} /// {@macro bottom_boundary}
_BottomBoundary() _BottomBoundary()
: super( : super(
priority: 1, priority: RenderPriority.bottomBoundary,
children: [_BottomBoundarySpriteComponent()], children: [_BottomBoundarySpriteComponent()],
) { ) {
renderBody = false; renderBody = false;
@ -78,7 +78,7 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
position = Vector2(-5.4, 55.8); position = Vector2(-5.4, 55.6);
} }
} }
@ -90,7 +90,7 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
/// {@macro outer_boundary} /// {@macro outer_boundary}
_OuterBoundary() _OuterBoundary()
: super( : super(
priority: Ball.launchRampPriority - 1, priority: RenderPriority.outerBoudary,
children: [_OuterBoundarySpriteComponent()], children: [_OuterBoundarySpriteComponent()],
) { ) {
renderBody = false; renderBody = false;

@ -12,10 +12,12 @@ import 'package:pinball_components/pinball_components.dart';
/// {@endtemplate} /// {@endtemplate}
class ChromeDino extends BodyComponent with InitialPosition { class ChromeDino extends BodyComponent with InitialPosition {
/// {@macro chrome_dino} /// {@macro chrome_dino}
ChromeDino() { ChromeDino()
: super(
// TODO(alestiago): Remove once sprites are defined. // TODO(alestiago): Remove once sprites are defined.
paint = Paint()..color = Colors.blue; paint: Paint()..color = Colors.blue,
} priority: RenderPriority.dino,
);
/// The size of the dinosaur mouth. /// The size of the dinosaur mouth.
static final size = Vector2(5, 2.5); static final size = Vector2(5, 2.5);

@ -12,7 +12,6 @@ export 'dash_nest_bumper.dart';
export 'dino_walls.dart'; export 'dino_walls.dart';
export 'fire_effect.dart'; export 'fire_effect.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'flutter_sign_post.dart';
export 'google_letter.dart'; export 'google_letter.dart';
export 'initial_position.dart'; export 'initial_position.dart';
export 'joint_anchor.dart'; export 'joint_anchor.dart';
@ -21,9 +20,11 @@ export 'launch_ramp.dart';
export 'layer.dart'; export 'layer.dart';
export 'layer_sensor.dart'; export 'layer_sensor.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'render_priority.dart';
export 'rocket.dart'; export 'rocket.dart';
export 'score_text.dart'; export 'score_text.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'signpost.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship.dart'; export 'spaceship.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';

@ -10,6 +10,7 @@ class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
playing: false, playing: false,
priority: RenderPriority.dashAnimatronic,
); );
@override @override

@ -15,7 +15,8 @@ abstract class DashNestBumper extends BodyComponent with InitialPosition {
required SpriteComponent spriteComponent, required SpriteComponent spriteComponent,
}) : _activeAssetPath = activeAssetPath, }) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath, _inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent; _spriteComponent = spriteComponent,
super(priority: RenderPriority.dashBumper);
final String _activeAssetPath; final String _activeAssetPath;
late final Sprite _activeSprite; late final Sprite _activeSprite;

@ -31,7 +31,7 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall} ///{@macro dino_top_wall}
_DinoTopWall() _DinoTopWall()
: super( : super(
priority: 1, priority: RenderPriority.dinoTopWall,
children: [_DinoTopWallSpriteComponent()], children: [_DinoTopWallSpriteComponent()],
) { ) {
renderBody = false; renderBody = false;
@ -42,7 +42,7 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
final topStraightShape = EdgeShape() final topStraightShape = EdgeShape()
..set( ..set(
Vector2(28.4, -35.1), Vector2(28.65, -35.1),
Vector2(29.5, -35.1), Vector2(29.5, -35.1),
); );
final topStraightFixtureDef = FixtureDef(topStraightShape); final topStraightFixtureDef = FixtureDef(topStraightShape);
@ -69,8 +69,8 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
final bottomCurveShape = BezierCurveShape( final bottomCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
middleCurveShape.vertices.last, middleCurveShape.vertices.last,
Vector2(21.15, -16), Vector2(21.5, -15.8),
Vector2(25.6, -15.2), Vector2(25.8, -14.8),
], ],
); );
fixturesDef.add(FixtureDef(bottomCurveShape)); fixturesDef.add(FixtureDef(bottomCurveShape));
@ -126,6 +126,7 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall} ///{@macro dino_top_wall}
_DinoBottomWall() _DinoBottomWall()
: super( : super(
priority: RenderPriority.dinoBottomWall,
children: [_DinoBottomWallSpriteComponent()], children: [_DinoBottomWallSpriteComponent()],
) { ) {
renderBody = false; renderBody = false;
@ -136,7 +137,7 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
const restitution = 1.0; const restitution = 1.0;
final topStraightControlPoints = [ final topStraightControlPoints = [
Vector2(32.4, -8.3), Vector2(32.4, -8.8),
Vector2(25, -7.7), Vector2(25, -7.7),
]; ];
final topStraightShape = EdgeShape() final topStraightShape = EdgeShape()
@ -220,6 +221,6 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef {
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
position = Vector2(23.8, -9.5); position = Vector2(23.6, -9.5);
} }
} }

@ -1,42 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template flutter_sign_post}
/// A sign, found in the Flutter Forest.
/// {@endtemplate}
class FlutterSignPost extends BodyComponent with InitialPosition {
/// {@macro flutter_sign_post}
FlutterSignPost()
: super(
children: [_FlutterSignPostSpriteComponent()],
) {
renderBody = false;
}
@override
Body createBody() {
final shape = CircleShape()..radius = 0.25;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: 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);
}
}

@ -42,10 +42,21 @@ class LaunchRamp extends Forge2DBlueprint {
/// {@endtemplate} /// {@endtemplate}
class _LaunchRampBase extends BodyComponent with InitialPosition, Layered { class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
/// {@macro launch_ramp_base} /// {@macro launch_ramp_base}
_LaunchRampBase() : super(priority: Ball.launchRampPriority - 1) { _LaunchRampBase()
: super(
priority: RenderPriority.launchRamp,
children: [
_LaunchRampBackgroundRailingSpriteComponent(),
_LaunchRampBaseSpriteComponent(),
],
) {
layer = Layer.launcher; layer = Layer.launcher;
renderBody = false;
} }
// TODO(ruimiguel): final asset differs slightly from the current shape. We
// need to fix shape with correct vertices, but right now merge them to have
// final assets at game and not be blocked.
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
@ -114,28 +125,36 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
return body; return body;
} }
}
class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
renderBody = false;
await add(_LaunchRampBaseSpriteComponent()); final sprite = await gameRef.loadSprite(
Assets.images.launchRamp.ramp.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(25.65, 0.7);
} }
} }
class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef { class _LaunchRampBackgroundRailingSpriteComponent extends SpriteComponent
with HasGameRef {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprite = await gameRef.loadSprite( final sprite = await gameRef.loadSprite(
Assets.images.launchRamp.ramp.keyName, Assets.images.launchRamp.backgroundRailing.keyName,
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
position = Vector2(25.65, 0); position = Vector2(25.6, -1.3);
} }
} }
@ -145,7 +164,13 @@ class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {
/// {@endtemplate} /// {@endtemplate}
class _LaunchRampForegroundRailing extends BodyComponent with InitialPosition { class _LaunchRampForegroundRailing extends BodyComponent with InitialPosition {
/// {@macro launch_ramp_foreground_railing} /// {@macro launch_ramp_foreground_railing}
_LaunchRampForegroundRailing() : super(priority: Ball.launchRampPriority + 1); _LaunchRampForegroundRailing()
: super(
priority: RenderPriority.launchRampForegroundRailing,
children: [_LaunchRampForegroundRailingSpriteComponent()],
) {
renderBody = false;
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
@ -153,7 +178,7 @@ class _LaunchRampForegroundRailing extends BodyComponent with InitialPosition {
final rightStraightShape = EdgeShape() final rightStraightShape = EdgeShape()
..set( ..set(
Vector2(27.6, -57.9), Vector2(27.6, -57.9),
Vector2(30, -35.1), Vector2(38.1, 42.6),
); );
final rightStraightFixtureDef = FixtureDef(rightStraightShape); final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef); fixturesDef.add(rightStraightFixtureDef);
@ -189,14 +214,6 @@ class _LaunchRampForegroundRailing extends BodyComponent with InitialPosition {
return body; return body;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_LaunchRampForegroundRailingSpriteComponent());
}
} }
class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent
@ -211,7 +228,7 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
position = Vector2(22.8, 0); position = Vector2(22.8, 0.5);
} }
} }
@ -248,8 +265,8 @@ class _LaunchRampExit extends LayerSensor {
insideLayer: Layer.launcher, insideLayer: Layer.launcher,
outsideLayer: Layer.board, outsideLayer: Layer.board,
orientation: LayerEntranceOrientation.down, orientation: LayerEntranceOrientation.down,
insidePriority: Ball.launchRampPriority, insidePriority: RenderPriority.ballOnLaunchRamp,
outsidePriority: 0, outsidePriority: RenderPriority.ballOnBoard,
) { ) {
layer = Layer.launcher; layer = Layer.launcher;
renderBody = false; renderBody = false;

@ -34,7 +34,7 @@ abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
}) : _insideLayer = insideLayer, }) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board, _outsideLayer = outsideLayer ?? Layer.board,
_insidePriority = insidePriority, _insidePriority = insidePriority,
_outsidePriority = outsidePriority ?? Ball.boardPriority { _outsidePriority = outsidePriority ?? RenderPriority.ballOnBoard {
layer = Layer.opening; layer = Layer.opening;
} }
final Layer _insideLayer; final Layer _insideLayer;

@ -14,25 +14,49 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
required this.compressionDistance, required this.compressionDistance,
// TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities // TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities
// are fixed. // are fixed.
}) : super(priority: 0) { }) : super(priority: RenderPriority.plunger) {
layer = Layer.launcher; layer = Layer.launcher;
renderBody = false;
} }
/// Distance the plunger can lower. /// Distance the plunger can lower.
final double compressionDistance; final double compressionDistance;
late final _PlungerSpriteAnimationGroupComponent _spriteComponent;
List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[];
final leftShapeVertices = [
Vector2(0, 0),
Vector2(-1.8, 0),
Vector2(-1.8, -2.2),
Vector2(0, -0.3),
]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle))
.toList();
final leftTriangleShape = PolygonShape()..set(leftShapeVertices);
final leftTriangleFixtureDef = FixtureDef(leftTriangleShape)..density = 80;
fixturesDef.add(leftTriangleFixtureDef);
final rightShapeVertices = [
Vector2(0, 0),
Vector2(1.8, 0),
Vector2(1.8, -2.2),
Vector2(0, -0.3),
]..map((vector) => vector.rotate(BoardDimensions.perspectiveAngle))
.toList();
final rightTriangleShape = PolygonShape()..set(rightShapeVertices);
final rightTriangleFixtureDef = FixtureDef(rightTriangleShape)
..density = 80;
fixturesDef.add(rightTriangleFixtureDef);
return fixturesDef;
}
@override @override
Body createBody() { Body createBody() {
final shape = PolygonShape()
..setAsBox(
1.35,
0.5,
Vector2.zero(),
BoardDimensions.perspectiveAngle,
);
final fixtureDef = FixtureDef(shape)..density = 80;
final bodyDef = BodyDef( final bodyDef = BodyDef(
position: initialPosition, position: initialPosition,
userData: this, userData: this,
@ -40,12 +64,15 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
gravityScale: Vector2.zero(), gravityScale: Vector2.zero(),
); );
return world.createBody(bodyDef)..createFixture(fixtureDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
} }
/// Set a constant downward velocity on the [Plunger]. /// Set a constant downward velocity on the [Plunger].
void pull() { void pull() {
body.linearVelocity = Vector2(0, 7); body.linearVelocity = Vector2(0, 7);
_spriteComponent.pull();
} }
/// Set an upward velocity on the [Plunger]. /// Set an upward velocity on the [Plunger].
@ -55,6 +82,7 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
void release() { void release() {
final velocity = (initialPosition.y - body.position.y) * 5; final velocity = (initialPosition.y - body.position.y) * 5;
body.linearVelocity = Vector2(0, velocity); body.linearVelocity = Vector2(0, velocity);
_spriteComponent.release();
} }
/// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical /// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical
@ -77,24 +105,84 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await _anchorToJoint(); await _anchorToJoint();
renderBody = false;
await add(_PlungerSpriteComponent()); _spriteComponent = _PlungerSpriteAnimationGroupComponent();
await add(_spriteComponent);
}
}
/// Animation states associated with a [Plunger].
enum _PlungerAnimationState {
/// Pull state.
pull,
/// Release state.
release,
}
/// Animations for pulling and releasing [Plunger].
class _PlungerSpriteAnimationGroupComponent
extends SpriteAnimationGroupComponent<_PlungerAnimationState>
with HasGameRef {
_PlungerSpriteAnimationGroupComponent()
: super(
anchor: Anchor.center,
position: Vector2(1.87, 14.9),
);
void pull() {
if (current != _PlungerAnimationState.pull) {
animation?.reset();
} }
current = _PlungerAnimationState.pull;
}
void release() {
if (current != _PlungerAnimationState.release) {
animation?.reset();
}
current = _PlungerAnimationState.release;
} }
class _PlungerSpriteComponent extends SpriteComponent with HasGameRef {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprite = await gameRef.loadSprite(
final spriteSheet = await gameRef.images.load(
Assets.images.plunger.plunger.keyName, Assets.images.plunger.plunger.keyName,
); );
this.sprite = sprite; const amountPerRow = 20;
size = sprite.originalSize / 10; const amountPerColumn = 1;
anchor = Anchor.center;
position = Vector2(1.5, 13.4); final textureSize = Vector2(
angle = -0.008; spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
// TODO(ruimiguel): we only need plunger pull animation, and release is just
// to reverse it, so we need to divide by 2 while we don't have only half of
// the animation (but amountPerRow and amountPerColumn needs to be correct
// in order of calculate textureSize correctly).
final pullAnimation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn ~/ 2,
amountPerRow: amountPerRow ~/ 2,
stepTime: 1 / 24,
textureSize: textureSize,
texturePosition: Vector2.zero(),
loop: false,
),
);
animations = {
_PlungerAnimationState.release: pullAnimation.reversed(),
_PlungerAnimationState.pull: pullAnimation,
};
current = _PlungerAnimationState.release;
} }
} }

@ -0,0 +1,114 @@
// ignore_for_file: public_member_api_docs
import 'package:pinball_components/pinball_components.dart';
/// {@template render_priority}
/// Priorities for the component rendering order in the pinball game.
/// {@endtemplate}
// TODO(allisonryan0002): find alternative to section comments.
abstract class RenderPriority {
static const _base = 0;
static const _above = 1;
static const _below = -1;
// Ball
/// Render priority for the [Ball] while it's on the board.
static const int ballOnBoard = _base;
/// Render priority for the [Ball] while it's on the [SpaceshipRamp].
static const int ballOnSpaceshipRamp =
_above + spaceshipRampBackgroundRailing;
/// Render priority for the [Ball] while it's on the [Spaceship].
static const int ballOnSpaceship = _above + spaceshipSaucer;
/// Render priority for the [Ball] while it's on the [SpaceshipRail].
static const int ballOnSpaceshipRail = _below + spaceshipSaucer;
/// Render priority for the [Ball] while it's on the [LaunchRamp].
static const int ballOnLaunchRamp = _above + launchRamp;
// Background
// TODO(allisonryan0002): fix this magic priority. Could bump all priorities
// so there are no negatives.
static const int background = 3 * _below + _base;
// Boundaries
static const int bottomBoundary = _above + dinoBottomWall;
static const int outerBoudary = _above + background;
// Bottom Group
static const int bottomGroup = _above + ballOnBoard;
// Launcher
static const int launchRamp = _above + outerBoudary;
static const int launchRampForegroundRailing = _above + ballOnLaunchRamp;
static const int plunger = _above + launchRamp;
static const int rocket = _above + bottomBoundary;
// Dino Land
static const int dinoTopWall = _above + ballOnBoard;
static const int dino = _above + dinoTopWall;
static const int dinoBottomWall = _above + dino;
static const int slingshot = _above + ballOnBoard;
// Flutter Forest
static const int signpost = _above + launchRampForegroundRailing;
static const int dashBumper = _above + ballOnBoard;
static const int dashAnimatronic = _above + launchRampForegroundRailing;
// Sparky Fire Zone
static const int computerBase = _below + ballOnBoard;
static const int computerTop = _above + ballOnBoard;
static const int sparkyAnimatronic = _above + spaceshipRampForegroundRailing;
static const int sparkyBumper = _above + ballOnBoard;
static const int turboChargeFlame = _above + ballOnBoard;
// Android Spaceship
static const int spaceshipRail = _above + bottomGroup;
static const int spaceshipRailForeground = _above + spaceshipRail;
static const int spaceshipSaucer = _above + spaceshipRail;
static const int spaceshipSaucerWall = _above + spaceshipSaucer;
static const int androidHead = _above + spaceshipSaucer;
static const int spaceshipRamp = _above + ballOnBoard;
static const int spaceshipRampBackgroundRailing = _above + spaceshipRamp;
static const int spaceshipRampForegroundRailing =
_above + ballOnSpaceshipRamp;
static const int spaceshipRampBoardOpening = _below + ballOnBoard;
static const int alienBumper = _above + ballOnBoard;
// Score Text
static const int scoreText = _above + spaceshipRampForegroundRailing;
}

@ -9,7 +9,7 @@ class RocketSpriteComponent extends SpriteComponent with HasGameRef {
// TODO(ruimiguel): change this priority to be over launcher ramp and bottom // TODO(ruimiguel): change this priority to be over launcher ramp and bottom
// wall. // wall.
/// {@macro rocket_sprite_component} /// {@macro rocket_sprite_component}
RocketSpriteComponent() : super(priority: 5); RocketSpriteComponent() : super(priority: RenderPriority.rocket);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {

@ -18,7 +18,7 @@ class ScoreText extends TextComponent {
text: text, text: text,
position: position, position: position,
anchor: Anchor.center, anchor: Anchor.center,
priority: Ball.spaceshipRampPriority + 1, priority: RenderPriority.scoreText,
); );
late final Effect _effect; late final Effect _effect;

@ -0,0 +1,102 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// Represents the [Signpost]'s current [Sprite] state.
@visibleForTesting
enum SignpostSpriteState {
/// Signpost with no active dashes.
inactive,
/// Signpost with a single sign of active dashes.
active1,
/// Signpost with two signs of active dashes.
active2,
/// Signpost with all signs of active dashes.
active3,
}
extension on SignpostSpriteState {
String get path {
switch (this) {
case SignpostSpriteState.inactive:
return Assets.images.signpost.inactive.keyName;
case SignpostSpriteState.active1:
return Assets.images.signpost.active1.keyName;
case SignpostSpriteState.active2:
return Assets.images.signpost.active2.keyName;
case SignpostSpriteState.active3:
return Assets.images.signpost.active3.keyName;
}
}
SignpostSpriteState get next {
return SignpostSpriteState
.values[(index + 1) % SignpostSpriteState.values.length];
}
}
/// {@template signpost}
/// A sign, found in the Flutter Forest.
///
/// Lights up a new sign whenever all three [DashNestBumper]s are hit.
/// {@endtemplate}
class Signpost extends BodyComponent with InitialPosition {
/// {@macro signpost}
Signpost()
: super(
priority: RenderPriority.signpost,
children: [_SignpostSpriteComponent()],
) {
renderBody = false;
}
/// Forwards the sprite to the next [SignpostSpriteState].
///
/// If the current state is the last one it cycles back to the initial state.
void progress() => firstChild<_SignpostSpriteComponent>()!.progress();
@override
Body createBody() {
final shape = CircleShape()..radius = 0.25;
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: initialPosition,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _SignpostSpriteComponent extends SpriteGroupComponent<SignpostSpriteState>
with HasGameRef {
_SignpostSpriteComponent()
: super(
anchor: Anchor.bottomCenter,
position: Vector2(0.65, 0.45),
);
void progress() => current = current?.next;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = <SignpostSpriteState, Sprite>{};
this.sprites = sprites;
for (final spriteState in SignpostSpriteState.values) {
// TODO(allisonryan0002): Support caching
// https://github.com/VGVentures/pinball/pull/204
// sprites[spriteState] = Sprite(
// gameRef.images.fromCache(spriteState.path),
// );
sprites[spriteState] = await gameRef.loadSprite(spriteState.path);
}
current = SignpostSpriteState.inactive;
size = sprites[current]!.originalSize / 10;
}
}

@ -43,7 +43,7 @@ class Slingshot extends BodyComponent with InitialPosition {
}) : _length = length, }) : _length = length,
_angle = angle, _angle = angle,
super( super(
priority: 1, priority: RenderPriority.slingshot,
children: [_SlinghsotSpriteComponent(spritePath, angle: angle)], children: [_SlinghsotSpriteComponent(spritePath, angle: angle)],
) { ) {
renderBody = false; renderBody = false;

@ -35,9 +35,12 @@ class Spaceship extends Forge2DBlueprint {
AndroidHead()..initialPosition = position, AndroidHead()..initialPosition = position,
_SpaceshipHole( _SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail, outsideLayer: Layer.spaceshipExitRail,
outsidePriority: Ball.spaceshipRailPriority, outsidePriority: RenderPriority.ballOnSpaceshipRail,
)..initialPosition = position - Vector2(5.2, -4.8), )..initialPosition = position - Vector2(5.2, -4.8),
_SpaceshipHole()..initialPosition = position - Vector2(-7.2, -0.8), _SpaceshipHole(
outsideLayer: Layer.board,
outsidePriority: RenderPriority.ballOnBoard,
)..initialPosition = position - Vector2(-7.2, -0.8),
SpaceshipWall()..initialPosition = position, SpaceshipWall()..initialPosition = position,
]); ]);
} }
@ -48,7 +51,7 @@ class Spaceship extends Forge2DBlueprint {
/// {@endtemplate} /// {@endtemplate}
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_saucer} /// {@macro spaceship_saucer}
SpaceshipSaucer() : super(priority: Ball.spaceshipPriority - 1) { SpaceshipSaucer() : super(priority: RenderPriority.spaceshipSaucer) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@ -94,7 +97,7 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_bridge} /// {@macro spaceship_bridge}
AndroidHead() AndroidHead()
: super( : super(
priority: Ball.spaceshipPriority + 1, priority: RenderPriority.androidHead,
children: [_AndroidHeadSpriteAnimation()], children: [_AndroidHeadSpriteAnimation()],
) { ) {
renderBody = false; renderBody = false;
@ -145,7 +148,7 @@ class _SpaceshipEntrance extends LayerSensor {
: super( : super(
insideLayer: Layer.spaceship, insideLayer: Layer.spaceship,
orientation: LayerEntranceOrientation.up, orientation: LayerEntranceOrientation.up,
insidePriority: Ball.spaceshipPriority, insidePriority: RenderPriority.ballOnSpaceship,
) { ) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }
@ -169,12 +172,12 @@ class _SpaceshipEntrance extends LayerSensor {
} }
class _SpaceshipHole extends LayerSensor { class _SpaceshipHole extends LayerSensor {
_SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1}) _SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
: super( : super(
insideLayer: Layer.spaceship, insideLayer: Layer.spaceship,
outsideLayer: outsideLayer, outsideLayer: outsideLayer,
orientation: LayerEntranceOrientation.down, orientation: LayerEntranceOrientation.down,
insidePriority: 4, insidePriority: RenderPriority.ballOnSpaceship,
outsidePriority: outsidePriority, outsidePriority: outsidePriority,
) { ) {
renderBody = false; renderBody = false;
@ -225,7 +228,7 @@ class _SpaceshipWallShape extends ChainShape {
/// {@endtemplate} /// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered { class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_wall} /// {@macro spaceship_wall}
SpaceshipWall() : super(priority: Ball.spaceshipPriority + 1) { SpaceshipWall() : super(priority: RenderPriority.spaceshipSaucerWall) {
layer = Layer.spaceship; layer = Layer.spaceship;
} }

@ -41,10 +41,7 @@ class SpaceshipRail extends Forge2DBlueprint {
/// Represents the spaceship drop rail from the [Spaceship]. /// Represents the spaceship drop rail from the [Spaceship].
class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered { class _SpaceshipRailRamp extends BodyComponent with InitialPosition, Layered {
_SpaceshipRailRamp() _SpaceshipRailRamp() : super(priority: RenderPriority.spaceshipRail) {
: super(
priority: Ball.spaceshipRailPriority - 1,
) {
layer = Layer.spaceshipExitRail; layer = Layer.spaceshipExitRail;
} }
@ -160,7 +157,8 @@ class _SpaceshipRailRampSpriteComponent extends SpriteComponent
} }
class _SpaceshipRailForeground extends SpriteComponent with HasGameRef { class _SpaceshipRailForeground extends SpriteComponent with HasGameRef {
_SpaceshipRailForeground() : super(priority: Ball.spaceshipRailPriority + 1); _SpaceshipRailForeground()
: super(priority: RenderPriority.spaceshipRailForeground);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -177,13 +175,9 @@ class _SpaceshipRailForeground extends SpriteComponent with HasGameRef {
} }
/// Represents the ground bases of the [_SpaceshipRailRamp]. /// Represents the ground bases of the [_SpaceshipRailRamp].
class _SpaceshipRailBase extends BodyComponent with InitialPosition, Layered { class _SpaceshipRailBase extends BodyComponent with InitialPosition {
_SpaceshipRailBase({required this.radius}) _SpaceshipRailBase({required this.radius}) {
: super(
priority: Ball.spaceshipRailPriority + 1,
) {
renderBody = false; renderBody = false;
layer = Layer.board;
} }
final double radius; final double radius;
@ -206,7 +200,7 @@ class _SpaceshipRailExit extends LayerSensor {
: super( : super(
orientation: LayerEntranceOrientation.down, orientation: LayerEntranceOrientation.down,
insideLayer: Layer.spaceshipExitRail, insideLayer: Layer.spaceshipExitRail,
insidePriority: Ball.spaceshipRailPriority, insidePriority: RenderPriority.ballOnSpaceshipRail,
) { ) {
renderBody = false; renderBody = false;
layer = Layer.spaceshipExitRail; layer = Layer.spaceshipExitRail;

@ -22,15 +22,14 @@ class SpaceshipRamp extends Forge2DBlueprint {
]); ]);
final rightOpening = _SpaceshipRampOpening( final rightOpening = _SpaceshipRampOpening(
// TODO(ruimiguel): set Board priority when defined. outsidePriority: RenderPriority.ballOnBoard,
outsidePriority: 1,
rotation: math.pi, rotation: math.pi,
) )
..initialPosition = Vector2(1.7, -19.8) ..initialPosition = Vector2(1.7, -19.8)
..layer = Layer.opening; ..layer = Layer.opening;
final leftOpening = _SpaceshipRampOpening( final leftOpening = _SpaceshipRampOpening(
outsideLayer: Layer.spaceship, outsideLayer: Layer.spaceship,
outsidePriority: Ball.spaceshipPriority, outsidePriority: RenderPriority.ballOnSpaceship,
rotation: math.pi, rotation: math.pi,
) )
..initialPosition = Vector2(-13.7, -18.6) ..initialPosition = Vector2(-13.7, -18.6)
@ -51,6 +50,7 @@ class SpaceshipRamp extends Forge2DBlueprint {
rightOpening, rightOpening,
leftOpening, leftOpening,
baseRight, baseRight,
_SpaceshipRampBackgroundRailingSpriteComponent(),
spaceshipRamp, spaceshipRamp,
spaceshipRampForegroundRailing, spaceshipRampForegroundRailing,
]); ]);
@ -59,7 +59,7 @@ class SpaceshipRamp extends Forge2DBlueprint {
class _SpaceshipRampBackground extends BodyComponent class _SpaceshipRampBackground extends BodyComponent
with InitialPosition, Layered { with InitialPosition, Layered {
_SpaceshipRampBackground() : super(priority: Ball.spaceshipRampPriority - 1) { _SpaceshipRampBackground() : super(priority: RenderPriority.spaceshipRamp) {
layer = Layer.spaceshipEntranceRamp; layer = Layer.spaceshipEntranceRamp;
} }
@ -119,12 +119,13 @@ class _SpaceshipRampBackground extends BodyComponent
renderBody = false; renderBody = false;
await add(_SpaceshipRampBackgroundRampSpriteComponent()); await add(_SpaceshipRampBackgroundRampSpriteComponent());
await add(_SpaceshipRampBackgroundRailingSpriteComponent());
} }
} }
class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
with HasGameRef { with HasGameRef {
_SpaceshipRampBackgroundRailingSpriteComponent()
: super(priority: RenderPriority.spaceshipRampBackgroundRailing);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
@ -171,7 +172,7 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
with InitialPosition, Layered { with InitialPosition, Layered {
_SpaceshipRampForegroundRailing() _SpaceshipRampForegroundRailing()
: super( : super(
priority: Ball.spaceshipRampPriority + 1, priority: RenderPriority.spaceshipRampForegroundRailing,
children: [_SpaceshipRampForegroundRailingSpriteComponent()], children: [_SpaceshipRampForegroundRailingSpriteComponent()],
) { ) {
layer = Layer.spaceshipEntranceRamp; layer = Layer.spaceshipEntranceRamp;
@ -287,7 +288,7 @@ class _SpaceshipRampOpening extends LayerSensor {
insideLayer: Layer.spaceshipEntranceRamp, insideLayer: Layer.spaceshipEntranceRamp,
outsideLayer: outsideLayer, outsideLayer: outsideLayer,
orientation: LayerEntranceOrientation.down, orientation: LayerEntranceOrientation.down,
insidePriority: Ball.spaceshipRampPriority, insidePriority: RenderPriority.ballOnSpaceshipRamp,
outsidePriority: outsidePriority, outsidePriority: outsidePriority,
) { ) {
renderBody = false; renderBody = false;

@ -20,7 +20,8 @@ class SparkyBumper extends BodyComponent with InitialPosition {
_minorRadius = minorRadius, _minorRadius = minorRadius,
_activeAssetPath = activeAssetPath, _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath, _inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent; _spriteComponent = spriteComponent,
super(priority: RenderPriority.sparkyBumper);
/// {@macro sparky_bumper} /// {@macro sparky_bumper}
SparkyBumper.a() SparkyBumper.a()

@ -23,7 +23,7 @@ class SparkyComputer extends Forge2DBlueprint {
} }
class _ComputerBase extends BodyComponent with InitialPosition { class _ComputerBase extends BodyComponent with InitialPosition {
_ComputerBase(); _ComputerBase() : super(priority: RenderPriority.computerBase);
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
@ -101,7 +101,7 @@ class _ComputerTopSpriteComponent extends SpriteComponent with HasGameRef {
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-12.45, -49.75), position: Vector2(-12.45, -49.75),
priority: 1, priority: RenderPriority.computerTop,
); );
@override @override

@ -41,6 +41,7 @@ flutter:
assets: assets:
- assets/images/ - assets/images/
- assets/images/ball/
- assets/images/baseboard/ - assets/images/baseboard/
- assets/images/boundary/ - assets/images/boundary/
- assets/images/dino/ - assets/images/dino/
@ -65,6 +66,7 @@ flutter:
- assets/images/sparky/bumper/c/ - assets/images/sparky/bumper/c/
- assets/images/backboard/ - assets/images/backboard/
- assets/images/google_word/ - assets/images/google_word/
- assets/images/signpost/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -1,22 +1,30 @@
import 'dart:async'; import 'dart:async';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class FlutterSignPostGame extends BasicBallGame with Traceable { class SignpostGame extends BasicBallGame with Traceable, TapDetector {
static const info = ''' static const info = '''
Shows how a FlutterSignPost is rendered. Shows how a Signpost is rendered.
- Activate the "trace" parameter to overlay the body. - Activate the "trace" parameter to overlay the body.
- Tap to progress the sprite.
'''; ''';
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
camera.followVector2(Vector2.zero()); camera.followVector2(Vector2.zero());
await add(FlutterSignPost()..priority = 1); await add(Signpost()..priority = 1);
await traceAllBodies(); await traceAllBodies();
} }
@override
void onTap() {
super.onTap();
firstChild<Signpost>()!.progress();
}
} }

@ -2,20 +2,19 @@ import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:sandbox/common/common.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/big_dash_nest_bumper_game.dart';
import 'package:sandbox/stories/flutter_forest/flutter_sign_post_game.dart'; import 'package:sandbox/stories/flutter_forest/signpost_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart'; import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart';
void addDashNestBumperStories(Dashbook dashbook) { void addDashNestBumperStories(Dashbook dashbook) {
dashbook.storiesOf('Flutter Forest') dashbook.storiesOf('Flutter Forest')
..add( ..add(
'Flutter Sign Post', 'Signpost',
(context) => GameWidget( (context) => GameWidget(
game: FlutterSignPostGame() game: SignpostGame()..trace = context.boolProperty('Trace', true),
..trace = context.boolProperty('Trace', true),
), ),
codeLink: buildSourceLink('flutter_forest/flutter_sign_post.dart'), codeLink: buildSourceLink('flutter_forest/signpost.dart'),
info: FlutterSignPostGame.info, info: SignpostGame.info,
) )
..add( ..add(
'Big Dash Nest Bumper', 'Big Dash Nest Bumper',

@ -10,7 +10,7 @@ class LaunchRampGame extends BasicBallGame {
LaunchRampGame() LaunchRampGame()
: super( : super(
color: Colors.blue, color: Colors.blue,
ballPriority: Ball.launchRampPriority, ballPriority: RenderPriority.ballOnLaunchRamp,
ballLayer: Layer.launcher, ballLayer: Layer.launcher,
); );

@ -10,7 +10,7 @@ class SpaceshipRailGame extends BasicBallGame {
SpaceshipRailGame() SpaceshipRailGame()
: super( : super(
color: Colors.blue, color: Colors.blue,
ballPriority: Ball.spaceshipRailPriority, ballPriority: RenderPriority.ballOnSpaceshipRail,
ballLayer: Layer.spaceshipExitRail, ballLayer: Layer.spaceshipExitRail,
); );

@ -10,7 +10,7 @@ class SpaceshipRampGame extends BasicBallGame {
SpaceshipRampGame() SpaceshipRampGame()
: super( : super(
color: Colors.blue, color: Colors.blue,
ballPriority: Ball.spaceshipRampPriority, ballPriority: RenderPriority.ballOnSpaceshipRamp,
ballLayer: Layer.spaceshipEntranceRamp, ballLayer: Layer.spaceshipEntranceRamp,
); );

@ -14,7 +14,7 @@ class BasicCameraZoomGame extends BasicGame with TapDetector {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
final sprite = await loadSprite(Assets.images.flutterSignPost.keyName); final sprite = await loadSprite(Assets.images.signpost.inactive.keyName);
await add( await add(
SpriteComponent( SpriteComponent(

@ -1,5 +1,6 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -169,20 +170,41 @@ void main() {
expect(ball.body.linearVelocity, equals(Vector2.zero())); expect(ball.body.linearVelocity, equals(Vector2.zero()));
ball.boost(Vector2.all(10)); await ball.boost(Vector2.all(10));
expect(ball.body.linearVelocity.x, greaterThan(0)); expect(ball.body.linearVelocity.x, greaterThan(0));
expect(ball.body.linearVelocity.y, greaterThan(0)); expect(ball.body.linearVelocity.y, greaterThan(0));
}); });
flameTester.test('adds fire effect components to the game', (game) async { flameTester.test('adds TurboChargeSpriteAnimation', (game) async {
final ball = Ball(baseColor: Colors.blue); final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball); await game.ensureAdd(ball);
ball.boost(Vector2.all(10)); await ball.boost(Vector2.all(10));
game.update(0); game.update(0);
await game.ready();
expect(game.children.whereType<FireEffect>().length, greaterThan(0)); expect(
ball.children.whereType<SpriteAnimationComponent>().single,
isNotNull,
);
});
flameTester.test('removes TurboChargeSpriteAnimation after it finishes',
(game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
await ball.boost(Vector2.all(10));
game.update(0);
final turboChargeSpriteAnimation =
ball.children.whereType<SpriteAnimationComponent>().single;
expect(ball.contains(turboChargeSpriteAnimation), isTrue);
game.update(turboChargeSpriteAnimation.animation!.totalDuration());
game.update(0.1);
expect(ball.contains(turboChargeSpriteAnimation), isFalse);
}); });
}); });
}); });

@ -17,7 +17,7 @@ void main() {
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
game.camera.zoom = 10; game.camera.zoom = 10;
final sprite = await game.loadSprite( final sprite = await game.loadSprite(
Assets.images.flutterSignPost.keyName, Assets.images.signpost.inactive.keyName,
); );
await game.add( await game.add(

@ -1,40 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.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() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('FlutterSignPost', () {
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.ensureAdd(FlutterSignPost());
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/flutter-sign-post.png'),
);
},
);
flameTester.test(
'loads correctly',
(game) async {
final flutterSignPost = FlutterSignPost();
await game.ready();
await game.ensureAdd(flutterSignPost);
expect(game.contains(flutterSignPost), isTrue);
},
);
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

@ -16,6 +16,7 @@ void main() {
'renders correctly', 'renders correctly',
setUp: (game, tester) async { setUp: (game, tester) async {
await game.addFromBlueprint(LaunchRamp()); await game.addFromBlueprint(LaunchRamp());
await game.ready();
game.camera.followVector2(Vector2.zero()); game.camera.followVector2(Vector2.zero());
game.camera.zoom = 4.1; game.camera.zoom = 4.1;
}, },

@ -148,7 +148,7 @@ void main() {
callback.begin(ball, sensor, MockContact()); callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = Layer.board); verify(() => ball.layer = Layer.board);
verify(() => ball.priority = Ball.boardPriority).called(1); verify(() => ball.priority = RenderPriority.ballOnBoard).called(1);
verify(ball.reorderChildren).called(1); verify(ball.reorderChildren).called(1);
}); });
@ -174,7 +174,7 @@ void main() {
callback.begin(ball, sensor, MockContact()); callback.begin(ball, sensor, MockContact());
verify(() => ball.layer = Layer.board); verify(() => ball.layer = Layer.board);
verify(() => ball.priority = Ball.boardPriority).called(1); verify(() => ball.priority = RenderPriority.ballOnBoard).called(1);
verify(ball.reorderChildren).called(1); verify(ball.reorderChildren).called(1);
}); });
}); });

@ -23,9 +23,21 @@ void main() {
game.camera.zoom = 4.1; game.camera.zoom = 4.1;
}, },
verify: (game, tester) async { verify: (game, tester) async {
final plunger = game.descendants().whereType<Plunger>().first;
plunger.pull();
game.update(1);
await tester.pump();
await expectLater( await expectLater(
find.byGame<TestGame>(), find.byGame<TestGame>(),
matchesGoldenFile('golden/plunger.png'), matchesGoldenFile('golden/plunger/pull.png'),
);
plunger.release();
game.update(1);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/plunger/release.png'),
); );
}, },
); );
@ -110,12 +122,17 @@ void main() {
}); });
group('pull', () { group('pull', () {
late Plunger plunger;
setUp(() {
plunger = Plunger(
compressionDistance: compressionDistance,
);
});
flameTester.test( flameTester.test(
'moves downwards when pull is called', 'moves downwards when pull is called',
(game) async { (game) async {
final plunger = Plunger(
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
plunger.pull(); plunger.pull();
@ -123,6 +140,18 @@ void main() {
expect(plunger.body.linearVelocity.x, isZero); expect(plunger.body.linearVelocity.x, isZero);
}, },
); );
flameTester.test(
'moves downwards when pull is called '
'and plunger is below its starting position', (game) async {
await game.ensureAdd(plunger);
plunger.pull();
plunger.release();
plunger.pull();
expect(plunger.body.linearVelocity.y, isPositive);
expect(plunger.body.linearVelocity.x, isZero);
});
}); });
group('release', () { group('release', () {

@ -0,0 +1,141 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('Signpost', () {
flameTester.test(
'loads correctly',
(game) async {
final signpost = Signpost();
await game.ready();
await game.ensureAdd(signpost);
expect(game.contains(signpost), isTrue);
},
);
group('renders correctly', () {
flameTester.testGameWidget(
'inactive sprite',
setUp: (game, tester) async {
final signpost = Signpost();
await game.ensureAdd(signpost);
expect(
signpost.firstChild<SpriteGroupComponent>()!.current,
SignpostSpriteState.inactive,
);
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/signpost/inactive.png'),
);
},
);
flameTester.testGameWidget(
'active1 sprite',
setUp: (game, tester) async {
final signpost = Signpost();
await game.ensureAdd(signpost);
signpost.progress();
expect(
signpost.firstChild<SpriteGroupComponent>()!.current,
SignpostSpriteState.active1,
);
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/signpost/active1.png'),
);
},
);
flameTester.testGameWidget(
'active2 sprite',
setUp: (game, tester) async {
final signpost = Signpost();
await game.ensureAdd(signpost);
signpost
..progress()
..progress();
expect(
signpost.firstChild<SpriteGroupComponent>()!.current,
SignpostSpriteState.active2,
);
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/signpost/active2.png'),
);
},
);
flameTester.testGameWidget(
'active3 sprite',
setUp: (game, tester) async {
final signpost = Signpost();
await game.ensureAdd(signpost);
signpost
..progress()
..progress()
..progress();
expect(
signpost.firstChild<SpriteGroupComponent>()!.current,
SignpostSpriteState.active3,
);
game.camera.followVector2(Vector2.zero());
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/signpost/active3.png'),
);
},
);
});
flameTester.test(
'progress correctly cycles through all sprites',
(game) async {
final signpost = Signpost();
await game.ready();
await game.ensureAdd(signpost);
final spriteComponent = signpost.firstChild<SpriteGroupComponent>()!;
expect(spriteComponent.current, SignpostSpriteState.inactive);
signpost.progress();
expect(spriteComponent.current, SignpostSpriteState.active1);
signpost.progress();
expect(spriteComponent.current, SignpostSpriteState.active2);
signpost.progress();
expect(spriteComponent.current, SignpostSpriteState.active3);
signpost.progress();
expect(spriteComponent.current, SignpostSpriteState.inactive);
},
);
});
}

@ -7,8 +7,9 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/app/app.dart'; import 'package:pinball/app/app.dart';
import 'package:pinball/landing/landing.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import '../../helpers/mocks.dart'; import '../../helpers/mocks.dart';
@ -21,16 +22,18 @@ void main() {
setUp(() { setUp(() {
leaderboardRepository = MockLeaderboardRepository(); leaderboardRepository = MockLeaderboardRepository();
pinballAudio = MockPinballAudio(); pinballAudio = MockPinballAudio();
when(pinballAudio.load).thenAnswer((_) => Future.value());
}); });
testWidgets('renders LandingPage', (tester) async { testWidgets('renders PinballGamePage', (tester) async {
await tester.pumpWidget( await tester.pumpWidget(
App( App(
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
pinballAudio: pinballAudio, pinballAudio: pinballAudio,
), ),
); );
expect(find.byType(LandingPage), findsOneWidget); expect(find.byType(PinballGamePage), findsOneWidget);
}); });
}); });
} }

@ -94,6 +94,7 @@ void main() {
final controller = WrappedBallController(ball, gameRef); final controller = WrappedBallController(ball, gameRef);
when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc); when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc);
when(() => ball.controller).thenReturn(controller); when(() => ball.controller).thenReturn(controller);
when(() => ball.boost(any())).thenAnswer((_) async {});
await controller.turboCharge(); await controller.turboCharge();
@ -109,6 +110,7 @@ void main() {
final controller = WrappedBallController(ball, gameRef); final controller = WrappedBallController(ball, gameRef);
when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc); when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc);
when(() => ball.controller).thenReturn(controller); when(() => ball.controller).thenReturn(controller);
when(() => ball.boost(any())).thenAnswer((_) async {});
await controller.turboCharge(); await controller.turboCharge();
@ -124,6 +126,7 @@ void main() {
final controller = WrappedBallController(ball, gameRef); final controller = WrappedBallController(ball, gameRef);
when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc); when(() => gameRef.read<GameBloc>()).thenReturn(gameBloc);
when(() => ball.controller).thenReturn(controller); when(() => ball.controller).thenReturn(controller);
when(() => ball.boost(any())).thenAnswer((_) async {});
await controller.turboCharge(); await controller.turboCharge();

@ -27,13 +27,13 @@ void main() {
group('loads', () { group('loads', () {
flameTester.test( flameTester.test(
'a FlutterSignPost', 'a Signpost',
(game) async { (game) async {
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
expect( expect(
flutterForest.descendants().whereType<FlutterSignPost>().length, flutterForest.descendants().whereType<Signpost>().length,
equals(1), equals(1),
); );
}, },

@ -5,40 +5,46 @@ import 'package:flame/game.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball/theme/theme.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void main() { void main() {
const theme = PinballTheme(characterTheme: DashTheme());
final game = PinballTestGame(); final game = PinballTestGame();
group('PinballGamePage', () { group('PinballGamePage', () {
testWidgets('renders PinballGameView', (tester) async { late ThemeCubit themeCubit;
final gameBloc = MockGameBloc(); late GameBloc gameBloc;
setUp(() {
themeCubit = MockThemeCubit();
gameBloc = MockGameBloc();
whenListen(
themeCubit,
const Stream<ThemeState>.empty(),
initialState: const ThemeState.initial(),
);
whenListen( whenListen(
gameBloc, gameBloc,
Stream.value(const GameState.initial()), Stream.value(const GameState.initial()),
initialState: const GameState.initial(), initialState: const GameState.initial(),
); );
});
testWidgets('renders PinballGameView', (tester) async {
await tester.pumpApp( await tester.pumpApp(
PinballGamePage(theme: theme, game: game), PinballGamePage(),
gameBloc: gameBloc, themeCubit: themeCubit,
); );
expect(find.byType(PinballGameView), findsOneWidget); expect(find.byType(PinballGameView), findsOneWidget);
}); });
testWidgets( testWidgets(
'renders the loading indicator while the assets load', 'renders the loading indicator while the assets load',
(tester) async { (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
final assetsManagerCubit = MockAssetsManagerCubit(); final assetsManagerCubit = MockAssetsManagerCubit();
final initialAssetsState = AssetsManagerState( final initialAssetsState = AssetsManagerState(
loadables: [Future<void>.value()], loadables: [Future<void>.value()],
@ -51,11 +57,27 @@ void main() {
); );
await tester.pumpApp( await tester.pumpApp(
PinballGamePage(theme: theme, game: game), PinballGameView(
gameBloc: gameBloc, game: game,
),
assetsManagerCubit: assetsManagerCubit, assetsManagerCubit: assetsManagerCubit,
themeCubit: themeCubit,
); );
expect(find.text('0.0'), findsOneWidget);
expect(
find.byWidgetPredicate(
(widget) =>
widget is LinearProgressIndicator && widget.value == 0.0,
),
findsOneWidget,
);
},
);
testWidgets(
'renders PinballGameLoadedView after resources have been loaded',
(tester) async {
final assetsManagerCubit = MockAssetsManagerCubit();
final loadedAssetsState = AssetsManagerState( final loadedAssetsState = AssetsManagerState(
loadables: [Future<void>.value()], loadables: [Future<void>.value()],
@ -67,11 +89,20 @@ void main() {
initialState: loadedAssetsState, initialState: loadedAssetsState,
); );
await tester.pump(); await tester.pumpApp(
expect(find.byType(PinballGameView), findsOneWidget); PinballGameView(
}, game: game,
),
assetsManagerCubit: assetsManagerCubit,
themeCubit: themeCubit,
gameBloc: gameBloc,
); );
await tester.pump();
expect(find.byType(PinballGameLoadedView), findsOneWidget);
});
group('route', () { group('route', () {
Future<void> pumpRoute({ Future<void> pumpRoute({
required WidgetTester tester, required WidgetTester tester,
@ -85,7 +116,6 @@ void main() {
onPressed: () { onPressed: () {
Navigator.of(context).push<void>( Navigator.of(context).push<void>(
PinballGamePage.route( PinballGamePage.route(
theme: theme,
isDebugMode: isDebugMode, isDebugMode: isDebugMode,
), ),
); );
@ -95,6 +125,7 @@ void main() {
}, },
), ),
), ),
themeCubit: themeCubit,
); );
await tester.tap(find.text('Tap me')); await tester.tap(find.text('Tap me'));

@ -1,8 +1,9 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/theme/theme.dart';
import '../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
void main() { void main() {
group('PlayButtonOverlay', () { group('PlayButtonOverlay', () {
@ -31,5 +32,15 @@ void main() {
verify(gameFlowController.start).called(1); verify(gameFlowController.start).called(1);
}); });
testWidgets('displays CharacterSelectionDialog when tapped',
(tester) async {
await tester.pumpApp(PlayButtonOverlay(game: game));
await tester.tap(find.text('Play'));
await tester.pump();
expect(find.byType(CharacterSelectionDialog), findsOneWidget);
});
}); });
} }

@ -1,71 +0,0 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/landing/landing.dart';
import '../../helpers/helpers.dart';
void main() {
group('LandingPage', () {
testWidgets('renders correctly', (tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
await tester.pumpApp(LandingPage());
expect(find.byType(TextButton), findsNWidgets(2));
expect(find.text(l10n.play), findsOneWidget);
expect(find.text(l10n.howToPlay), findsOneWidget);
});
testWidgets('tapping on play button navigates to CharacterSelectionPage',
(tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
await tester.pumpApp(
LandingPage(),
navigator: navigator,
);
await tester.tap(find.widgetWithText(TextButton, l10n.play));
verify(() => navigator.push<void>(any())).called(1);
});
testWidgets('tapping on how to play button displays dialog with controls',
(tester) async {
final l10n = await AppLocalizations.delegate.load(Locale('en'));
await tester.pumpApp(LandingPage());
await tester.tap(find.widgetWithText(TextButton, l10n.howToPlay));
await tester.pump();
expect(find.byType(Dialog), findsOneWidget);
});
});
group('KeyIndicator', () {
testWidgets('fromKeyName renders correctly', (tester) async {
const keyName = 'A';
await tester.pumpApp(
KeyIndicator.fromKeyName(keyName: keyName),
);
expect(find.text(keyName), findsOneWidget);
});
testWidgets('fromIcon renders correctly', (tester) async {
const keyIcon = Icons.keyboard_arrow_down;
await tester.pumpApp(
KeyIndicator.fromIcon(keyIcon: keyIcon),
);
expect(find.byIcon(keyIcon), findsOneWidget);
});
});
}

@ -0,0 +1,39 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/start_game/start_game.dart';
import '../../helpers/helpers.dart';
void main() {
group('HowToPlayDialog', () {
testWidgets('displays dialog', (tester) async {
await tester.pumpApp(HowToPlayDialog());
expect(find.byType(Dialog), findsOneWidget);
});
});
group('KeyIndicator', () {
testWidgets('fromKeyName renders correctly', (tester) async {
const keyName = 'A';
await tester.pumpApp(
KeyIndicator.fromKeyName(keyName: keyName),
);
expect(find.text(keyName), findsOneWidget);
});
testWidgets('fromIcon renders correctly', (tester) async {
const keyIcon = Icons.keyboard_arrow_down;
await tester.pumpApp(
KeyIndicator.fromIcon(keyIcon: keyIcon),
);
expect(find.byIcon(keyIcon), findsOneWidget);
});
});
}

@ -4,6 +4,7 @@ import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart'; import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball/theme/theme.dart'; import 'package:pinball/theme/theme.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
@ -24,7 +25,7 @@ void main() {
group('CharacterSelectionPage', () { group('CharacterSelectionPage', () {
testWidgets('renders CharacterSelectionView', (tester) async { testWidgets('renders CharacterSelectionView', (tester) async {
await tester.pumpApp( await tester.pumpApp(
CharacterSelectionPage(), CharacterSelectionDialog(),
themeCubit: themeCubit, themeCubit: themeCubit,
); );
expect(find.byType(CharacterSelectionView), findsOneWidget); expect(find.byType(CharacterSelectionView), findsOneWidget);
@ -38,7 +39,7 @@ void main() {
return ElevatedButton( return ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context) Navigator.of(context)
.push<void>(CharacterSelectionPage.route()); .push<void>(CharacterSelectionDialog.route());
}, },
child: Text('Tap me'), child: Text('Tap me'),
); );
@ -51,7 +52,7 @@ void main() {
await tester.tap(find.text('Tap me')); await tester.tap(find.text('Tap me'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(CharacterSelectionPage), findsOneWidget); expect(find.byType(CharacterSelectionDialog), findsOneWidget);
}); });
}); });
@ -82,20 +83,17 @@ void main() {
verify(() => themeCubit.characterSelected(SparkyTheme())).called(1); verify(() => themeCubit.characterSelected(SparkyTheme())).called(1);
}); });
testWidgets('navigates to PinballGamePage when start is tapped', testWidgets('displays how to play dialog when start is tapped',
(tester) async { (tester) async {
final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
await tester.pumpApp( await tester.pumpApp(
CharacterSelectionView(), CharacterSelectionView(),
themeCubit: themeCubit, themeCubit: themeCubit,
navigator: navigator,
); );
await tester.ensureVisible(find.byType(TextButton)); await tester.ensureVisible(find.byType(TextButton));
await tester.tap(find.byType(TextButton)); await tester.tap(find.byType(TextButton));
await tester.pumpAndSettle();
verify(() => navigator.push<void>(any())).called(1); expect(find.byType(HowToPlayDialog), findsOneWidget);
}); });
}); });

Loading…
Cancel
Save