Merge branch 'main' into feat/animate-dino-mouth

pull/229/head
Allison Ryan 3 years ago
commit 2d028820fa

3
.gitignore vendored

@ -131,6 +131,3 @@ app.*.map.json
test/.test_runner.dart
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_localizations/flutter_localizations.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/game.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';
class App extends StatelessWidget {
@ -34,20 +35,17 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _pinballAudio),
],
child: MaterialApp(
child: BlocProvider(
create: (context) => ThemeCubit(),
child: const MaterialApp(
title: 'I/O Pinball',
theme: ThemeData(
appBarTheme: const AppBarTheme(color: Color(0xFF13B9FF)),
colorScheme: ColorScheme.fromSwatch(
accentColor: const Color(0xFF13B9FF),
),
),
localizationsDelegates: const [
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: const LandingPage(),
home: PinballGamePage(),
),
),
);
}

@ -5,14 +5,12 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template alien_zone}
/// Area positioned below [Spaceship] where the [Ball]
/// can bounce off [AlienBumper]s.
///
/// When a [Ball] hits [AlienBumper]s, they toggle between activated and
/// deactivated states.
/// When a [Ball] hits an [AlienBumper], the bumper animates.
/// {@endtemplate}
class AlienZone extends Component with HasGameRef<PinballGame> {
/// {@macro alien_zone}
@ -22,12 +20,12 @@ class AlienZone extends Component with HasGameRef<PinballGame> {
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_ControlledAlienBumperBallContactCallback());
gameRef.addContactCallback(AlienBumperBallContactCallback());
final lowerBumper = ControlledAlienBumper.a()
..initialPosition = Vector2(-32.52, -9.34);
final upperBumper = ControlledAlienBumper.b()
..initialPosition = Vector2(-22.89, -17.43);
final lowerBumper = _AlienBumper.a()
..initialPosition = Vector2(-32.52, -9.1);
final upperBumper = _AlienBumper.b()
..initialPosition = Vector2(-22.89, -17.35);
await addAll([
lowerBumper,
@ -36,60 +34,27 @@ class AlienZone extends Component with HasGameRef<PinballGame> {
}
}
/// {@template controlled_alien_bumper}
/// [AlienBumper] with [_AlienBumperController] attached.
/// {@endtemplate}
@visibleForTesting
class ControlledAlienBumper extends AlienBumper
with Controls<_AlienBumperController>, ScorePoints {
/// {@macro controlled_alien_bumper}
ControlledAlienBumper.a() : super.a() {
controller = _AlienBumperController(this);
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _AlienBumper extends AlienBumper with ScorePoints {
_AlienBumper.a() : super.a();
/// {@macro controlled_alien_bumper}
ControlledAlienBumper.b() : super.b() {
controller = _AlienBumperController(this);
}
_AlienBumper.b() : super.b();
@override
// TODO(ruimiguel): change points when get final points map.
int get points => 20;
}
/// {@template alien_bumper_controller}
/// Controls a [AlienBumper].
/// {@endtemplate}
class _AlienBumperController extends ComponentController<AlienBumper>
with HasGameRef<PinballGame> {
/// {@macro alien_bumper_controller}
_AlienBumperController(AlienBumper alienBumper) : super(alienBumper);
/// Flag for activated state of the [AlienBumper].
///
/// Used to toggle [AlienBumper]s' state between activated and deactivated.
bool isActivated = false;
/// Registers when a [AlienBumper] is hit by a [Ball].
void hit() {
if (isActivated) {
component.deactivate();
} else {
component.activate();
}
isActivated = !isActivated;
}
}
/// Listens when a [Ball] bounces bounces against a [AlienBumper].
class _ControlledAlienBumperBallContactCallback
extends ContactCallback<Controls<_AlienBumperController>, Ball> {
/// Listens when a [Ball] bounces against an [AlienBumper].
@visibleForTesting
class AlienBumperBallContactCallback
extends ContactCallback<AlienBumper, Ball> {
@override
void begin(
Controls<_AlienBumperController> controlledAlienBumper,
AlienBumper alienBumper,
Ball _,
Contact __,
) {
controlledAlienBumper.controller.hit();
alienBumper.animate();
}
}

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

@ -9,7 +9,6 @@ export 'flutter_forest.dart';
export 'game_flow_controller.dart';
export 'google_word.dart';
export 'launcher.dart';
export 'score_effect_controller.dart';
export 'score_points.dart';
export 'sparky_fire_zone.dart';
export 'wall.dart';

@ -18,7 +18,7 @@ class ControlledBall extends Ball with Controls<BallController> {
required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) {
controller = BallController(this);
priority = Ball.launchRampPriority;
priority = RenderPriority.ballOnLaunchRamp;
layer = Layer.launcher;
}
@ -31,13 +31,13 @@ class ControlledBall extends Ball with Controls<BallController> {
required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) {
controller = BallController(this);
priority = Ball.boardPriority;
priority = RenderPriority.ballOnBoard;
}
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = DebugBallController(this);
priority = Ball.boardPriority;
priority = RenderPriority.ballOnBoard;
}
}

@ -11,7 +11,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// can bounce off [DashNestBumper]s.
///
/// When all [DashNestBumper]s are hit at least once, the [GameBonus.dashNest]
/// is awarded, and the [BigDashNestBumper] releases a new [Ball].
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
/// {@endtemplate}
class FlutterForest extends Component
with Controls<_FlutterForestController>, HasGameRef<PinballGame> {
@ -25,18 +25,18 @@ class FlutterForest extends Component
await super.onLoad();
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 = _DashNestBumper.main()
..initialPosition = Vector2(18.55, -59.35);
final smallLeftNest = _SmallDashNestBumper.a()
final smallLeftNest = _DashNestBumper.a()
..initialPosition = Vector2(8.95, -51.95);
final smallRightNest = _SmallDashNestBumper.b()
final smallRightNest = _DashNestBumper.b()
..initialPosition = Vector2(23.3, -46.75);
final dashAnimatronic = DashAnimatronic()..position = Vector2(20, -66);
await addAll([
signPost,
signpost,
smallLeftNest,
smallRightNest,
bigNest,
@ -70,8 +70,6 @@ class _FlutterForestController extends ComponentController<FlutterForest>
}
Future<void> _addBonusBall() async {
// TODO(alestiago): Remove hardcoded duration.
await Future<void>.delayed(const Duration(milliseconds: 700));
await gameRef.add(
ControlledBall.bonus(theme: gameRef.theme)
..initialPosition = Vector2(17.2, -52.7),
@ -81,15 +79,12 @@ class _FlutterForestController extends ComponentController<FlutterForest>
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _BigDashNestBumper extends BigDashNestBumper with ScorePoints {
@override
int get points => 20;
}
class _DashNestBumper extends DashNestBumper with ScorePoints {
_DashNestBumper.main() : super.main();
class _SmallDashNestBumper extends SmallDashNestBumper with ScorePoints {
_SmallDashNestBumper.a() : super.a();
_DashNestBumper.a() : super.a();
_SmallDashNestBumper.b() : super.b();
_DashNestBumper.b() : super.b();
@override
int get points => 20;

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

@ -1,45 +0,0 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template score_effect_controller}
/// A [ComponentController] responsible for adding [ScoreText]s
/// on the game screen when the user earns points.
/// {@endtemplate}
class ScoreEffectController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState> {
/// {@macro score_effect_controller}
ScoreEffectController(PinballGame component) : super(component);
int _lastScore = 0;
final _random = Random();
double _noise() {
return _random.nextDouble() * 5 * (_random.nextBool() ? -1 : 1);
}
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.score != newState.score;
}
@override
void onNewState(GameState state) {
final newScore = state.score - _lastScore;
_lastScore = state.score;
component.add(
ScoreText(
text: newScore.toString(),
position: Vector2(
_noise(),
_noise() + (BoardDimensions.bounds.topCenter.dy + 10),
),
),
);
}
}

@ -30,11 +30,18 @@ class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
@override
void begin(
Ball _,
Ball ball,
ScorePoints scorePoints,
Contact __,
Contact _,
) {
_gameRef.read<GameBloc>().add(Scored(points: scorePoints.points));
_gameRef.audio.score();
_gameRef.add(
ScoreText(
text: scorePoints.points.toString(),
position: ball.body.position,
),
);
}
}

@ -5,14 +5,12 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_fire_zone}
/// Area positioned at the top left of the [Board] where the [Ball]
/// can bounce off [SparkyBumper]s.
///
/// When a [Ball] hits [SparkyBumper]s, they toggle between activated and
/// deactivated states.
/// When a [Ball] hits [SparkyBumper]s, the bumper animates.
/// {@endtemplate}
class SparkyFireZone extends Component with HasGameRef<PinballGame> {
/// {@macro sparky_fire_zone}
@ -22,14 +20,14 @@ class SparkyFireZone extends Component with HasGameRef<PinballGame> {
Future<void> onLoad() async {
await super.onLoad();
gameRef.addContactCallback(_ControlledSparkyBumperBallContactCallback());
gameRef.addContactCallback(SparkyBumperBallContactCallback());
final lowerLeftBumper = ControlledSparkyBumper.a()
..initialPosition = Vector2(-23.15, -41.65);
final upperLeftBumper = ControlledSparkyBumper.b()
..initialPosition = Vector2(-21.25, -58.15);
final rightBumper = ControlledSparkyBumper.c()
..initialPosition = Vector2(-3.56, -53.051);
final lowerLeftBumper = _SparkyBumper.a()
..initialPosition = Vector2(-22.9, -41.65);
final upperLeftBumper = _SparkyBumper.b()
..initialPosition = Vector2(-21.25, -57.9);
final rightBumper = _SparkyBumper.c()
..initialPosition = Vector2(-3.3, -52.55);
await addAll([
lowerLeftBumper,
@ -39,65 +37,29 @@ class SparkyFireZone extends Component with HasGameRef<PinballGame> {
}
}
/// {@template controlled_sparky_bumper}
/// [SparkyBumper] with [_SparkyBumperController] attached.
/// {@endtemplate}
@visibleForTesting
class ControlledSparkyBumper extends SparkyBumper
with Controls<_SparkyBumperController>, ScorePoints {
///{@macro controlled_sparky_bumper}
ControlledSparkyBumper.a() : super.a() {
controller = _SparkyBumperController(this);
}
// TODO(alestiago): Revisit ScorePoints logic once the FlameForge2D
// ContactCallback process is enhanced.
class _SparkyBumper extends SparkyBumper with ScorePoints {
_SparkyBumper.a() : super.a();
///{@macro controlled_sparky_bumper}
ControlledSparkyBumper.b() : super.b() {
controller = _SparkyBumperController(this);
}
_SparkyBumper.b() : super.b();
///{@macro controlled_sparky_bumper}
ControlledSparkyBumper.c() : super.c() {
controller = _SparkyBumperController(this);
}
_SparkyBumper.c() : super.c();
@override
int get points => 20;
}
/// {@template sparky_bumper_controller}
/// Controls a [SparkyBumper].
/// {@endtemplate}
class _SparkyBumperController extends ComponentController<SparkyBumper>
with HasGameRef<PinballGame> {
/// {@macro sparky_bumper_controller}
_SparkyBumperController(ControlledSparkyBumper controlledSparkyBumper)
: super(controlledSparkyBumper);
/// Flag for activated state of the [SparkyBumper].
///
/// Used to toggle [SparkyBumper]s' state between activated and deactivated.
bool isActivated = false;
/// Registers when a [SparkyBumper] is hit by a [Ball].
void hit() {
if (isActivated) {
component.deactivate();
} else {
component.activate();
}
isActivated = !isActivated;
}
}
/// Listens when a [Ball] bounces bounces against a [SparkyBumper].
class _ControlledSparkyBumperBallContactCallback
extends ContactCallback<Controls<_SparkyBumperController>, Ball> {
@visibleForTesting
class SparkyBumperBallContactCallback
extends ContactCallback<SparkyBumper, Ball> {
@override
void begin(
Controls<_SparkyBumperController> controlledSparkyBumper,
SparkyBumper sparkyBumper,
Ball _,
Contact __,
) {
controlledSparkyBumper.controller.hit();
sparkyBumper.animate();
}
}

@ -9,7 +9,10 @@ extension PinballGameAssetsX on PinballGame {
return [
images.load(components.Assets.images.ball.ball.keyName),
images.load(components.Assets.images.ball.flameEffect.keyName),
images.load(components.Assets.images.flutterSignPost.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.right.keyName),
images.load(components.Assets.images.baseboard.left.keyName),
@ -38,16 +41,37 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.boundary.outer.keyName),
images.load(components.Assets.images.spaceship.saucer.keyName),
images.load(components.Assets.images.spaceship.bridge.keyName),
images.load(components.Assets.images.spaceship.ramp.main.keyName),
images.load(components.Assets.images.spaceship.ramp.boardOpening.keyName),
images.load(
components.Assets.images.spaceship.ramp.railingForeground.keyName,
),
images.load(
components.Assets.images.spaceship.ramp.railingBackground.keyName,
),
images.load(components.Assets.images.spaceship.ramp.main.keyName),
images
.load(components.Assets.images.spaceship.ramp.arrow.inactive.keyName),
images.load(
components.Assets.images.spaceship.ramp.railingForeground.keyName,
components.Assets.images.spaceship.ramp.arrow.active1.keyName,
),
images.load(
components.Assets.images.spaceship.ramp.arrow.active2.keyName,
),
images.load(
components.Assets.images.spaceship.ramp.arrow.active3.keyName,
),
images.load(
components.Assets.images.spaceship.ramp.arrow.active4.keyName,
),
images.load(
components.Assets.images.spaceship.ramp.arrow.active5.keyName,
),
images.load(components.Assets.images.spaceship.rail.main.keyName),
images.load(components.Assets.images.spaceship.rail.foreground.keyName),
images.load(components.Assets.images.alienBumper.a.active.keyName),
images.load(components.Assets.images.alienBumper.a.inactive.keyName),
images.load(components.Assets.images.alienBumper.b.active.keyName),
images.load(components.Assets.images.alienBumper.b.inactive.keyName),
images.load(components.Assets.images.chromeDino.mouth.keyName),
images.load(components.Assets.images.chromeDino.head.keyName),
images.load(components.Assets.images.plunger.plunger.keyName),

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

@ -5,32 +5,40 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGamePage extends StatelessWidget {
const PinballGamePage({
Key? key,
required this.theme,
required this.game,
this.isDebugMode = kDebugMode,
}) : super(key: key);
final PinballTheme theme;
final PinballGame game;
final bool isDebugMode;
static Route route({
required PinballTheme theme,
bool isDebugMode = kDebugMode,
}) {
return MaterialPageRoute<void>(
builder: (context) {
return PinballGamePage(
isDebugMode: isDebugMode,
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = context.read<ThemeCubit>().state.theme;
final audio = context.read<PinballAudio>();
final pinballAudio = context.read<PinballAudio>();
final game = isDebugMode
? DebugPinballGame(theme: theme, audio: audio)
: PinballGame(theme: theme, audio: audio);
final pinballAudio = context.read<PinballAudio>();
final loadables = [
...game.preLoadAssets(),
pinballAudio.load(),
@ -38,21 +46,15 @@ class PinballGamePage extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => StartGameBloc(game: game)),
BlocProvider(create: (_) => GameBloc()),
BlocProvider(
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 {
@ -65,18 +67,51 @@ class PinballGameView extends StatelessWidget {
@override
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(
body: Center(
child: Text(
loadingProgress.toString(),
backgroundColor: Colors.blue,
body: isLoading
? 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(
children: [
Positioned.fill(

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:pinball/game/pinball_game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
/// {@template play_button_overlay}
/// [Widget] that renders the button responsible to starting the game
@ -18,9 +19,27 @@ class PlayButtonOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Center(
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),
),
);

@ -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),
TextButton(
onPressed: () => Navigator.of(context).push<void>(
CharacterSelectionPage.route(),
CharacterSelectionDialog.route(),
),
child: Text(l10n.retry),
),

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

@ -2,42 +2,9 @@
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
class LandingPage extends StatelessWidget {
const LandingPage({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);
class HowToPlayDialog extends StatelessWidget {
const HowToPlayDialog({Key? key}) : super(key: key);
@override
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 'view/view.dart';

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 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: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -21,17 +21,13 @@ class $AssetsImagesGen {
$AssetsImagesDashGen get dash => const $AssetsImagesDashGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
/// File path: assets/images/flutter_sign_post.png
AssetGenImage get flutterSignPost =>
const AssetGenImage('assets/images/flutter_sign_post.png');
$AssetsImagesGoogleWordGen get googleWord =>
const $AssetsImagesGoogleWordGen();
$AssetsImagesKickerGen get kicker => const $AssetsImagesKickerGen();
$AssetsImagesLaunchRampGen get launchRamp =>
const $AssetsImagesLaunchRampGen();
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
@ -209,6 +205,26 @@ class $AssetsImagesPlungerGen {
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 {
const $AssetsImagesSlingshotGen();
@ -295,6 +311,9 @@ class $AssetsImagesSpaceshipRailGen {
class $AssetsImagesSpaceshipRampGen {
const $AssetsImagesSpaceshipRampGen();
$AssetsImagesSpaceshipRampArrowGen get arrow =>
const $AssetsImagesSpaceshipRampArrowGen();
/// File path: assets/images/spaceship/ramp/board-opening.png
AssetGenImage get boardOpening =>
const AssetGenImage('assets/images/spaceship/ramp/board-opening.png');
@ -368,6 +387,34 @@ class $AssetsImagesDashBumperMainGen {
const AssetGenImage('assets/images/dash/bumper/main/inactive.png');
}
class $AssetsImagesSpaceshipRampArrowGen {
const $AssetsImagesSpaceshipRampArrowGen();
/// File path: assets/images/spaceship/ramp/arrow/active1.png
AssetGenImage get active1 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active1.png');
/// File path: assets/images/spaceship/ramp/arrow/active2.png
AssetGenImage get active2 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active2.png');
/// File path: assets/images/spaceship/ramp/arrow/active3.png
AssetGenImage get active3 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active3.png');
/// File path: assets/images/spaceship/ramp/arrow/active4.png
AssetGenImage get active4 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active4.png');
/// File path: assets/images/spaceship/ramp/arrow/active5.png
AssetGenImage get active5 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active5.png');
/// File path: assets/images/spaceship/ramp/arrow/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/inactive.png');
}
class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen();

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

@ -1,16 +1,14 @@
import 'package:pinball_components/gen/fonts.gen.dart';
String _prefixFont(String font) {
return 'packages/pinball_components/$font';
}
const String _fontPath = 'packages/pinball_components/';
/// Class with the fonts available on the pinball game
class PinballFonts {
PinballFonts._();
/// 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
static final String pixeloidSans = _prefixFont(FontFamily.pixeloidMono);
static const String pixeloidSans = '$_fontPath/${FontFamily.pixeloidSans}';
}

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

@ -1,36 +1,41 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template alien_bumper}
/// Bumper for Alien area.
/// Bumper for area under the [Spaceship].
/// {@endtemplate}
// TODO(ruimiguel): refactor later to unify with DashBumpers.
class AlienBumper extends BodyComponent with InitialPosition {
/// {@macro alien_bumper}
AlienBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
required String onAssetPath,
required String offAssetPath,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
_activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent;
super(
priority: RenderPriority.alienBumper,
children: [
_AlienBumperSpriteGroupComponent(
onAssetPath: onAssetPath,
offAssetPath: offAssetPath,
),
],
) {
renderBody = false;
}
/// {@macro alien_bumper}
AlienBumper.a()
: this._(
majorRadius: 3.52,
minorRadius: 2.97,
activeAssetPath: Assets.images.alienBumper.a.active.keyName,
inactiveAssetPath: Assets.images.alienBumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.1),
),
onAssetPath: Assets.images.alienBumper.a.active.keyName,
offAssetPath: Assets.images.alienBumper.a.inactive.keyName,
);
/// {@macro alien_bumper}
@ -38,32 +43,12 @@ class AlienBumper extends BodyComponent with InitialPosition {
: this._(
majorRadius: 3.19,
minorRadius: 2.79,
activeAssetPath: Assets.images.alienBumper.b.active.keyName,
inactiveAssetPath: Assets.images.alienBumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.1),
),
onAssetPath: Assets.images.alienBumper.b.active.keyName,
offAssetPath: Assets.images.alienBumper.b.inactive.keyName,
);
final double _majorRadius;
final double _minorRadius;
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await _loadSprites();
deactivate();
await add(_spriteComponent);
}
@override
Body createBody() {
@ -84,25 +69,53 @@ class AlienBumper extends BodyComponent with InitialPosition {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
Future<void> _loadSprites() async {
// TODO(alestiago): I think ideally we would like to do:
// Sprite(path).load so we don't require to store the activeAssetPath and
// the inactive assetPath.
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
/// Animates the [AlienBumper].
Future<void> animate() async {
final spriteGroupComponent = firstChild<_AlienBumperSpriteGroupComponent>()
?..current = AlienBumperSpriteState.inactive;
await Future<void>.delayed(const Duration(milliseconds: 50));
spriteGroupComponent?.current = AlienBumperSpriteState.active;
}
}
/// Activates the [AlienBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
}
/// Indicates the [AlienBumper]'s current sprite state.
@visibleForTesting
enum AlienBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}
class _AlienBumperSpriteGroupComponent
extends SpriteGroupComponent<AlienBumperSpriteState> with HasGameRef {
_AlienBumperSpriteGroupComponent({
required String onAssetPath,
required String offAssetPath,
}) : _onAssetPath = onAssetPath,
_offAssetPath = offAssetPath,
super(
anchor: Anchor.center,
position: Vector2(0, -0.1),
);
final String _onAssetPath;
final String _offAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = {
AlienBumperSpriteState.active:
Sprite(gameRef.images.fromCache(_onAssetPath)),
AlienBumperSpriteState.inactive:
Sprite(gameRef.images.fromCache(_offAssetPath)),
};
this.sprites = sprites;
/// Deactivates the [AlienBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
current = AlienBumperSpriteState.active;
size = sprites[current]!.originalSize / 10;
}
}

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

@ -29,21 +29,6 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
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].
static final Vector2 size = Vector2.all(4.13);
@ -152,7 +137,7 @@ class _TurboChargeSpriteAnimationComponent extends SpriteAnimationComponent
_TurboChargeSpriteAnimationComponent()
: super(
anchor: const Anchor(0.53, 0.72),
priority: Ball.boardPriority + 1,
priority: RenderPriority.turboChargeFlame,
removeOnFinish: true,
);

@ -26,8 +26,7 @@ class _BottomBoundary extends BodyComponent with InitialPosition {
/// {@macro bottom_boundary}
_BottomBoundary()
: super(
// TODO(ruimiguel): set final priority when RenderPriority PR merged.
priority: Ball.boardPriority + 2,
priority: RenderPriority.bottomBoundary,
children: [_BottomBoundarySpriteComponent()],
) {
renderBody = false;
@ -91,7 +90,7 @@ class _OuterBoundary extends BodyComponent with InitialPosition {
/// {@macro outer_boundary}
_OuterBoundary()
: super(
priority: Ball.launchRampPriority - 1,
priority: RenderPriority.outerBoudary,
children: [_OuterBoundarySpriteComponent()],
) {
renderBody = false;

@ -12,10 +12,12 @@ import 'package:pinball_components/pinball_components.dart';
/// {@endtemplate}
class ChromeDino extends BodyComponent with InitialPosition {
/// {@macro chrome_dino}
ChromeDino() {
ChromeDino()
: super(
// 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.
static final size = Vector2(5, 2.5);

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

@ -2,7 +2,7 @@ import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template dash_animatronic}
/// Animated Dash that sits on top of the [BigDashNestBumper].
/// Animated Dash that sits on top of the [DashNestBumper.main].
/// {@endtemplate}
class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
/// {@macro dash_animatronic}
@ -10,6 +10,7 @@ class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
: super(
anchor: Anchor.center,
playing: false,
priority: RenderPriority.dashAnimatronic,
);
@override

@ -2,138 +2,76 @@ import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template dash_nest_bumper}
/// Bumper with a nest appearance.
/// {@endtemplate}
abstract class DashNestBumper extends BodyComponent with InitialPosition {
class DashNestBumper extends BodyComponent with InitialPosition {
/// {@macro dash_nest_bumper}
DashNestBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent;
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
Future<void> _loadSprites() async {
// TODO(alestiago): I think ideally we would like to do:
// Sprite(path).load so we don't require to store the activeAssetPath and
// the inactive assetPath.
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
}
/// Activates the [DashNestBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
}
/// Deactivates the [DashNestBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
// TODO(erickzanardo): Look into using onNewState instead.
// Currently doing: onNewState(gameRef.read<GameState>()) will throw an
// `Exception: build context is not available yet`
deactivate();
await add(_spriteComponent);
required Vector2 spritePosition,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
super(
priority: RenderPriority.dashBumper,
children: [
_DashNestBumperSpriteGroupComponent(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
position: spritePosition,
),
],
) {
renderBody = false;
}
}
/// {@macro dash_nest_bumper}
class BigDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
BigDashNestBumper()
: super._(
activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.3),
),
);
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
DashNestBumper.main()
: this._(
majorRadius: 5.1,
minorRadius: 3.75,
)..rotate(math.pi / 1.9);
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
/// {@macro dash_nest_bumper}
class SmallDashNestBumper extends DashNestBumper {
/// {@macro dash_nest_bumper}
SmallDashNestBumper._({
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
}) : super._(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
spriteComponent: spriteComponent,
activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spritePosition: Vector2(0, -0.3),
);
/// {@macro dash_nest_bumper}
SmallDashNestBumper.a()
DashNestBumper.a()
: this._(
majorRadius: 3,
minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
spritePosition: Vector2(0.35, -1.2),
);
/// {@macro dash_nest_bumper}
SmallDashNestBumper.b()
DashNestBumper.b()
: this._(
majorRadius: 3,
minorRadius: 2.5,
activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0.35, -1.2),
),
spritePosition: Vector2(0.35, -1.2),
);
final double _majorRadius;
final double _minorRadius;
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 3,
minorRadius: 2.25,
)..rotate(math.pi / 2);
final fixtureDef = FixtureDef(
shape,
restitution: 4,
);
majorRadius: _majorRadius,
minorRadius: _minorRadius,
)..rotate(math.pi / 1.9);
final fixtureDef = FixtureDef(shape, restitution: 4);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
@ -141,4 +79,58 @@ class SmallDashNestBumper extends DashNestBumper {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
/// Activates the [DashNestBumper].
void activate() {
firstChild<_DashNestBumperSpriteGroupComponent>()?.current =
DashNestBumperSpriteState.active;
}
/// Deactivates the [DashNestBumper].
void deactivate() {
firstChild<_DashNestBumperSpriteGroupComponent>()?.current =
DashNestBumperSpriteState.inactive;
}
}
/// Indicates the [DashNestBumper]'s current sprite state.
@visibleForTesting
enum DashNestBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}
class _DashNestBumperSpriteGroupComponent
extends SpriteGroupComponent<DashNestBumperSpriteState> with HasGameRef {
_DashNestBumperSpriteGroupComponent({
required String activeAssetPath,
required String inactiveAssetPath,
required Vector2 position,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
super(
anchor: Anchor.center,
position: position,
);
final String _activeAssetPath;
final String _inactiveAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = {
DashNestBumperSpriteState.active:
Sprite(gameRef.images.fromCache(_activeAssetPath)),
DashNestBumperSpriteState.inactive:
Sprite(gameRef.images.fromCache(_inactiveAssetPath)),
};
this.sprites = sprites;
current = DashNestBumperSpriteState.inactive;
size = sprites[current]!.originalSize / 10;
}
}

@ -31,8 +31,7 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall}
_DinoTopWall()
: super(
// TODO(ruimiguel): set final priority when RenderPriority PR merged.
priority: Ball.boardPriority + 1,
priority: RenderPriority.dinoTopWall,
children: [_DinoTopWallSpriteComponent()],
) {
renderBody = false;
@ -127,8 +126,7 @@ class _DinoBottomWall extends BodyComponent with InitialPosition {
///{@macro dino_top_wall}
_DinoBottomWall()
: super(
// TODO(ruimiguel): set final priority when RenderPriority PR merged.
priority: Ball.boardPriority + 1,
priority: RenderPriority.dinoBottomWall,
children: [_DinoBottomWallSpriteComponent()],
) {
renderBody = false;

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

@ -44,7 +44,7 @@ class _LaunchRampBase extends BodyComponent with InitialPosition, Layered {
/// {@macro launch_ramp_base}
_LaunchRampBase()
: super(
priority: Ball.launchRampPriority - 1,
priority: RenderPriority.launchRamp,
children: [
_LaunchRampBackgroundRailingSpriteComponent(),
_LaunchRampBaseSpriteComponent(),
@ -166,7 +166,7 @@ class _LaunchRampForegroundRailing extends BodyComponent with InitialPosition {
/// {@macro launch_ramp_foreground_railing}
_LaunchRampForegroundRailing()
: super(
priority: Ball.launchRampPriority + 1,
priority: RenderPriority.launchRampForegroundRailing,
children: [_LaunchRampForegroundRailingSpriteComponent()],
) {
renderBody = false;
@ -265,8 +265,8 @@ class _LaunchRampExit extends LayerSensor {
insideLayer: Layer.launcher,
outsideLayer: Layer.board,
orientation: LayerEntranceOrientation.down,
insidePriority: Ball.launchRampPriority,
outsidePriority: 0,
insidePriority: RenderPriority.ballOnLaunchRamp,
outsidePriority: RenderPriority.ballOnBoard,
) {
layer = Layer.launcher;
renderBody = false;

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

@ -14,25 +14,49 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
required this.compressionDistance,
// TODO(ruimiguel): set to priority +1 over LaunchRamp once all priorities
// are fixed.
}) : super(priority: 0) {
}) : super(priority: RenderPriority.plunger) {
layer = Layer.launcher;
renderBody = false;
}
/// Distance the plunger can lower.
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
Body createBody() {
final shape = PolygonShape()
..setAsBox(
1.35,
0.5,
Vector2.zero(),
BoardDimensions.perspectiveAngle,
);
final fixtureDef = FixtureDef(shape)..density = 80;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
@ -40,12 +64,15 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
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].
void pull() {
body.linearVelocity = Vector2(0, 7);
_spriteComponent.pull();
}
/// Set an upward velocity on the [Plunger].
@ -55,6 +82,7 @@ class Plunger extends BodyComponent with InitialPosition, Layered {
void release() {
final velocity = (initialPosition.y - body.position.y) * 5;
body.linearVelocity = Vector2(0, velocity);
_spriteComponent.release();
}
/// 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 {
await super.onLoad();
await _anchorToJoint();
renderBody = false;
await add(_PlungerSpriteComponent());
_spriteComponent = _PlungerSpriteAnimationGroupComponent();
await add(_spriteComponent);
}
}
class _PlungerSpriteComponent extends SpriteComponent with HasGameRef {
/// 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;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
final spriteSheet = await gameRef.images.load(
Assets.images.plunger.plunger.keyName,
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(1.5, 13.4);
angle = -0.008;
const amountPerRow = 20;
const amountPerColumn = 1;
final textureSize = Vector2(
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,116 @@
// 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 spaceshipRampArrow = _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
// wall.
/// {@macro rocket_sprite_component}
RocketSpriteComponent() : super(priority: 5);
RocketSpriteComponent() : super(priority: RenderPriority.rocket);
@override
Future<void> onLoad() async {

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

@ -0,0 +1,99 @@
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) {
sprites[spriteState] = Sprite(
gameRef.images.fromCache(spriteState.path),
);
}
current = SignpostSpriteState.inactive;
size = sprites[current]!.originalSize / 10;
}
}

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

@ -35,9 +35,12 @@ class Spaceship extends Forge2DBlueprint {
AndroidHead()..initialPosition = position,
_SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail,
outsidePriority: Ball.spaceshipRailPriority,
outsidePriority: RenderPriority.ballOnSpaceshipRail,
)..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,
]);
}
@ -48,7 +51,7 @@ class Spaceship extends Forge2DBlueprint {
/// {@endtemplate}
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_saucer}
SpaceshipSaucer() : super(priority: Ball.spaceshipPriority - 1) {
SpaceshipSaucer() : super(priority: RenderPriority.spaceshipSaucer) {
layer = Layer.spaceship;
}
@ -94,7 +97,7 @@ class AndroidHead extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_bridge}
AndroidHead()
: super(
priority: Ball.spaceshipPriority + 1,
priority: RenderPriority.androidHead,
children: [_AndroidHeadSpriteAnimation()],
) {
layer = Layer.spaceship;
@ -151,7 +154,7 @@ class _SpaceshipEntrance extends LayerSensor {
: super(
insideLayer: Layer.spaceship,
orientation: LayerEntranceOrientation.up,
insidePriority: Ball.spaceshipPriority,
insidePriority: RenderPriority.ballOnSpaceship,
) {
layer = Layer.spaceship;
}
@ -175,12 +178,12 @@ class _SpaceshipEntrance extends LayerSensor {
}
class _SpaceshipHole extends LayerSensor {
_SpaceshipHole({Layer? outsideLayer, int? outsidePriority = 1})
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
: super(
insideLayer: Layer.spaceship,
outsideLayer: outsideLayer,
orientation: LayerEntranceOrientation.down,
insidePriority: 4,
insidePriority: RenderPriority.ballOnSpaceship,
outsidePriority: outsidePriority,
) {
renderBody = false;
@ -231,7 +234,7 @@ class _SpaceshipWallShape extends ChainShape {
/// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_wall}
SpaceshipWall() : super(priority: Ball.spaceshipPriority + 1) {
SpaceshipWall() : super(priority: RenderPriority.spaceshipSaucerWall) {
layer = Layer.spaceship;
}

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

@ -4,6 +4,7 @@ import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
@ -15,6 +16,15 @@ class SpaceshipRamp extends Forge2DBlueprint {
/// {@macro spaceship_ramp}
SpaceshipRamp();
/// [SpriteGroupComponent] representing the arrow that lights up.
@visibleForTesting
late final SpaceshipRampArrowSpriteComponent spaceshipRampArrow;
/// Forwards the sprite to the next [SpaceshipRampArrowSpriteState].
///
/// If the current state is the last one it cycles back to the initial state.
void progress() => spaceshipRampArrow.progress();
@override
void build(_) {
addAllContactCallback([
@ -22,22 +32,23 @@ class SpaceshipRamp extends Forge2DBlueprint {
]);
final rightOpening = _SpaceshipRampOpening(
// TODO(ruimiguel): set Board priority when defined.
outsidePriority: 1,
rotation: math.pi,
outsidePriority: RenderPriority.ballOnBoard,
rotation: -5 * math.pi / 180,
)
..initialPosition = Vector2(1.7, -19.8)
..initialPosition = Vector2(1.7, -19.12)
..layer = Layer.opening;
final leftOpening = _SpaceshipRampOpening(
outsideLayer: Layer.spaceship,
outsidePriority: Ball.spaceshipPriority,
rotation: math.pi,
outsidePriority: RenderPriority.ballOnSpaceship,
rotation: -5 * math.pi / 180,
)
..initialPosition = Vector2(-13.7, -18.6)
..initialPosition = Vector2(-13.7, -19)
..layer = Layer.spaceshipEntranceRamp;
final spaceshipRamp = _SpaceshipRampBackground();
spaceshipRampArrow = SpaceshipRampArrowSpriteComponent();
final spaceshipRampBoardOpeningSprite =
_SpaceshipRampBoardOpeningSpriteComponent()
..position = Vector2(3.4, -39.5);
@ -51,16 +62,71 @@ class SpaceshipRamp extends Forge2DBlueprint {
rightOpening,
leftOpening,
baseRight,
_SpaceshipRampBackgroundRailingSpriteComponent(),
spaceshipRamp,
spaceshipRampArrow,
spaceshipRampForegroundRailing,
]);
}
}
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState {
String get path {
switch (this) {
case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.spaceship.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
return Assets.images.spaceship.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
return Assets.images.spaceship.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
return Assets.images.spaceship.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
return Assets.images.spaceship.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
return Assets.images.spaceship.ramp.arrow.active5.keyName;
}
}
SpaceshipRampArrowSpriteState get next {
return SpaceshipRampArrowSpriteState
.values[(index + 1) % SpaceshipRampArrowSpriteState.values.length];
}
}
class _SpaceshipRampBackground extends BodyComponent
with InitialPosition, Layered {
_SpaceshipRampBackground() : super(priority: Ball.spaceshipRampPriority - 1) {
_SpaceshipRampBackground()
: super(
priority: RenderPriority.spaceshipRamp,
children: [
_SpaceshipRampBackgroundRampSpriteComponent(),
],
) {
layer = Layer.spaceshipEntranceRamp;
renderBody = false;
}
/// Width between walls of the ramp.
@ -112,29 +178,26 @@ class _SpaceshipRampBackground extends BodyComponent
return body;
}
@override
Future<void> onLoad() async {
await super.onLoad();
renderBody = false;
await add(_SpaceshipRampBackgroundRampSpriteComponent());
await add(_SpaceshipRampBackgroundRailingSpriteComponent());
}
}
class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
with HasGameRef {
_SpaceshipRampBackgroundRailingSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-11.7, -54.3),
priority: RenderPriority.spaceshipRampBackgroundRailing,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.spaceship.ramp.railingBackground.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-11.7, -54.3);
}
}
@ -143,13 +206,50 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.spaceship.ramp.main.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-11.7, -53.6);
position = Vector2(-10.7, -53.6);
}
}
/// {@template spaceship_ramp_arrow_sprite_component}
/// An arrow inside [SpaceshipRamp].
///
/// Lights up a each dash whenever a [Ball] gets into [SpaceshipRamp].
/// {@endtemplate}
class SpaceshipRampArrowSpriteComponent
extends SpriteGroupComponent<SpaceshipRampArrowSpriteState>
with HasGameRef {
/// {@macro spaceship_ramp_arrow_sprite_component}
SpaceshipRampArrowSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-3.9, -56.5),
priority: RenderPriority.spaceshipRampArrow,
);
/// Changes arrow image to the next [Sprite].
void progress() => current = current?.next;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = <SpaceshipRampArrowSpriteState, Sprite>{};
this.sprites = sprites;
for (final spriteState in SpaceshipRampArrowSpriteState.values) {
sprites[spriteState] = Sprite(
gameRef.images.fromCache(spriteState.path),
);
}
current = SpaceshipRampArrowSpriteState.inactive;
size = sprites[current]!.originalSize / 10;
}
}
@ -158,8 +258,10 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.spaceship.ramp.boardOpening.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
@ -171,7 +273,7 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
with InitialPosition, Layered {
_SpaceshipRampForegroundRailing()
: super(
priority: Ball.spaceshipRampPriority + 1,
priority: RenderPriority.spaceshipRampForegroundRailing,
children: [_SpaceshipRampForegroundRailingSpriteComponent()],
) {
layer = Layer.spaceshipEntranceRamp;
@ -230,16 +332,22 @@ class _SpaceshipRampForegroundRailing extends BodyComponent
class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
with HasGameRef {
_SpaceshipRampForegroundRailingSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-12.3, -52.5),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.spaceship.ramp.railingForeground.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
anchor = Anchor.center;
position = Vector2(-12.3, -52.5);
}
}
@ -287,7 +395,7 @@ class _SpaceshipRampOpening extends LayerSensor {
insideLayer: Layer.spaceshipEntranceRamp,
outsideLayer: outsideLayer,
orientation: LayerEntranceOrientation.down,
insidePriority: Ball.spaceshipRampPriority,
insidePriority: RenderPriority.ballOnSpaceshipRamp,
outsidePriority: outsidePriority,
) {
renderBody = false;

@ -2,37 +2,43 @@ import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_bumper}
/// Bumper for Sparky area.
/// {@endtemplate}
// TODO(ruimiguel): refactor later to unify with DashBumpers.
class SparkyBumper extends BodyComponent with InitialPosition {
/// {@macro sparky_bumper}
SparkyBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
required String inactiveAssetPath,
required SpriteComponent spriteComponent,
required String onAssetPath,
required String offAssetPath,
required Vector2 spritePosition,
}) : _majorRadius = majorRadius,
_minorRadius = minorRadius,
_activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
_spriteComponent = spriteComponent;
super(
priority: RenderPriority.sparkyBumper,
children: [
_SparkyBumperSpriteGroupComponent(
onAssetPath: onAssetPath,
offAssetPath: offAssetPath,
position: spritePosition,
),
],
) {
renderBody = false;
}
/// {@macro sparky_bumper}
SparkyBumper.a()
: this._(
majorRadius: 2.9,
minorRadius: 2.1,
activeAssetPath: Assets.images.sparky.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.sparky.bumper.a.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.25),
),
onAssetPath: Assets.images.sparky.bumper.a.active.keyName,
offAssetPath: Assets.images.sparky.bumper.a.inactive.keyName,
spritePosition: Vector2(0, -0.25),
);
/// {@macro sparky_bumper}
@ -40,12 +46,9 @@ class SparkyBumper extends BodyComponent with InitialPosition {
: this._(
majorRadius: 2.85,
minorRadius: 2,
activeAssetPath: Assets.images.sparky.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.sparky.bumper.b.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.35),
),
onAssetPath: Assets.images.sparky.bumper.b.active.keyName,
offAssetPath: Assets.images.sparky.bumper.b.inactive.keyName,
spritePosition: Vector2(0, -0.35),
);
/// {@macro sparky_bumper}
@ -53,33 +56,13 @@ class SparkyBumper extends BodyComponent with InitialPosition {
: this._(
majorRadius: 3,
minorRadius: 2.2,
activeAssetPath: Assets.images.sparky.bumper.c.active.keyName,
inactiveAssetPath: Assets.images.sparky.bumper.c.inactive.keyName,
spriteComponent: SpriteComponent(
anchor: Anchor.center,
position: Vector2(0, -0.4),
),
onAssetPath: Assets.images.sparky.bumper.c.active.keyName,
offAssetPath: Assets.images.sparky.bumper.c.inactive.keyName,
spritePosition: Vector2(0, -0.4),
);
final double _majorRadius;
final double _minorRadius;
final String _activeAssetPath;
late final Sprite _activeSprite;
final String _inactiveAssetPath;
late final Sprite _inactiveSprite;
final SpriteComponent _spriteComponent;
@override
Future<void> onLoad() async {
await super.onLoad();
await _loadSprites();
// TODO(erickzanardo): Look into using onNewState instead.
// Currently doing: onNewState(gameRef.read<GameState>()) will throw an
// `Exception: build context is not available yet`
deactivate();
await add(_spriteComponent);
}
@override
Body createBody() {
@ -101,25 +84,53 @@ class SparkyBumper extends BodyComponent with InitialPosition {
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
Future<void> _loadSprites() async {
// TODO(alestiago): I think ideally we would like to do:
// Sprite(path).load so we don't require to store the activeAssetPath and
// the inactive assetPath.
_inactiveSprite = await gameRef.loadSprite(_inactiveAssetPath);
_activeSprite = await gameRef.loadSprite(_activeAssetPath);
/// Animates the [DashNestBumper].
Future<void> animate() async {
final spriteGroupComponent = firstChild<_SparkyBumperSpriteGroupComponent>()
?..current = SparkyBumperSpriteState.inactive;
await Future<void>.delayed(const Duration(milliseconds: 50));
spriteGroupComponent?.current = SparkyBumperSpriteState.active;
}
}
/// Activates the [DashNestBumper].
void activate() {
_spriteComponent
..sprite = _activeSprite
..size = _activeSprite.originalSize / 10;
}
/// Indicates the [SparkyBumper]'s current sprite state.
@visibleForTesting
enum SparkyBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}
class _SparkyBumperSpriteGroupComponent
extends SpriteGroupComponent<SparkyBumperSpriteState> with HasGameRef {
_SparkyBumperSpriteGroupComponent({
required String onAssetPath,
required String offAssetPath,
required Vector2 position,
}) : _onAssetPath = onAssetPath,
_offAssetPath = offAssetPath,
super(
anchor: Anchor.center,
position: position,
);
final String _onAssetPath;
final String _offAssetPath;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprites = {
SparkyBumperSpriteState.active:
Sprite(gameRef.images.fromCache(_onAssetPath)),
SparkyBumperSpriteState.inactive:
Sprite(gameRef.images.fromCache(_offAssetPath)),
};
this.sprites = sprites;
/// Deactivates the [DashNestBumper].
void deactivate() {
_spriteComponent
..sprite = _inactiveSprite
..size = _inactiveSprite.originalSize / 10;
current = SparkyBumperSpriteState.active;
size = sprites[current]!.originalSize / 10;
}
}

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

@ -52,6 +52,7 @@ flutter:
- assets/images/spaceship/
- assets/images/spaceship/rail/
- assets/images/spaceship/ramp/
- assets/images/spaceship/ramp/arrow/
- assets/images/chrome_dino/
- assets/images/kicker/
- assets/images/plunger/
@ -64,6 +65,7 @@ flutter:
- assets/images/sparky/bumper/c/
- assets/images/backboard/
- assets/images/google_word/
- assets/images/signpost/
flutter_gen:
line_length: 80

@ -17,6 +17,11 @@ class AlienBumperAGame extends BasicBallGame {
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.alienBumper.a.active.keyName,
Assets.images.alienBumper.a.inactive.keyName,
]);
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperA = AlienBumper.a()
..initialPosition = Vector2(center.x - 20, center.y - 20)

@ -17,6 +17,11 @@ class AlienBumperBGame extends BasicBallGame {
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.alienBumper.b.active.keyName,
Assets.images.alienBumper.b.inactive.keyName,
]);
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final alienBumperB = AlienBumper.b()
..initialPosition = Vector2(center.x - 10, center.y + 10)

@ -15,9 +15,14 @@ class BigDashNestBumperGame extends BasicBallGame with Traceable {
@override
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
]);
camera.followVector2(Vector2.zero());
await add(BigDashNestBumper()..priority = 1);
await traceAllBodies();
await add(DashNestBumper.main()..priority = 1);
await traceAllBodies();
}
}

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

@ -0,0 +1,37 @@
import 'dart:async';
import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SignpostGame extends BasicBallGame with Traceable, TapDetector {
static const info = '''
Shows how a Signpost is rendered.
- Activate the "trace" parameter to overlay the body.
- Tap to progress the sprite.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
]);
camera.followVector2(Vector2.zero());
await add(Signpost());
await traceAllBodies();
}
@override
void onTap() {
super.onTap();
firstChild<Signpost>()!.progress();
}
}

@ -15,9 +15,14 @@ class SmallDashNestBumperAGame extends BasicBallGame with Traceable {
@override
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
]);
camera.followVector2(Vector2.zero());
await add(SmallDashNestBumper.a()..priority = 1);
await traceAllBodies();
await add(DashNestBumper.a()..priority = 1);
await traceAllBodies();
}
}

@ -15,9 +15,14 @@ class SmallDashNestBumperBGame extends BasicBallGame with Traceable {
@override
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
]);
camera.followVector2(Vector2.zero());
await add(SmallDashNestBumper.b()..priority = 1);
await traceAllBodies();
await add(DashNestBumper.b()..priority = 1);
await traceAllBodies();
}
}

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

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

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

@ -2,15 +2,16 @@ import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SpaceshipRampGame extends BasicBallGame {
class SpaceshipRampGame extends BasicBallGame with KeyboardEvents {
SpaceshipRampGame()
: super(
color: Colors.blue,
ballPriority: Ball.spaceshipRampPriority,
ballPriority: RenderPriority.ballOnSpaceshipRamp,
ballLayer: Layer.spaceshipEntranceRamp,
);
@ -19,13 +20,45 @@ class SpaceshipRampGame extends BasicBallGame {
- Activate the "trace" parameter to overlay the body.
- Tap anywhere on the screen to spawn a ball into the game.
- Press space to progress arrow sprites.
''';
late final SpaceshipRamp _spaceshipRamp;
@override
Future<void> onLoad() async {
await super.onLoad();
await addFromBlueprint(SpaceshipRamp());
await images.loadAll([
Assets.images.spaceship.ramp.railingBackground.keyName,
Assets.images.spaceship.ramp.main.keyName,
Assets.images.spaceship.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.arrow.inactive.keyName,
Assets.images.spaceship.ramp.arrow.active1.keyName,
Assets.images.spaceship.ramp.arrow.active2.keyName,
Assets.images.spaceship.ramp.arrow.active3.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName,
]);
_spaceshipRamp = SpaceshipRamp();
await addFromBlueprint(_spaceshipRamp);
camera.followVector2(Vector2(-12, -50));
await traceAllBodies();
}
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) {
_spaceshipRamp.progress();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
}

@ -16,12 +16,21 @@ class SparkyBumperGame extends BasicBallGame with Traceable {
Future<void> onLoad() async {
await super.onLoad();
await images.loadAll([
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
]);
final center = screenToWorld(camera.viewport.canvasSize! / 2);
final sparkyBumperA = SparkyBumper.a()
..initialPosition = Vector2(center.x - 20, center.y - 20)
..initialPosition = Vector2(center.x - 20, center.y + 20)
..priority = 1;
final sparkyBumperB = SparkyBumper.b()
..initialPosition = Vector2(center.x - 10, center.y + 10)
..initialPosition = Vector2(center.x - 10, center.y - 10)
..priority = 1;
final sparkyBumperC = SparkyBumper.c()
..initialPosition = Vector2(center.x + 20, center.y)

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

@ -2,9 +2,19 @@ import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
class TestGame extends Forge2DGame {
TestGame() {
TestGame([List<String>? assets]) : _assets = assets {
images.prefix = '';
}
final List<String>? _assets;
@override
Future<void> onLoad() async {
if (_assets != null) {
await images.loadAll(_assets!);
}
await super.onLoad();
}
}
class KeyboardTestGame extends TestGame with HasKeyboardHandlerComponents {}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save