fix: fixed merge conflicts

pull/406/head
RuiAlonso 3 years ago
commit 13fd78f5a0

@ -21,7 +21,7 @@ jobs:
- name: Build Flutter App - name: Build Flutter App
run: | run: |
flutter packages get flutter packages get
flutter build web --target lib/main_development.dart --web-renderer canvaskit --release flutter build web --target lib/main.dart --web-renderer canvaskit --release
- name: Deploy to Firebase - name: Deploy to Firebase
uses: FirebaseExtended/action-hosting-deploy@v0 uses: FirebaseExtended/action-hosting-deploy@v0

@ -14,3 +14,4 @@ jobs:
flutter_version: 2.10.5 flutter_version: 2.10.5
coverage_excludes: "lib/gen/*.dart" coverage_excludes: "lib/gen/*.dart"
test_optimization: false test_optimization: false

@ -0,0 +1,21 @@
name: spell_check
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: [pull_request]
jobs:
build:
name: Spell Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Check spelling
uses: zwaldowski/cspell-action@v1
with:
paths: '**/*.{dart,arb,md}'
config: .vscode/cspell.json

@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="development" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="buildFlavor" value="development" />
<option name="filePath" value="$PROJECT_DIR$/lib/main_development.dart" />
<method v="2" />
</configuration>
</component>

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method v="2" />
</configuration>
</component>

@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="production" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="buildFlavor" value="production" />
<option name="filePath" value="$PROJECT_DIR$/lib/main_production.dart" />
<method v="2" />
</configuration>
</component>

@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="staging" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="buildFlavor" value="staging" />
<option name="filePath" value="$PROJECT_DIR$/lib/main_staging.dart" />
<method v="2" />
</configuration>
</component>

@ -0,0 +1,50 @@
{
"version": "0.2",
"enabled": true,
"language": "en",
"words": [
"animatronic",
"argb",
"audioplayers",
"backbox",
"bezier",
"contador",
"cupertino",
"dashbook",
"deserialization",
"dpad",
"endtemplate",
"firestore",
"gapless",
"genhtml",
"goldens",
"lcov",
"leaderboard",
"loadables",
"localizable",
"mixins",
"mocktail",
"mostrado",
"multiball",
"multiballs",
"occluder",
"página",
"pixelated",
"pixeloid",
"rects",
"rrect",
"serializable",
"sparky's",
"tappable",
"tappables",
"texto",
"theming",
"unawaited",
"unfocus",
"unlayered",
"vsync"
],
"ignorePaths": [
".github/workflows/**"
]
}

@ -5,30 +5,10 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Launch development", "name": "Launch pinball",
"request": "launch", "request": "launch",
"type": "dart", "type": "dart",
"program": "lib/main_development.dart", "program": "lib/main.dart"
"args": [
"--flavor",
"development",
"--target",
"lib/main_development.dart"
]
},
{
"name": "Launch staging",
"request": "launch",
"type": "dart",
"program": "lib/main_staging.dart",
"args": ["--flavor", "staging", "--target", "lib/main_staging.dart"]
},
{
"name": "Launch production",
"request": "launch",
"type": "dart",
"program": "lib/main_production.dart",
"args": ["--flavor", "production", "--target", "lib/main_production.dart"]
}, },
{ {
"name": "Launch component sandbox", "name": "Launch component sandbox",

@ -1,37 +1,31 @@
# Pinball # I/O Pinball
[![Pinball Header][logo]][pinball_link]
[![io_pinball][build_status_badge]][workflow_link]
![coverage][coverage_badge] ![coverage][coverage_badge]
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] [![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link] [![License: MIT][license_badge]][license_link]
Generated by the [Very Good CLI][very_good_cli_link] 🤖 A Pinball game built with [Flutter][flutter_link] and [Firebase][firebase_link] for [Google I/O 2022][google_io_link].
Google I/O 2022 Pinball game built with Flutter and Firebase [Try it now][pinball_link] and [learn about how it's made][blog_link].
--- _Built by [Very Good Ventures][very_good_ventures_link] in partnership with Google_
## Getting Started 🚀 _Created using [Very Good CLI][very_good_cli_link] 🤖_
This project contains 3 flavors: ---
- development ## Getting Started 🚀
- staging
- production
To run the desired flavor either use the launch configuration in VSCode/Android Studio or use the following commands: To run the desired project either use the launch configuration in VSCode/Android Studio or use the following commands:
```sh ```sh
# Development $ flutter run -d chrome
$ flutter run --flavor development --target lib/main_development.dart
# Staging
$ flutter run --flavor staging --target lib/main_staging.dart
# Production
$ flutter run --flavor production --target lib/main_production.dart
``` ```
_\*Pinball works on iOS, Android, Web, and Windows._ _\*I/O Pinball works on Web for desktop and mobile._
--- ---
@ -48,7 +42,6 @@ To view the generated coverage report you can use [lcov](https://github.com/linu
```sh ```sh
# Generate Coverage Report # Generate Coverage Report
$ genhtml coverage/lcov.info -o coverage/ $ genhtml coverage/lcov.info -o coverage/
# Open Coverage Report # Open Coverage Report
$ open coverage/index.html $ open coverage/index.html
``` ```
@ -101,22 +94,6 @@ Widget build(BuildContext context) {
} }
``` ```
### Adding Supported Locales
Update the `CFBundleLocalizations` array in the `Info.plist` at `ios/Runner/Info.plist` to include the new locale.
```xml
...
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>es</string>
</array>
...
```
### Adding Translations ### Adding Translations
1. For each supported locale, add a new ARB file in `lib/l10n/arb`. 1. For each supported locale, add a new ARB file in `lib/l10n/arb`.
@ -154,38 +131,20 @@ Update the `CFBundleLocalizations` array in the `Info.plist` at `ios/Runner/Info
} }
``` ```
### Deploy application to Firebase hosting [build_status_badge]: https://github.com/flutter/pinball/actions/workflows/main.yaml/badge.svg
Follow the following steps to deploy the application.
## Firebase CLI
Install and authenticate with [Firebase CLI tools](https://firebase.google.com/docs/cli)
## Build the project using the desired environment
```bash
# Development
$ flutter build web --release --target lib/main_development.dart
# Staging
$ flutter build web --release --target lib/main_staging.dart
# Production
$ flutter build web --release --target lib/main_production.dart
```
## Deploy
```bash
$ firebase deploy
```
[coverage_badge]: coverage_badge.svg [coverage_badge]: coverage_badge.svg
[firebase_link]: https://firebase.google.com/
[flutter_link]: https://flutter.dev
[flutter_localizations_link]: https://api.flutter.dev/flutter/flutter_localizations/flutter_localizations-library.html [flutter_localizations_link]: https://api.flutter.dev/flutter/flutter_localizations/flutter_localizations-library.html
[google_io_link]: https://events.google.com/io/
[blog_link]: https://medium.com/flutter/i-o-pinball-powered-by-flutter-and-firebase-d22423f3f5d
[internationalization_link]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization [internationalization_link]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT [license_link]: https://opensource.org/licenses/MIT
[logo]: art/readme_header.png
[pinball_link]: https://pinball.flutter.dev
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg [very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis [very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli [very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli
[very_good_ventures_link]: https://verygood.ventures/
[workflow_link]: https://github.com/flutter/pinball/actions/workflows/main.yaml

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

@ -17,17 +17,17 @@ class App extends StatelessWidget {
required AuthenticationRepository authenticationRepository, required AuthenticationRepository authenticationRepository,
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository, required ShareRepository shareRepository,
required PinballPlayer pinballPlayer, required PinballAudioPlayer pinballAudioPlayer,
}) : _authenticationRepository = authenticationRepository, }) : _authenticationRepository = authenticationRepository,
_leaderboardRepository = leaderboardRepository, _leaderboardRepository = leaderboardRepository,
_shareRepository = shareRepository, _shareRepository = shareRepository,
_pinballPlayer = pinballPlayer, _pinballAudioPlayer = pinballAudioPlayer,
super(key: key); super(key: key);
final AuthenticationRepository _authenticationRepository; final AuthenticationRepository _authenticationRepository;
final LeaderboardRepository _leaderboardRepository; final LeaderboardRepository _leaderboardRepository;
final ShareRepository _shareRepository; final ShareRepository _shareRepository;
final PinballPlayer _pinballPlayer; final PinballAudioPlayer _pinballAudioPlayer;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -36,7 +36,7 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _authenticationRepository), RepositoryProvider.value(value: _authenticationRepository),
RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _shareRepository), RepositoryProvider.value(value: _shareRepository),
RepositoryProvider.value(value: _pinballPlayer), RepositoryProvider.value(value: _pinballAudioPlayer),
], ],
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [

@ -1,27 +1,39 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
part 'assets_manager_state.dart'; part 'assets_manager_state.dart';
/// {@template assets_manager_cubit}
/// Cubit responsable for pre loading any game assets
/// {@endtemplate}
class AssetsManagerCubit extends Cubit<AssetsManagerState> { class AssetsManagerCubit extends Cubit<AssetsManagerState> {
/// {@macro assets_manager_cubit} AssetsManagerCubit(this._game, this._audioPlayer)
AssetsManagerCubit(List<Future> loadables) : super(const AssetsManagerState.initial());
: super(
AssetsManagerState.initial( final PinballGame _game;
loadables: loadables, final PinballAudioPlayer _audioPlayer;
),
);
/// Loads the assets
Future<void> load() async { Future<void> load() async {
/// Assigning loadables is a very expensive operation. With this purposeful
/// delay here, which is a bit random in duration but enough to let the UI
/// do its job without adding too much delay for the user, we are letting
/// the UI paint first, and then we start loading the assets.
await Future<void>.delayed(const Duration(milliseconds: 300));
emit(
state.copyWith(
loadables: [
_game.preFetchLeaderboard(),
..._game.preLoadAssets(),
..._audioPlayer.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
],
),
);
final all = state.loadables.map((loadable) async { final all = state.loadables.map((loadable) async {
await loadable; await loadable;
emit(state.copyWith(loaded: [...state.loaded, loadable])); emit(state.copyWith(loaded: [...state.loaded, loadable]));
}).toList(); }).toList();
await Future.wait(all); await Future.wait(all);
} }
} }

@ -11,9 +11,8 @@ class AssetsManagerState extends Equatable {
}); });
/// {@macro assets_manager_state} /// {@macro assets_manager_state}
const AssetsManagerState.initial({ const AssetsManagerState.initial()
required List<Future> loadables, : this(loadables: const [], loaded: const []);
}) : this(loadables: loadables, loaded: const []);
/// List of futures to load /// List of futures to load
final List<Future> loadables; final List<Future> loadables;
@ -22,7 +21,11 @@ class AssetsManagerState extends Equatable {
final List<Future> loaded; final List<Future> loaded;
/// Returns a value between 0 and 1 to indicate the loading progress /// Returns a value between 0 and 1 to indicate the loading progress
double get progress => loaded.length / loadables.length; double get progress =>
loadables.isEmpty ? 0 : loaded.length / loadables.length;
/// Only returns false if all the assets have been loaded
bool get isLoading => progress != 1;
/// Returns a copy of this instance with the given parameters /// Returns a copy of this instance with the given parameters
/// updated /// updated

@ -1,6 +1,7 @@
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/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -20,10 +21,9 @@ class AssetsLoadingPage extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Padding(
l10n.ioPinball, padding: const EdgeInsets.symmetric(horizontal: 20),
style: headline1!.copyWith(fontSize: 80), child: Assets.images.loadingGame.ioPinball.image(),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
AnimatedEllipsisText( AnimatedEllipsisText(

@ -1,5 +1,6 @@
export 'ball_spawning_behavior.dart'; export 'ball_spawning_behavior.dart';
export 'ball_theming_behavior.dart'; export 'ball_theming_behavior.dart';
export 'bonus_ball_spawning_behavior.dart';
export 'bonus_noise_behavior.dart'; export 'bonus_noise_behavior.dart';
export 'bumper_noise_behavior.dart'; export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart'; export 'camera_focusing_behavior.dart';

@ -0,0 +1,30 @@
import 'package:flame/components.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template bonus_ball_spawning_behavior}
/// After a duration, spawns a bonus ball from the [DinoWalls] and boosts it
/// into the middle of the board.
/// {@endtemplate}
class BonusBallSpawningBehavior extends TimerComponent with HasGameRef {
/// {@macro bonus_ball_spawning_behavior}
BonusBallSpawningBehavior()
: super(
period: 5,
removeOnFinish: true,
);
@override
void onTick() {
final characterTheme = readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme;
gameRef.descendants().whereType<ZCanvasComponent>().single.add(
Ball(assetPath: characterTheme.ball.keyName)
..add(BallImpulsingBehavior(impulse: Vector2(-40, 0)))
..initialPosition = Vector2(29.2, -24.5)
..zIndex = ZIndexes.ballOnBoard,
);
}
}

@ -15,7 +15,7 @@ class BonusNoiseBehavior extends Component {
}, },
onNewState: (state) { onNewState: (state) {
final bonus = state.bonusHistory.last; final bonus = state.bonusHistory.last;
final audioPlayer = readProvider<PinballPlayer>(); final audioPlayer = readProvider<PinballAudioPlayer>();
switch (bonus) { switch (bonus) {
case GameBonus.googleWord: case GameBonus.googleWord:
@ -25,10 +25,13 @@ class BonusNoiseBehavior extends Component {
audioPlayer.play(PinballAudio.sparky); audioPlayer.play(PinballAudio.sparky);
break; break;
case GameBonus.dinoChomp: case GameBonus.dinoChomp:
audioPlayer.play(PinballAudio.dino);
break; break;
case GameBonus.androidSpaceship: case GameBonus.androidSpaceship:
audioPlayer.play(PinballAudio.android);
break; break;
case GameBonus.dashNest: case GameBonus.dashNest:
audioPlayer.play(PinballAudio.dash);
break; break;
} }
}, },

@ -6,6 +6,6 @@ class BumperNoiseBehavior extends ContactBehavior {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);
readProvider<PinballPlayer>().play(PinballAudio.bumper); readProvider<PinballAudioPlayer>().play(PinballAudio.bumper);
} }
} }

@ -15,7 +15,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@endtemplate} /// {@endtemplate}
class ScoringBehavior extends Component class ScoringBehavior extends Component
with HasGameRef, FlameBlocReader<GameBloc, GameState> { with HasGameRef, FlameBlocReader<GameBloc, GameState> {
/// {@macto scoring_behavior} /// {@macro scoring_behavior}
ScoringBehavior({ ScoringBehavior({
required Points points, required Points points,
required Vector2 position, required Vector2 position,

@ -12,7 +12,6 @@ class GameBloc extends Bloc<GameEvent, GameState> {
on<Scored>(_onScored); on<Scored>(_onScored);
on<MultiplierIncreased>(_onIncreasedMultiplier); on<MultiplierIncreased>(_onIncreasedMultiplier);
on<BonusActivated>(_onBonusActivated); on<BonusActivated>(_onBonusActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
on<GameOver>(_onGameOver); on<GameOver>(_onGameOver);
on<GameStarted>(_onGameStarted); on<GameStarted>(_onGameStarted);
} }
@ -65,18 +64,4 @@ class GameBloc extends Bloc<GameEvent, GameState> {
), ),
); );
} }
Future<void> _onSparkyTurboChargeActivated(
SparkyTurboChargeActivated event,
Emitter emit,
) async {
emit(
state.copyWith(
bonusHistory: [
...state.bonusHistory,
GameBonus.sparkyTurboCharge,
],
),
);
}
} }

@ -40,13 +40,6 @@ class BonusActivated extends GameEvent {
List<Object?> get props => [bonus]; List<Object?> get props => [bonus];
} }
class SparkyTurboChargeActivated extends GameEvent {
const SparkyTurboChargeActivated();
@override
List<Object?> get props => [];
}
/// {@template multiplier_increased_game_event} /// {@template multiplier_increased_game_event}
/// Added when a multiplier is gained. /// Added when a multiplier is gained.
/// {@endtemplate} /// {@endtemplate}

@ -5,7 +5,7 @@ enum GameBonus {
/// Bonus achieved when the ball activates all Google letters. /// Bonus achieved when the ball activates all Google letters.
googleWord, googleWord,
/// Bonus achieved when the user activates all dash nest bumpers. /// Bonus achieved when the user activates all dash bumpers.
dashNest, dashNest,
/// Bonus achieved when a ball enters Sparky's computer. /// Bonus achieved when a ball enters Sparky's computer.

@ -1,6 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
@ -14,15 +15,14 @@ class AndroidAcres extends Component {
/// {@macro android_acres} /// {@macro android_acres}
AndroidAcres() AndroidAcres()
: super( : super(
children: [
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>(
create: AndroidSpaceshipCubit.new,
children: [ children: [
SpaceshipRamp( SpaceshipRamp(
children: [ children: [
RampShotBehavior( RampShotBehavior(points: Points.fiveThousand),
points: Points.fiveThousand, RampBonusBehavior(points: Points.oneMillion),
),
RampBonusBehavior(
points: Points.oneMillion,
),
], ],
), ),
SpaceshipRail(), SpaceshipRail(),
@ -52,6 +52,8 @@ class AndroidAcres extends Component {
)..initialPosition = Vector2(-20.7, -13), )..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(), AndroidSpaceshipBonusBehavior(),
], ],
),
],
); );
/// Creates [AndroidAcres] without any children. /// Creates [AndroidAcres] without any children.

@ -5,18 +5,21 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus. /// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus.
class AndroidSpaceshipBonusBehavior extends Component class AndroidSpaceshipBonusBehavior extends Component {
with ParentIsA<AndroidAcres>, FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { Future<void> onLoad() async {
super.onMount(); await super.onLoad();
final androidSpaceship = parent.firstChild<AndroidSpaceship>()!; await add(
androidSpaceship.bloc.stream.listen((state) { FlameBlocListener<AndroidSpaceshipCubit, AndroidSpaceshipState>(
final listenWhen = state == AndroidSpaceshipState.withBonus; listenWhen: (_, state) => state == AndroidSpaceshipState.withBonus,
if (!listenWhen) return; onNewState: (state) {
readBloc<GameBloc, GameState>().add(
bloc.add(const BonusActivated(GameBonus.androidSpaceship)); const BonusActivated(GameBonus.androidSpaceship),
androidSpaceship.bloc.onBonusAwarded(); );
}); readBloc<AndroidSpaceshipCubit, AndroidSpaceshipState>()
.onBonusAwarded();
},
),
);
} }
} }

@ -5,34 +5,44 @@ import 'package:flutter/material.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart'; import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart'; import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
import 'package:platform_helper/platform_helper.dart';
import 'package:share_repository/share_repository.dart'; import 'package:share_repository/share_repository.dart';
/// {@template backbox} /// {@template backbox}
/// The [Backbox] of the pinball machine. /// The [Backbox] of the pinball machine.
/// {@endtemplate} /// {@endtemplate}
class Backbox extends PositionComponent with ZIndex { class Backbox extends PositionComponent with ZIndex, HasGameRef {
/// {@macro backbox} /// {@macro backbox}
Backbox({ Backbox({
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository, required ShareRepository shareRepository,
}) : _shareRepository = shareRepository, required List<LeaderboardEntryData>? entries,
_bloc = BackboxBloc(leaderboardRepository: leaderboardRepository); }) : _bloc = BackboxBloc(
leaderboardRepository: leaderboardRepository,
initialEntries: entries,
),
_shareRepository = shareRepository,
_platformHelper = PlatformHelper();
/// {@macro backbox} /// {@macro backbox}
@visibleForTesting @visibleForTesting
Backbox.test({ Backbox.test({
required BackboxBloc bloc, required BackboxBloc bloc,
required ShareRepository shareRepository, required ShareRepository shareRepository,
required PlatformHelper platformHelper,
}) : _bloc = bloc, }) : _bloc = bloc,
_shareRepository = shareRepository; _shareRepository = shareRepository,
_platformHelper = platformHelper;
final ShareRepository _shareRepository; final ShareRepository _shareRepository;
late final Component _display; late final Component _display;
final BackboxBloc _bloc; final BackboxBloc _bloc;
final PlatformHelper _platformHelper;
late StreamSubscription<BackboxState> _subscription; late StreamSubscription<BackboxState> _subscription;
@override @override
@ -41,8 +51,6 @@ class Backbox extends PositionComponent with ZIndex {
anchor = Anchor.bottomCenter; anchor = Anchor.bottomCenter;
zIndex = ZIndexes.backbox; zIndex = ZIndexes.backbox;
_bloc.add(LeaderboardRequested());
await add(_BackboxSpriteComponent()); await add(_BackboxSpriteComponent());
await add(_display = Component()); await add(_display = Component());
_build(_bloc.state); _build(_bloc.state);
@ -64,7 +72,12 @@ class Backbox extends PositionComponent with ZIndex {
_display.add(LoadingDisplay()); _display.add(LoadingDisplay());
} else if (state is LeaderboardSuccessState) { } else if (state is LeaderboardSuccessState) {
_display.add(LeaderboardDisplay(entries: state.entries)); _display.add(LeaderboardDisplay(entries: state.entries));
} else if (state is LeaderboardFailureState) {
_display.add(LeaderboardFailureDisplay());
} else if (state is InitialsFormState) { } else if (state is InitialsFormState) {
if (_platformHelper.isMobile) {
gameRef.overlays.add(PinballGame.mobileControlsOverlay);
}
_display.add( _display.add(
InitialsInputDisplay( InitialsInputDisplay(
score: state.score, score: state.score,
@ -81,7 +94,13 @@ class Backbox extends PositionComponent with ZIndex {
), ),
); );
} else if (state is InitialsSuccessState) { } else if (state is InitialsSuccessState) {
_display.add(InitialsSubmissionSuccessDisplay()); _display.add(
GameOverInfoDisplay(
onShare: () {
_bloc.add(ShareScoreRequested(score: state.score));
},
),
);
} else if (state is ShareState) { } else if (state is ShareState) {
_display.add( _display.add(
ShareDisplay( ShareDisplay(
@ -95,7 +114,18 @@ class Backbox extends PositionComponent with ZIndex {
), ),
); );
} else if (state is InitialsFailureState) { } else if (state is InitialsFailureState) {
_display.add(InitialsSubmissionFailureDisplay()); _display.add(
InitialsSubmissionFailureDisplay(
onDismissed: () {
_bloc.add(
PlayerInitialsRequested(
score: state.score,
character: state.character,
),
);
},
),
);
} }
} }

@ -14,8 +14,13 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
/// {@macro backbox_bloc} /// {@macro backbox_bloc}
BackboxBloc({ BackboxBloc({
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required List<LeaderboardEntryData>? initialEntries,
}) : _leaderboardRepository = leaderboardRepository, }) : _leaderboardRepository = leaderboardRepository,
super(LoadingState()) { super(
initialEntries != null
? LeaderboardSuccessState(entries: initialEntries)
: LeaderboardFailureState(),
) {
on<PlayerInitialsRequested>(_onPlayerInitialsRequested); on<PlayerInitialsRequested>(_onPlayerInitialsRequested);
on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted); on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted);
on<ShareScoreRequested>(_onScoreShareRequested); on<ShareScoreRequested>(_onScoreShareRequested);
@ -49,10 +54,19 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
character: event.character.toType, character: event.character.toType,
), ),
); );
emit(InitialsSuccessState()); emit(
InitialsSuccessState(
score: event.score,
),
);
} catch (error, stackTrace) { } catch (error, stackTrace) {
addError(error, stackTrace); addError(error, stackTrace);
emit(InitialsFailureState()); emit(
InitialsFailureState(
score: event.score,
character: event.character,
),
);
} }
} }
@ -61,11 +75,7 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
Emitter<BackboxState> emit, Emitter<BackboxState> emit,
) async { ) async {
emit( emit(
ShareState( ShareState(score: event.score),
initials: event.initials,
score: event.score,
character: event.character,
),
); );
} }

@ -53,27 +53,19 @@ class PlayerInitialsSubmitted extends BackboxEvent {
} }
/// {@template share_score_requested} /// {@template share_score_requested}
/// Event that request the user to share score and initials. /// Event when user requests to share their score.
/// {@endtemplate} /// {@endtemplate}
class ShareScoreRequested extends BackboxEvent { class ShareScoreRequested extends BackboxEvent {
/// {@macro share_score_requested} /// {@macro share_score_requested}
const ShareScoreRequested({ const ShareScoreRequested({
required this.score, required this.score,
required this.initials,
required this.character,
}); });
/// Player's score. /// Player's score.
final int score; final int score;
/// Player's initials.
final String initials;
/// Player's character.
final CharacterTheme character;
@override @override
List<Object?> get props => [score, initials, character]; List<Object?> get props => [score];
} }
/// Event that triggers the fetching of the leaderboard /// Event that triggers the fetching of the leaderboard

@ -54,16 +54,37 @@ class InitialsFormState extends BackboxState {
List<Object?> get props => [score, character]; List<Object?> get props => [score, character];
} }
/// State when the leaderboard was successfully loaded. /// {@template initials_success_state}
/// State when the score and initials were successfully submitted.
/// {@endtemplate}
class InitialsSuccessState extends BackboxState { class InitialsSuccessState extends BackboxState {
/// {@macro initials_success_state}
const InitialsSuccessState({
required this.score,
}) : super();
/// Player's score.
final int score;
@override @override
List<Object?> get props => []; List<Object?> get props => [score];
} }
/// State when the initials submission failed. /// State when the initials submission failed.
class InitialsFailureState extends BackboxState { class InitialsFailureState extends BackboxState {
const InitialsFailureState({
required this.score,
required this.character,
});
/// Player's score.
final int score;
/// Player's character.
final CharacterTheme character;
@override @override
List<Object?> get props => []; List<Object?> get props => [score, character];
} }
/// {@template share_state} /// {@template share_state}
@ -73,19 +94,11 @@ class ShareState extends BackboxState {
/// {@macro share_state} /// {@macro share_state}
const ShareState({ const ShareState({
required this.score, required this.score,
required this.initials,
required this.character,
}) : super(); }) : super();
/// Player's score. /// Player's score.
final int score; final int score;
/// Player's initials.
final String initials;
/// Player's character.
final CharacterTheme character;
@override @override
List<Object?> get props => [score, initials, character]; List<Object?> get props => [score];
} }

@ -1,6 +1,8 @@
export 'game_over_info_display.dart';
export 'initials_input_display.dart'; export 'initials_input_display.dart';
export 'initials_submission_failure_display.dart'; export 'initials_submission_failure_display.dart';
export 'initials_submission_success_display.dart'; export 'initials_submission_success_display.dart';
export 'leaderboard_display.dart'; export 'leaderboard_display.dart';
export 'leaderboard_failure_display.dart';
export 'loading_display.dart'; export 'loading_display.dart';
export 'share_display.dart'; export 'share_display.dart';

@ -0,0 +1,311 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
import 'package:share_repository/share_repository.dart';
/// Signature for the callback called when the user tries to share their score
/// from the [GameOverInfoDisplay].
typedef OnShareTap = void Function();
final _titleTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.6,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
final _titleBoldTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.4,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
fontWeight: FontWeight.bold,
),
);
final _linkTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.7,
color: PinballColors.orange,
fontFamily: PinballFonts.pixeloidSans,
fontWeight: FontWeight.bold,
),
);
final _descriptionTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.6,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template game_over_info_display}
/// Display with links to share your score or go to the IO webpage.
/// {@endtemplate}
class GameOverInfoDisplay extends Component with HasGameRef {
/// {@macro game_over_info_display}
GameOverInfoDisplay({
OnShareTap? onShare,
}) : super(
children: [
_InstructionsComponent(
onShare: onShare,
),
],
);
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.overlays.add(PinballGame.playButtonOverlay);
}
}
class _InstructionsComponent extends PositionComponent with HasGameRef {
_InstructionsComponent({
OnShareTap? onShare,
}) : super(
anchor: Anchor.center,
position: Vector2(0, -25),
children: [
_TitleComponent(),
_LinksComponent(
onShare: onShare,
),
_DescriptionComponent(),
],
);
}
class _TitleComponent extends PositionComponent with HasGameRef {
_TitleComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 3),
children: [
_TitleBackgroundSpriteComponent(),
_ShareScoreTextComponent(),
_ChallengeFriendsTextComponent(),
],
);
}
class _ShareScoreTextComponent extends TextComponent with HasGameRef {
_ShareScoreTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, -1.5),
textRenderer: _titleTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().shareYourScore;
}
}
class _ChallengeFriendsTextComponent extends TextComponent with HasGameRef {
_ChallengeFriendsTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 1.5),
textRenderer: _titleBoldTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().andChallengeYourFriends;
}
}
class _TitleBackgroundSpriteComponent extends SpriteComponent with HasGameRef {
_TitleBackgroundSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2.zero(),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images
.fromCache(Assets.images.backbox.displayTitleDecoration.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 22;
}
}
class _LinksComponent extends PositionComponent with HasGameRef {
_LinksComponent({
OnShareTap? onShare,
}) : super(
anchor: Anchor.center,
position: Vector2(0, 9.2),
children: [
ShareLinkComponent(onTap: onShare),
GoogleIOLinkComponent(),
],
);
}
/// {@template share_link_component}
/// Link button to navigate to sharing score display.
/// {@endtemplate}
class ShareLinkComponent extends TextComponent with HasGameRef, Tappable {
/// {@macro share_link_component}
ShareLinkComponent({
OnShareTap? onTap,
}) : _onTap = onTap,
super(
anchor: Anchor.center,
position: Vector2(-7, 0),
textRenderer: _linkTextPaint,
);
final OnShareTap? _onTap;
@override
bool onTapDown(TapDownInfo info) {
_onTap?.call();
return true;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await add(
RectangleComponent(
size: Vector2(6.4, 0.2),
paint: Paint()..color = PinballColors.orange,
anchor: Anchor.center,
position: Vector2(3.2, 2.3),
),
);
text = readProvider<AppLocalizations>().share;
}
}
/// {@template google_io_link_component}
/// Link button to navigate to Google I/O site.
/// {@endtemplate}
class GoogleIOLinkComponent extends TextComponent with HasGameRef, Tappable {
/// {@macro google_io_link_component}
GoogleIOLinkComponent()
: super(
anchor: Anchor.center,
position: Vector2(6, 0),
textRenderer: _linkTextPaint,
);
@override
bool onTapDown(TapDownInfo info) {
openLink(ShareRepository.googleIOEvent);
return true;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await add(
RectangleComponent(
size: Vector2(10.2, 0.2),
paint: Paint()..color = PinballColors.orange,
anchor: Anchor.center,
position: Vector2(5.1, 2.3),
),
);
text = readProvider<AppLocalizations>().gotoIO;
}
}
class _DescriptionComponent extends PositionComponent with HasGameRef {
_DescriptionComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 13),
children: [
_LearnMoreTextComponent(),
_FirebaseTextComponent(),
OpenSourceTextComponent(),
],
);
}
class _LearnMoreTextComponent extends TextComponent with HasGameRef {
_LearnMoreTextComponent()
: super(
anchor: Anchor.center,
position: Vector2.zero(),
textRenderer: _descriptionTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().learnMore;
}
}
class _FirebaseTextComponent extends TextComponent with HasGameRef {
_FirebaseTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(-8.5, 2.5),
textRenderer: _descriptionTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().firebaseOr;
}
}
/// {@template open_source_link_component}
/// Link text to navigate to Open Source site.
/// {@endtemplate}
@visibleForTesting
class OpenSourceTextComponent extends TextComponent with HasGameRef, Tappable {
/// {@macro open_source_link_component}
OpenSourceTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(13.5, 2.5),
textRenderer: _descriptionTextPaint,
);
@override
bool onTapDown(TapDownInfo info) {
openLink(ShareRepository.openSourceCode);
return true;
}
@override
Future<void> onLoad() async {
await super.onLoad();
await add(
RectangleComponent(
size: Vector2(16, 0.2),
paint: Paint()..color = PinballColors.white,
anchor: Anchor.center,
position: Vector2(8, 2.3),
),
);
text = readProvider<AppLocalizations>().openSourceCode;
}
}

@ -77,7 +77,7 @@ class InitialsInputDisplay extends Component with HasGameRef {
); );
} }
/// Returns the current inputed initials /// Returns the current entered initials
String get initials => children String get initials => children
.whereType<InitialsLetterPrompt>() .whereType<InitialsLetterPrompt>()
.map((prompt) => prompt.char) .map((prompt) => prompt.char)

@ -1,27 +1,47 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
final _bodyTextPaint = TextPaint( final _bodyTextPaint = TextPaint(
style: const TextStyle( style: const TextStyle(
fontSize: 3, fontSize: 1.8,
color: PinballColors.white, color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans, fontFamily: PinballFonts.pixeloidSans,
fontWeight: FontWeight.w400,
), ),
); );
/// {@template initials_submission_failure_display} /// {@template initials_submission_failure_display}
/// [Backbox] display for when a failure occurs during initials submission. /// [Backbox] display for when a failure occurs during initials submission.
/// {@endtemplate} /// {@endtemplate}
class InitialsSubmissionFailureDisplay extends TextComponent { class InitialsSubmissionFailureDisplay extends Component {
/// {@macro initials_submission_failure_display}
InitialsSubmissionFailureDisplay({
required this.onDismissed,
});
final VoidCallback onDismissed;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); final l10n = readProvider<AppLocalizations>();
position = Vector2(0, -10);
anchor = Anchor.center; await addAll([
text = 'Failure!'; ErrorComponent.bold(
textRenderer = _bodyTextPaint; label: l10n.initialsErrorTitle,
position: Vector2(0, -20),
),
TextComponent(
text: l10n.initialsErrorMessage,
anchor: Anchor.center,
position: Vector2(0, -12),
textRenderer: _bodyTextPaint,
),
TimerComponent(period: 4, onTick: onDismissed),
]);
} }
} }

@ -0,0 +1,23 @@
import 'package:flame/components.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template leaderboard_failure_display}
/// Display showing an error message when the leaderboard couldn't be loaded
/// {@endtemplate}
class LeaderboardFailureDisplay extends Component {
/// {@macro leaderboard_failure_display}
LeaderboardFailureDisplay();
@override
Future<void> onLoad() async {
final l10n = readProvider<AppLocalizations>();
await add(
ErrorComponent(
label: l10n.leaderboardErrorMessage,
position: Vector2(0, -18),
),
);
}
}

@ -1,6 +1,5 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -40,7 +39,7 @@ class _BottomGroupSide extends Component {
final direction = _side.direction; final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? -0.45 : -6.8; final centerXAdjustment = _side.isLeft ? -0.45 : -6.8;
final flipper = ControlledFlipper( final flipper = Flipper(
side: _side, side: _side,
)..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6); )..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side) final baseboard = Baseboard(side: _side)

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart'; export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart'; export 'backbox/backbox.dart';
export 'bottom_group.dart'; export 'bottom_group.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart'; export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart'; export 'dino_desert/dino_desert.dart';
export 'drain/drain.dart'; export 'drain/drain.dart';

@ -30,7 +30,7 @@ class PlungerNoiseBehavior extends Component {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
readProvider<PinballPlayer>().play(PinballAudio.launcher); readProvider<PinballAudioPlayer>().play(PinballAudio.launcher);
} }
@override @override

@ -1,15 +1,15 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// Bonus obtained at the [FlutterForest]. /// Bonus obtained at the [FlutterForest].
/// ///
/// When all [DashNestBumper]s are hit at least once three times, the [Signpost] /// When all [DashBumper]s are hit at least once three times, the [Signpost]
/// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest] /// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball]. /// is awarded, and the [DashBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component class FlutterForestBonusBehavior extends Component
with with
ParentIsA<FlutterForest>, ParentIsA<FlutterForest>,
@ -19,15 +19,14 @@ class FlutterForestBonusBehavior extends Component
void onMount() { void onMount() {
super.onMount(); super.onMount();
final bumpers = parent.children.whereType<DashNestBumper>(); final bumpers = parent.children.whereType<DashBumper>();
final signpost = parent.firstChild<Signpost>()!; final signpost = parent.firstChild<Signpost>()!;
final animatronic = parent.firstChild<DashAnimatronic>()!; final animatronic = parent.firstChild<DashAnimatronic>()!;
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
for (final bumper in bumpers) { for (final bumper in bumpers) {
bumper.bloc.stream.listen((state) { bumper.bloc.stream.listen((state) {
final activatedAllBumpers = bumpers.every( final activatedAllBumpers = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active, (bumper) => bumper.bloc.state == DashBumperState.active,
); );
if (activatedAllBumpers) { if (activatedAllBumpers) {
@ -38,15 +37,7 @@ class FlutterForestBonusBehavior extends Component
if (signpost.bloc.isFullyProgressed()) { if (signpost.bloc.isFullyProgressed()) {
bloc.add(const BonusActivated(GameBonus.dashNest)); bloc.add(const BonusActivated(GameBonus.dashNest));
final characterTheme = add(BonusBallSpawningBehavior());
readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme;
canvas.add(
Ball(assetPath: characterTheme.ball.keyName)
..initialPosition = Vector2(29.2, -24.5)
..zIndex = ZIndexes.ballOnBoard,
);
animatronic.playing = true; animatronic.playing = true;
signpost.bloc.onProgressed(); signpost.bloc.onProgressed();
} }

@ -9,7 +9,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template flutter_forest} /// {@template flutter_forest}
/// Area positioned at the top right of the board where the [Ball] can bounce /// Area positioned at the top right of the board where the [Ball] can bounce
/// off [DashNestBumper]s. /// off [DashBumper]s.
/// {@endtemplate} /// {@endtemplate}
class FlutterForest extends Component with ZIndex { class FlutterForest extends Component with ZIndex {
/// {@macro flutter_forest} /// {@macro flutter_forest}
@ -22,19 +22,19 @@ class FlutterForest extends Component with ZIndex {
BumperNoiseBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(7.95, -58.35), )..initialPosition = Vector2(7.95, -58.35),
DashNestBumper.main( DashBumper.main(
children: [ children: [
ScoringContactBehavior(points: Points.twoHundredThousand), ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoiseBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(18.55, -59.35), )..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a( DashBumper.a(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(8.95, -51.95), )..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b( DashBumper.b(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(), BumperNoiseBehavior(),

@ -3,6 +3,7 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// Listens to the [GameBloc] and updates the game accordingly. /// Listens to the [GameBloc] and updates the game accordingly.
@ -19,18 +20,39 @@ class GameBlocStatusListener extends Component
case GameStatus.waiting: case GameStatus.waiting:
break; break;
case GameStatus.playing: case GameStatus.playing:
readProvider<PinballPlayer>().play(PinballAudio.backgroundMusic); readProvider<PinballAudioPlayer>().play(PinballAudio.backgroundMusic);
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_addFlipperKeyControls);
gameRef.overlays.remove(PinballGame.playButtonOverlay); gameRef.overlays.remove(PinballGame.playButtonOverlay);
break; break;
case GameStatus.gameOver: case GameStatus.gameOver:
readProvider<PinballPlayer>().play(PinballAudio.gameOverVoiceOver); readProvider<PinballAudioPlayer>().play(PinballAudio.gameOverVoiceOver);
gameRef.descendants().whereType<Backbox>().first.requestInitials( gameRef.descendants().whereType<Backbox>().first.requestInitials(
score: state.displayScore, score: state.displayScore,
character: readBloc<CharacterThemeCubit, CharacterThemeState>() character: readBloc<CharacterThemeCubit, CharacterThemeState>()
.state .state
.characterTheme, .characterTheme,
); );
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_removeFlipperKeyControls);
break; break;
} }
} }
void _addFlipperKeyControls(Flipper flipper) {
flipper
..add(FlipperKeyControllingBehavior())
..moveDown();
}
void _removeFlipperKeyControls(Flipper flipper) => flipper
.descendants()
.whereType<FlipperKeyControllingBehavior>()
.forEach(flipper.remove);
} }

@ -1,3 +1,4 @@
import 'package:flame/extensions.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart' as components; import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
@ -5,7 +6,7 @@ import 'package:pinball_theme/pinball_theme.dart' hide Assets;
/// Add methods to help loading and caching game assets. /// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame { 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<Image>> preLoadAssets() {
const dashTheme = DashTheme(); const dashTheme = DashTheme();
const sparkyTheme = SparkyTheme(); const sparkyTheme = SparkyTheme();
const androidTheme = AndroidTheme(); const androidTheme = AndroidTheme();
@ -102,6 +103,9 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.backbox.displayDivider.keyName), images.load(components.Assets.images.backbox.displayDivider.keyName),
images.load(components.Assets.images.backbox.button.facebook.keyName), images.load(components.Assets.images.backbox.button.facebook.keyName),
images.load(components.Assets.images.backbox.button.twitter.keyName), images.load(components.Assets.images.backbox.button.twitter.keyName),
images.load(
components.Assets.images.backbox.displayTitleDecoration.keyName,
),
images.load(components.Assets.images.googleWord.letter1.lit.keyName), images.load(components.Assets.images.googleWord.letter1.lit.keyName),
images.load(components.Assets.images.googleWord.letter1.dimmed.keyName), images.load(components.Assets.images.googleWord.letter1.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter2.lit.keyName), images.load(components.Assets.images.googleWord.letter2.lit.keyName),

@ -24,10 +24,10 @@ class PinballGame extends PinballForge2DGame
required this.shareRepository, required this.shareRepository,
required GameBloc gameBloc, required GameBloc gameBloc,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballPlayer player, required PinballAudioPlayer audioPlayer,
}) : focusNode = FocusNode(), }) : focusNode = FocusNode(),
_gameBloc = gameBloc, _gameBloc = gameBloc,
_player = player, _audioPlayer = audioPlayer,
_characterThemeBloc = characterThemeBloc, _characterThemeBloc = characterThemeBloc,
_l10n = l10n, _l10n = l10n,
super( super(
@ -39,6 +39,9 @@ class PinballGame extends PinballForge2DGame
/// Identifier of the play button overlay /// Identifier of the play button overlay
static const playButtonOverlay = 'play_button'; static const playButtonOverlay = 'play_button';
/// Identifier of the mobile controls overlay
static const mobileControlsOverlay = 'mobile_controls';
@override @override
Color backgroundColor() => Colors.transparent; Color backgroundColor() => Colors.transparent;
@ -46,7 +49,7 @@ class PinballGame extends PinballForge2DGame
final CharacterThemeCubit _characterThemeBloc; final CharacterThemeCubit _characterThemeBloc;
final PinballPlayer _player; final PinballAudioPlayer _audioPlayer;
final LeaderboardRepository leaderboardRepository; final LeaderboardRepository leaderboardRepository;
@ -56,6 +59,18 @@ class PinballGame extends PinballForge2DGame
final GameBloc _gameBloc; final GameBloc _gameBloc;
List<LeaderboardEntryData>? _entries;
Future<void> preFetchLeaderboard() async {
try {
_entries = await leaderboardRepository.fetchTop10Leaderboard();
} catch (_) {
// An initial null leaderboard means that we couldn't fetch
// the entries for the [Backbox] and it will show the relevant display.
_entries = null;
}
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await add( await add(
@ -71,7 +86,7 @@ class PinballGame extends PinballForge2DGame
children: [ children: [
MultiFlameProvider( MultiFlameProvider(
providers: [ providers: [
FlameProvider<PinballPlayer>.value(_player), FlameProvider<PinballAudioPlayer>.value(_audioPlayer),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository), FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<ShareRepository>.value(shareRepository), FlameProvider<ShareRepository>.value(shareRepository),
FlameProvider<AppLocalizations>.value(_l10n), FlameProvider<AppLocalizations>.value(_l10n),
@ -96,6 +111,7 @@ class PinballGame extends PinballForge2DGame
Backbox( Backbox(
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
entries: _entries,
), ),
GoogleWord(position: Vector2(-4.45, 1.8)), GoogleWord(position: Vector2(-4.45, 1.8)),
Multipliers(), Multipliers(),
@ -133,7 +149,7 @@ class PinballGame extends PinballForge2DGame
final rocket = descendants().whereType<RocketSpriteComponent>().first; final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size; final bounds = rocket.topLeftPosition & rocket.size;
// NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. // NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually.
if (bounds.contains(info.eventPosition.game.toOffset())) { if (bounds.contains(info.eventPosition.game.toOffset())) {
descendants().whereType<Plunger>().single.pullFor(2); descendants().whereType<Plunger>().single.pullFor(2);
} else { } else {
@ -179,11 +195,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository, required ShareRepository shareRepository,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballPlayer player, required PinballAudioPlayer audioPlayer,
required GameBloc gameBloc, required GameBloc gameBloc,
}) : super( }) : super(
characterThemeBloc: characterThemeBloc, characterThemeBloc: characterThemeBloc,
player: player, audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
l10n: l10n, l10n: l10n,

@ -22,23 +22,17 @@ class PinballGamePage extends StatelessWidget {
final bool isDebugMode; final bool isDebugMode;
static Route route({bool isDebugMode = kDebugMode}) {
return MaterialPageRoute<void>(
builder: (_) => PinballGamePage(isDebugMode: isDebugMode),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final characterThemeBloc = context.read<CharacterThemeCubit>(); final characterThemeBloc = context.read<CharacterThemeCubit>();
final player = context.read<PinballPlayer>(); final audioPlayer = context.read<PinballAudioPlayer>();
final leaderboardRepository = context.read<LeaderboardRepository>(); final leaderboardRepository = context.read<LeaderboardRepository>();
final shareRepository = context.read<ShareRepository>(); final shareRepository = context.read<ShareRepository>();
final gameBloc = context.read<GameBloc>(); final gameBloc = context.read<GameBloc>();
final game = isDebugMode final game = isDebugMode
? DebugPinballGame( ? DebugPinballGame(
characterThemeBloc: characterThemeBloc, characterThemeBloc: characterThemeBloc,
player: player, audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
l10n: context.l10n, l10n: context.l10n,
@ -46,58 +40,46 @@ class PinballGamePage extends StatelessWidget {
) )
: PinballGame( : PinballGame(
characterThemeBloc: characterThemeBloc, characterThemeBloc: characterThemeBloc,
player: player, audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
l10n: context.l10n, l10n: context.l10n,
gameBloc: gameBloc, gameBloc: gameBloc,
); );
final loadables = [ return Container(
...game.preLoadAssets(), decoration: const CrtBackground(),
...player.load(), child: Scaffold(
...BonusAnimation.loadAssets(), backgroundColor: PinballColors.transparent,
...SelectedCharacter.loadAssets(), body: BlocProvider(
]; create: (_) => AssetsManagerCubit(game, audioPlayer)..load(),
child: PinballGameView(game),
return BlocProvider( ),
create: (_) => AssetsManagerCubit(loadables)..load(), ),
child: PinballGameView(game: game),
); );
} }
} }
class PinballGameView extends StatelessWidget { class PinballGameView extends StatelessWidget {
const PinballGameView({ const PinballGameView(this.game, {Key? key}) : super(key: key);
Key? key,
required this.game,
}) : super(key: key);
final PinballGame game; final PinballGame game;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLoading = context.select( return BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
(AssetsManagerCubit bloc) => bloc.state.progress != 1, builder: (context, state) {
); return state.isLoading
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: isLoading
? const AssetsLoadingPage() ? const AssetsLoadingPage()
: PinballGameLoadedView(game: game), : PinballGameLoadedView(game);
), },
); );
} }
} }
@visibleForTesting @visibleForTesting
class PinballGameLoadedView extends StatelessWidget { class PinballGameLoadedView extends StatelessWidget {
const PinballGameLoadedView({ const PinballGameLoadedView(this.game, {Key? key}) : super(key: key);
Key? key,
required this.game,
}) : super(key: key);
final PinballGame game; final PinballGame game;
@ -126,6 +108,14 @@ class PinballGameLoadedView extends StatelessWidget {
child: PlayButtonOverlay(), child: PlayButtonOverlay(),
); );
}, },
PinballGame.mobileControlsOverlay: (context, game) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: MobileControls(game: game),
);
},
}, },
), ),
), ),

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template mobile_controls}
/// Widget with the controls used to enable the user initials input on mobile.
/// {@endtemplate}
class MobileControls extends StatelessWidget {
/// {@macro mobile_controls}
const MobileControls({
Key? key,
required this.game,
}) : super(key: key);
/// Game instance
final PinballGame game;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
MobileDpad(
onTapUp: () => game.triggerVirtualKeyUp(LogicalKeyboardKey.arrowUp),
onTapDown: () => game.triggerVirtualKeyUp(
LogicalKeyboardKey.arrowDown,
),
onTapLeft: () => game.triggerVirtualKeyUp(
LogicalKeyboardKey.arrowLeft,
),
onTapRight: () => game.triggerVirtualKeyUp(
LogicalKeyboardKey.arrowRight,
),
),
PinballButton(
text: l10n.enter,
onTap: () => game.triggerVirtualKeyUp(LogicalKeyboardKey.enter),
),
],
);
}
}

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template mobile_dpad}
/// Widget rendering 4 directional input arrows.
/// {@endtemplate}
class MobileDpad extends StatelessWidget {
/// {@template mobile_dpad}
const MobileDpad({
Key? key,
required this.onTapUp,
required this.onTapDown,
required this.onTapLeft,
required this.onTapRight,
}) : super(key: key);
static const _size = 180.0;
/// Called when dpad up is pressed
final VoidCallback onTapUp;
/// Called when dpad down is pressed
final VoidCallback onTapDown;
/// Called when dpad left is pressed
final VoidCallback onTapLeft;
/// Called when dpad right is pressed
final VoidCallback onTapRight;
@override
Widget build(BuildContext context) {
return SizedBox(
width: _size,
height: _size,
child: Column(
children: [
Row(
children: [
const Spacer(),
PinballDpadButton(
direction: PinballDpadDirection.up,
onTap: onTapUp,
),
const Spacer(),
],
),
Row(
children: [
PinballDpadButton(
direction: PinballDpadDirection.left,
onTap: onTapLeft,
),
const Spacer(),
PinballDpadButton(
direction: PinballDpadDirection.right,
onTap: onTapRight,
),
],
),
Row(
children: [
const Spacer(),
PinballDpadButton(
direction: PinballDpadDirection.down,
onTap: onTapDown,
),
const Spacer(),
],
),
],
),
);
}
}

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template replay_button_overlay}
/// [Widget] that renders the button responsible for restarting the game.
/// {@endtemplate}
class ReplayButtonOverlay extends StatelessWidget {
/// {@macro replay_button_overlay}
const ReplayButtonOverlay({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return PinballButton(
text: l10n.replay,
onTap: () {
context.read<StartGameBloc>().add(const ReplayTapped());
},
);
}
}

@ -1,5 +1,8 @@
export 'bonus_animation.dart'; export 'bonus_animation.dart';
export 'game_hud.dart'; export 'game_hud.dart';
export 'mobile_controls.dart';
export 'mobile_dpad.dart';
export 'play_button_overlay.dart'; export 'play_button_overlay.dart';
export 'replay_button_overlay.dart';
export 'round_count_display.dart'; export 'round_count_display.dart';
export 'score_view.dart'; export 'score_view.dart';

@ -15,6 +15,8 @@ class $AssetsImagesGen {
$AssetsImagesComponentsGen get components => $AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen(); $AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesLoadingGameGen get loadingGame =>
const $AssetsImagesLoadingGameGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
} }
@ -62,6 +64,14 @@ class $AssetsImagesLinkBoxGen {
const AssetGenImage('assets/images/link_box/info_icon.png'); const AssetGenImage('assets/images/link_box/info_icon.png');
} }
class $AssetsImagesLoadingGameGen {
const $AssetsImagesLoadingGameGen();
/// File path: assets/images/loading_game/io_pinball.png
AssetGenImage get ioPinball =>
const AssetGenImage('assets/images/loading_game/io_pinball.png');
}
class $AssetsImagesScoreGen { class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen(); const $AssetsImagesScoreGen();

@ -91,7 +91,9 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
return WillPopScope( return WillPopScope(
onWillPop: () { onWillPop: () {
widget.onDismissCallback.call(); widget.onDismissCallback.call();
context.read<PinballPlayer>().play(PinballAudio.ioPinballVoiceOver); context
.read<PinballAudioPlayer>()
.play(PinballAudio.ioPinballVoiceOver);
return Future.value(true); return Future.value(true);
}, },
child: PinballDialog( child: PinballDialog(
@ -240,7 +242,7 @@ class _DesktopFlipperControls extends StatelessWidget {
children: [ children: [
Text( Text(
l10n.flipperControls, l10n.flipperControls,
style: Theme.of(context).textTheme.subtitle2, style: Theme.of(context).textTheme.headline4,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Column( Column(

@ -148,9 +148,57 @@
"@loading": { "@loading": {
"description": "Text shown to indicate loading times" "description": "Text shown to indicate loading times"
}, },
"replay": "Replay",
"@replay": {
"description": "Text displayed on the share page for replay button"
},
"ioPinball": "I/O Pinball", "ioPinball": "I/O Pinball",
"@ioPinball": { "@ioPinball": {
"description": "I/O Pinball - Name of the game" "description": "I/O Pinball - Name of the game"
},
"shareYourScore": "Share your score",
"@shareYourScore": {
"description": "Text shown on title of info screen"
},
"andChallengeYourFriends": "AND CHALLENGE YOUR FRIENDS",
"@challengeYourFriends": {
"description": "Text shown on title of info screen"
},
"share": "SHARE",
"@share": {
"description": "Text for share link on info screen."
},
"gotoIO": "GO TO I/O",
"@gotoIO": {
"description": "Text for going to I/O site link on info screen."
},
"learnMore": "Learn more about building games in Flutter with",
"@learnMore": {
"description": "Text shown on description of info screen"
},
"firebaseOr": "Firebase or dive right into the",
"@firebaseOr": {
"description": "Text shown on description of info screen"
},
"openSourceCode": "open source code.",
"@openSourceCode": {
"description": "Text shown on description of info screen"
},
"enter": "Enter",
"@enter": {
"description": "Text shown on the mobile controls enter button"
},
"initialsErrorTitle": "Uh-oh... well, that didnt work",
"@enter": {
"description": "Title shown when the initials submission fails"
},
"initialsErrorMessage": "Please try a different combination of letters",
"@initialsErrorMessage": {
"description": "Message on shown when the initials submission fails"
},
"leaderboardErrorMessage": "No connection. Leaderboard and sharing functionality is unavailable.",
"@leaderboardErrorMessage": {
"description": "Text shown when the leaderboard had an error while loading"
} }
, ,
"letEveryone": "Let everyone know about I/O Pinball", "letEveryone": "Let everyone know about I/O Pinball",

@ -14,7 +14,7 @@ void main() {
const shareRepository = const shareRepository =
ShareRepository(appUrl: ShareRepository.googleIOEvent); ShareRepository(appUrl: ShareRepository.googleIOEvent);
final authenticationRepository = AuthenticationRepository(firebaseAuth); final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballPlayer = PinballPlayer(); final pinballAudioPlayer = PinballAudioPlayer();
unawaited( unawaited(
Firebase.initializeApp().then( Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(), (_) => authenticationRepository.authenticateAnonymously(),
@ -24,7 +24,7 @@ void main() {
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository, shareRepository: shareRepository,
pinballPlayer: pinballPlayer, pinballAudioPlayer: pinballAudioPlayer,
); );
}); });
} }

@ -1,30 +0,0 @@
import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:share_repository/share_repository.dart';
void main() {
bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore);
const shareRepository =
ShareRepository(appUrl: ShareRepository.googleIOEvent);
final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballPlayer = PinballPlayer();
unawaited(
Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(),
),
);
return App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
pinballPlayer: pinballPlayer,
);
});
}

@ -1,30 +0,0 @@
import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:share_repository/share_repository.dart';
void main() {
bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore);
const shareRepository =
ShareRepository(appUrl: ShareRepository.googleIOEvent);
final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballPlayer = PinballPlayer();
unawaited(
Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(),
),
);
return App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
pinballPlayer: pinballPlayer,
);
});
}

@ -55,8 +55,6 @@ class _LinkBoxHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
final indent = MediaQuery.of(context).size.width / 5;
return Column( return Column(
children: [ children: [
Text( Text(
@ -68,11 +66,9 @@ class _LinkBoxHeader extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Divider( const SizedBox(
color: PinballColors.white, width: 200,
endIndent: indent, child: Divider(color: PinballColors.white, thickness: 2),
indent: indent,
thickness: 2,
), ),
], ],
); );

@ -11,6 +11,7 @@ class StartGameBloc extends Bloc<StartGameEvent, StartGameState> {
/// {@macro start_game_bloc} /// {@macro start_game_bloc}
StartGameBloc() : super(const StartGameState.initial()) { StartGameBloc() : super(const StartGameState.initial()) {
on<PlayTapped>(_onPlayTapped); on<PlayTapped>(_onPlayTapped);
on<ReplayTapped>(_onReplayTapped);
on<CharacterSelected>(_onCharacterSelected); on<CharacterSelected>(_onCharacterSelected);
on<HowToPlayFinished>(_onHowToPlayFinished); on<HowToPlayFinished>(_onHowToPlayFinished);
} }
@ -26,6 +27,17 @@ class StartGameBloc extends Bloc<StartGameEvent, StartGameState> {
); );
} }
void _onReplayTapped(
ReplayTapped event,
Emitter<StartGameState> emit,
) {
emit(
state.copyWith(
status: StartGameStatus.selectCharacter,
),
);
}
void _onCharacterSelected( void _onCharacterSelected(
CharacterSelected event, CharacterSelected event,
Emitter<StartGameState> emit, Emitter<StartGameState> emit,

@ -19,6 +19,17 @@ class PlayTapped extends StartGameEvent {
List<Object> get props => []; List<Object> get props => [];
} }
/// {@template replay_tapped}
/// Replay tapped event.
/// {@endtemplate}
class ReplayTapped extends StartGameEvent {
/// {@macro replay_tapped}
const ReplayTapped();
@override
List<Object> get props => [];
}
/// {@template character_selected} /// {@template character_selected}
/// Character selected event. /// Character selected event.
/// {@endtemplate} /// {@endtemplate}

@ -14,8 +14,11 @@ class $AssetsMusicGen {
class $AssetsSfxGen { class $AssetsSfxGen {
const $AssetsSfxGen(); const $AssetsSfxGen();
String get android => 'assets/sfx/android.mp3';
String get bumperA => 'assets/sfx/bumper_a.mp3'; String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3'; String get bumperB => 'assets/sfx/bumper_b.mp3';
String get dash => 'assets/sfx/dash.mp3';
String get dino => 'assets/sfx/dino.mp3';
String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3'; String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3';
String get google => 'assets/sfx/google.mp3'; String get google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';

@ -28,6 +28,15 @@ enum PinballAudio {
/// Sparky /// Sparky
sparky, sparky,
/// Android
android,
/// Dino
dino,
/// Dash
dash,
} }
/// Defines the contract of the creation of an [AudioPool]. /// Defines the contract of the creation of an [AudioPool].
@ -136,12 +145,12 @@ class _BumperAudio extends _Audio {
} }
} }
/// {@template pinball_player} /// {@template pinball_audio_player}
/// Sound manager for the pinball game /// Sound manager for the pinball game
/// {@endtemplate} /// {@endtemplate}
class PinballPlayer { class PinballAudioPlayer {
/// {@macro pinball_player} /// {@macro pinball_audio_player}
PinballPlayer({ PinballAudioPlayer({
CreateAudioPool? createAudioPool, CreateAudioPool? createAudioPool,
PlaySingleAudio? playSingleAudio, PlaySingleAudio? playSingleAudio,
LoopSingleAudio? loopSingleAudio, LoopSingleAudio? loopSingleAudio,
@ -169,6 +178,21 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.sparky, path: Assets.sfx.sparky,
), ),
PinballAudio.dino: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.dino,
),
PinballAudio.dash: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.dash,
),
PinballAudio.android: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.android,
),
PinballAudio.launcher: _SimplePlayAudio( PinballAudio.launcher: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
@ -220,7 +244,7 @@ class PinballPlayer {
return audios.values.map((a) => a.load()).toList(); return audios.values.map((a) => a.load()).toList();
} }
/// Plays the received auido /// Plays the received audio
void play(PinballAudio audio) { void play(PinballAudio audio) {
assert( assert(
audios.containsKey(audio), audios.containsKey(audio),

@ -51,7 +51,7 @@ void main() {
late _MockLoopSingleAudio loopSingleAudio; late _MockLoopSingleAudio loopSingleAudio;
late _PreCacheSingleAudio preCacheSingleAudio; late _PreCacheSingleAudio preCacheSingleAudio;
late Random seed; late Random seed;
late PinballPlayer player; late PinballAudioPlayer audioPlayer;
setUpAll(() { setUpAll(() {
registerFallbackValue(_MockAudioCache()); registerFallbackValue(_MockAudioCache());
@ -81,7 +81,7 @@ void main() {
seed = _MockRandom(); seed = _MockRandom();
player = PinballPlayer( audioPlayer = PinballAudioPlayer(
configureAudioCache: configureAudioCache.onCall, configureAudioCache: configureAudioCache.onCall,
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
@ -92,12 +92,12 @@ void main() {
}); });
test('can be instantiated', () { test('can be instantiated', () {
expect(PinballPlayer(), isNotNull); expect(PinballAudioPlayer(), isNotNull);
}); });
group('load', () { group('load', () {
test('creates the bumpers pools', () async { test('creates the bumpers pools', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
verify( verify(
() => createAudioPool.onCall( () => createAudioPool.onCall(
@ -117,25 +117,25 @@ void main() {
}); });
test('configures the audio cache instance', () async { test('configures the audio cache instance', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
verify(() => configureAudioCache.onCall(FlameAudio.audioCache)) verify(() => configureAudioCache.onCall(FlameAudio.audioCache))
.called(1); .called(1);
}); });
test('sets the correct prefix', () async { test('sets the correct prefix', () async {
player = PinballPlayer( audioPlayer = PinballAudioPlayer(
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall,
); );
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
expect(FlameAudio.audioCache.prefix, equals('')); expect(FlameAudio.audioCache.prefix, equals(''));
}); });
test('pre cache the assets', () async { test('pre cache the assets', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
@ -145,6 +145,18 @@ void main() {
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/sparky.mp3'), .onCall('packages/pinball_audio/assets/sfx/sparky.mp3'),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/dino.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/android.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/dash.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio.onCall( () => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3', 'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
@ -197,8 +209,8 @@ void main() {
group('when seed is true', () { group('when seed is true', () {
test('plays the bumper A sound pool', () async { test('plays the bumper A sound pool', () async {
when(seed.nextBool).thenReturn(true); when(seed.nextBool).thenReturn(true);
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.bumper); audioPlayer.play(PinballAudio.bumper);
verify(() => bumperAPool.start(volume: 0.6)).called(1); verify(() => bumperAPool.start(volume: 0.6)).called(1);
}); });
@ -207,8 +219,8 @@ void main() {
group('when seed is false', () { group('when seed is false', () {
test('plays the bumper B sound pool', () async { test('plays the bumper B sound pool', () async {
when(seed.nextBool).thenReturn(false); when(seed.nextBool).thenReturn(false);
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.bumper); audioPlayer.play(PinballAudio.bumper);
verify(() => bumperBPool.start(volume: 0.6)).called(1); verify(() => bumperBPool.start(volume: 0.6)).called(1);
}); });
@ -217,8 +229,8 @@ void main() {
group('google', () { group('google', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.google); audioPlayer.play(PinballAudio.google);
verify( verify(
() => playSingleAudio () => playSingleAudio
@ -229,8 +241,8 @@ void main() {
group('sparky', () { group('sparky', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.sparky); audioPlayer.play(PinballAudio.sparky);
verify( verify(
() => playSingleAudio () => playSingleAudio
@ -239,10 +251,46 @@ void main() {
}); });
}); });
group('dino', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.dino);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.dino}'),
).called(1);
});
});
group('android', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.android);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.android}'),
).called(1);
});
});
group('dash', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.dash);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.dash}'),
).called(1);
});
});
group('launcher', () { group('launcher', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.launcher); audioPlayer.play(PinballAudio.launcher);
verify( verify(
() => playSingleAudio () => playSingleAudio
@ -253,8 +301,8 @@ void main() {
group('ioPinballVoiceOver', () { group('ioPinballVoiceOver', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.ioPinballVoiceOver); audioPlayer.play(PinballAudio.ioPinballVoiceOver);
verify( verify(
() => playSingleAudio.onCall( () => playSingleAudio.onCall(
@ -266,8 +314,8 @@ void main() {
group('gameOverVoiceOver', () { group('gameOverVoiceOver', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.gameOverVoiceOver); audioPlayer.play(PinballAudio.gameOverVoiceOver);
verify( verify(
() => playSingleAudio.onCall( () => playSingleAudio.onCall(
@ -279,8 +327,8 @@ void main() {
group('backgroundMusic', () { group('backgroundMusic', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.backgroundMusic); audioPlayer.play(PinballAudio.backgroundMusic);
verify( verify(
() => loopSingleAudio () => loopSingleAudio
@ -292,10 +340,13 @@ void main() {
test( test(
'throws assertions error when playing an unregistered audio', 'throws assertions error when playing an unregistered audio',
() async { () async {
player.audios.remove(PinballAudio.google); audioPlayer.audios.remove(PinballAudio.google);
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
expect(() => player.play(PinballAudio.google), throwsAssertionError); expect(
() => audioPlayer.play(PinballAudio.google),
throwsAssertionError,
);
}, },
); );
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

@ -66,6 +66,10 @@ class $AssetsImagesBackboxGen {
AssetGenImage get displayDivider => AssetGenImage get displayDivider =>
const AssetGenImage('assets/images/backbox/display-divider.png'); const AssetGenImage('assets/images/backbox/display-divider.png');
/// File path: assets/images/backbox/display_title_decoration.png
AssetGenImage get displayTitleDecoration =>
const AssetGenImage('assets/images/backbox/display_title_decoration.png');
/// File path: assets/images/backbox/marquee.png /// File path: assets/images/backbox/marquee.png
AssetGenImage get marquee => AssetGenImage get marquee =>
const AssetGenImage('assets/images/backbox/marquee.png'); const AssetGenImage('assets/images/backbox/marquee.png');

@ -11,10 +11,8 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_spaceship_cubit.dart'; export 'cubit/android_spaceship_cubit.dart';
class AndroidSpaceship extends Component { class AndroidSpaceship extends Component {
AndroidSpaceship({ AndroidSpaceship({required Vector2 position})
required Vector2 position, : super(
}) : bloc = AndroidSpaceshipCubit(),
super(
children: [ children: [
_SpaceshipSaucer()..initialPosition = position, _SpaceshipSaucer()..initialPosition = position,
_SpaceshipSaucerSpriteAnimationComponent()..position = position, _SpaceshipSaucerSpriteAnimationComponent()..position = position,
@ -38,17 +36,8 @@ class AndroidSpaceship extends Component {
/// This can be used for testing [AndroidSpaceship]'s behaviors in isolation. /// This can be used for testing [AndroidSpaceship]'s behaviors in isolation.
@visibleForTesting @visibleForTesting
AndroidSpaceship.test({ AndroidSpaceship.test({
required this.bloc,
Iterable<Component>? children, Iterable<Component>? children,
}) : super(children: children); }) : super(children: children);
final AndroidSpaceshipCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
} }
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered { class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {

@ -1,14 +1,18 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
class AndroidSpaceshipEntranceBallContactBehavior class AndroidSpaceshipEntranceBallContactBehavior
extends ContactBehavior<AndroidSpaceshipEntrance> { extends ContactBehavior<AndroidSpaceshipEntrance>
with FlameBlocReader<AndroidSpaceshipCubit, AndroidSpaceshipState> {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);
if (other is! Ball) return; if (other is! Ball) return;
parent.parent.bloc.onBallEntered(); bloc.onBallEntered();
} }
} }

@ -68,7 +68,7 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
return world.createBody(bodyDef)..createFixtureFromShape(shape, 1); return world.createBody(bodyDef)..createFixtureFromShape(shape, 1);
} }
/// Immediatly and completly [stop]s the ball. /// Immediately and completely [stop]s the ball.
/// ///
/// The [Ball] will no longer be affected by any forces, including it's /// The [Ball] will no longer be affected by any forces, including it's
/// weight and those emitted from collisions. /// weight and those emitted from collisions.

@ -0,0 +1,22 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ball_impulsing_behavior}
/// Impulses the [Ball] in a given direction.
/// {@endtemplate}
class BallImpulsingBehavior extends Component with ParentIsA<Ball> {
/// {@macro ball_impulsing_behavior}
BallImpulsingBehavior({
required Vector2 impulse,
}) : _impulse = impulse;
final Vector2 _impulse;
@override
Future<void> onLoad() async {
await super.onLoad();
parent.body.linearVelocity = _impulse;
shouldRemove = true;
}
}

@ -1,3 +1,4 @@
export 'ball_gravitating_behavior.dart'; export 'ball_gravitating_behavior.dart';
export 'ball_impulsing_behavior.dart';
export 'ball_scaling_behavior.dart'; export 'ball_scaling_behavior.dart';
export 'ball_turbo_charging_behavior.dart'; export 'ball_turbo_charging_behavior.dart';

@ -12,7 +12,7 @@ class BumpingBehavior extends ContactBehavior {
/// Determines how strong the bump is. /// Determines how strong the bump is.
final double _strength; final double _strength;
/// This is used to recoginze the current state of a contact manifold in world /// This is used to recognize the current state of a contact manifold in world
/// coordinates. /// coordinates.
@visibleForTesting @visibleForTesting
final WorldManifold worldManifold = WorldManifold(); final WorldManifold worldManifold = WorldManifold();

@ -10,12 +10,11 @@ export 'boundaries.dart';
export 'camera_zoom.dart'; export 'camera_zoom.dart';
export 'chrome_dino/chrome_dino.dart'; export 'chrome_dino/chrome_dino.dart';
export 'dash_animatronic.dart'; export 'dash_animatronic.dart';
export 'dash_nest_bumper/dash_nest_bumper.dart'; export 'dash_bumper/dash_bumper.dart';
export 'dino_walls.dart'; export 'dino_walls.dart';
export 'error_component.dart'; export 'error_component.dart';
export 'fire_effect.dart';
export 'flapper/flapper.dart'; export 'flapper/flapper.dart';
export 'flipper.dart'; export 'flipper/flipper.dart';
export 'google_letter/google_letter.dart'; export 'google_letter/google_letter.dart';
export 'initial_position.dart'; export 'initial_position.dart';
export 'joint_anchor.dart'; export 'joint_anchor.dart';
@ -26,7 +25,7 @@ export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart'; export 'multiplier/multiplier.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'rocket.dart'; export 'rocket.dart';
export 'score_component.dart'; export 'score_component/score_component.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'signpost/signpost.dart'; export 'signpost/signpost.dart';
export 'skill_shot/skill_shot.dart'; export 'skill_shot/skill_shot.dart';

@ -2,7 +2,7 @@ import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template dash_animatronic} /// {@template dash_animatronic}
/// Animated Dash that sits on top of the [DashNestBumper.main]. /// Animated Dash that sits on top of the [DashBumper.main].
/// {@endtemplate} /// {@endtemplate}
class DashAnimatronic extends SpriteAnimationComponent with HasGameRef { class DashAnimatronic extends SpriteAnimationComponent with HasGameRef {
/// {@macro dash_animatronic} /// {@macro dash_animatronic}

@ -2,8 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
class DashNestBumperBallContactBehavior class DashBumperBallContactBehavior extends ContactBehavior<DashBumper> {
extends ContactBehavior<DashNestBumper> {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);

@ -0,0 +1,17 @@
import 'package:bloc/bloc.dart';
part 'dash_bumper_state.dart';
class DashBumperCubit extends Cubit<DashBumperState> {
DashBumperCubit() : super(DashBumperState.inactive);
/// Event added when the bumper contacts with a ball.
void onBallContacted() {
emit(DashBumperState.active);
}
/// Event added when the bumper should return to its initial configuration.
void onReset() {
emit(DashBumperState.inactive);
}
}

@ -0,0 +1,10 @@
part of 'dash_bumper_cubit.dart';
/// Indicates the [DashBumperCubit]'s current state.
enum DashBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}

@ -5,17 +5,17 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/bumping_behavior.dart'; import 'package:pinball_components/src/components/bumping_behavior.dart';
import 'package:pinball_components/src/components/dash_nest_bumper/behaviors/behaviors.dart'; import 'package:pinball_components/src/components/dash_bumper/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/dash_nest_bumper_cubit.dart'; export 'cubit/dash_bumper_cubit.dart';
/// {@template dash_nest_bumper} /// {@template dash_bumper}
/// Bumper with a nest appearance. /// Bumper for the flutter forest.
/// {@endtemplate} /// {@endtemplate}
class DashNestBumper extends BodyComponent with InitialPosition { class DashBumper extends BodyComponent with InitialPosition {
/// {@macro dash_nest_bumper} /// {@macro dash_bumper}
DashNestBumper._({ DashBumper._({
required double majorRadius, required double majorRadius,
required double minorRadius, required double minorRadius,
required String activeAssetPath, required String activeAssetPath,
@ -28,19 +28,22 @@ class DashNestBumper extends BodyComponent with InitialPosition {
super( super(
renderBody: false, renderBody: false,
children: [ children: [
_DashNestBumperSpriteGroupComponent( _DashBumperSpriteGroupComponent(
activeAssetPath: activeAssetPath, activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath, inactiveAssetPath: inactiveAssetPath,
position: spritePosition, position: spritePosition,
current: bloc.state, current: bloc.state,
), ),
DashNestBumperBallContactBehavior(), DashBumperBallContactBehavior(),
...?children, ...?children,
], ],
); );
/// {@macro dash_nest_bumper} /// {@macro dash_bumper}
DashNestBumper.main({ ///
/// [DashBumper.main], usually positioned with a [DashAnimatronic] on top of
/// it.
DashBumper.main({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
majorRadius: 5.1, majorRadius: 5.1,
@ -48,15 +51,18 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.main.active.keyName, activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spritePosition: Vector2(0, -0.3), spritePosition: Vector2(0, -0.3),
bloc: DashNestBumperCubit(), bloc: DashBumperCubit(),
children: [ children: [
...?children, ...?children,
BumpingBehavior(strength: 20), BumpingBehavior(strength: 20),
], ],
); );
/// {@macro dash_nest_bumper} /// {@macro dash_bumper}
DashNestBumper.a({ ///
/// [DashBumper.a] is positioned at the right side of the [DashBumper.main] in
/// the flutter forest.
DashBumper.a({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
majorRadius: 3, majorRadius: 3,
@ -64,15 +70,18 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.a.active.keyName, activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spritePosition: Vector2(0.3, -1.3), spritePosition: Vector2(0.3, -1.3),
bloc: DashNestBumperCubit(), bloc: DashBumperCubit(),
children: [ children: [
...?children, ...?children,
BumpingBehavior(strength: 20), BumpingBehavior(strength: 20),
], ],
); );
/// {@macro dash_nest_bumper} /// {@macro dash_bumper}
DashNestBumper.b({ ///
/// [DashBumper.b] is positioned at the left side of the [DashBumper.main] in
/// the flutter forest.
DashBumper.b({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
majorRadius: 3.1, majorRadius: 3.1,
@ -80,25 +89,26 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.b.active.keyName, activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spritePosition: Vector2(0.4, -1.2), spritePosition: Vector2(0.4, -1.2),
bloc: DashNestBumperCubit(), bloc: DashBumperCubit(),
children: [ children: [
...?children, ...?children,
BumpingBehavior(strength: 20), BumpingBehavior(strength: 20),
], ],
); );
/// Creates an [DashNestBumper] without any children. /// Creates a [DashBumper] without any children.
/// ///
/// This can be used for testing [DashNestBumper]'s behaviors in isolation. /// This can be used for testing [DashBumper]'s behaviors in isolation.
@visibleForTesting @visibleForTesting
DashNestBumper.test({required this.bloc}) DashBumper.test({required this.bloc})
: _majorRadius = 3, : _majorRadius = 3,
_minorRadius = 2.5; _minorRadius = 2.5;
final double _majorRadius; final double _majorRadius;
final double _minorRadius; final double _minorRadius;
final DashNestBumperCubit bloc; // ignore: public_member_api_docs
final DashBumperCubit bloc;
@override @override
void onRemove() { void onRemove() {
@ -121,14 +131,14 @@ class DashNestBumper extends BodyComponent with InitialPosition {
} }
} }
class _DashNestBumperSpriteGroupComponent class _DashBumperSpriteGroupComponent
extends SpriteGroupComponent<DashNestBumperState> extends SpriteGroupComponent<DashBumperState>
with HasGameRef, ParentIsA<DashNestBumper> { with HasGameRef, ParentIsA<DashBumper> {
_DashNestBumperSpriteGroupComponent({ _DashBumperSpriteGroupComponent({
required String activeAssetPath, required String activeAssetPath,
required String inactiveAssetPath, required String inactiveAssetPath,
required Vector2 position, required Vector2 position,
required DashNestBumperState current, required DashBumperState current,
}) : _activeAssetPath = activeAssetPath, }) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath, _inactiveAssetPath = inactiveAssetPath,
super( super(
@ -146,9 +156,9 @@ class _DashNestBumperSpriteGroupComponent
parent.bloc.stream.listen((state) => current = state); parent.bloc.stream.listen((state) => current = state);
final sprites = { final sprites = {
DashNestBumperState.active: DashBumperState.active:
Sprite(gameRef.images.fromCache(_activeAssetPath)), Sprite(gameRef.images.fromCache(_activeAssetPath)),
DashNestBumperState.inactive: DashBumperState.inactive:
Sprite(gameRef.images.fromCache(_inactiveAssetPath)), Sprite(gameRef.images.fromCache(_inactiveAssetPath)),
}; };
this.sprites = sprites; this.sprites = sprites;

@ -1,17 +0,0 @@
import 'package:bloc/bloc.dart';
part 'dash_nest_bumper_state.dart';
class DashNestBumperCubit extends Cubit<DashNestBumperState> {
DashNestBumperCubit() : super(DashNestBumperState.inactive);
/// Event added when the bumper contacts with a ball.
void onBallContacted() {
emit(DashNestBumperState.active);
}
/// Event added when the bumper should return to its initial configuration.
void onReset() {
emit(DashNestBumperState.inactive);
}
}

@ -1,10 +0,0 @@
part of 'dash_nest_bumper_cubit.dart';
/// Indicates the [DashNestBumperCubit]'s current state.
enum DashNestBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}

@ -1,78 +0,0 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/particles.dart';
import 'package:flame_forge2d/flame_forge2d.dart' hide Particle;
import 'package:flutter/material.dart';
const _particleRadius = 0.25;
/// {@template fire_effect}
/// A [BodyComponent] which creates a fire trail effect using the given
/// parameters
/// {@endtemplate}
class FireEffect extends ParticleSystemComponent {
/// {@macro fire_effect}
FireEffect({
required this.burstPower,
required this.direction,
Vector2? position,
}) : super(
position: position,
);
/// A [double] value that will define how "strong" the burst of particles
/// will be.
final double burstPower;
/// Which direction the burst will aim.
final Vector2 direction;
@override
Future<void> onLoad() async {
await super.onLoad();
final children = [
...List.generate(4, (index) {
return CircleParticle(
radius: _particleRadius,
paint: Paint()..color = Colors.yellow.darken((index + 1) / 4),
);
}),
...List.generate(4, (index) {
return CircleParticle(
radius: _particleRadius,
paint: Paint()..color = Colors.red.darken((index + 1) / 4),
);
}),
...List.generate(4, (index) {
return CircleParticle(
radius: _particleRadius,
paint: Paint()..color = Colors.orange.darken((index + 1) / 4),
);
}),
];
final random = math.Random();
final spreadTween = Tween<double>(begin: -0.2, end: 0.2);
particle = Particle.generate(
count: math.max((random.nextDouble() * (burstPower * 10)).toInt(), 1),
generator: (_) {
final spread = Vector2(
spreadTween.transform(random.nextDouble()),
spreadTween.transform(random.nextDouble()),
);
final finalDirection = Vector2(direction.x, direction.y) + spread;
final speed = finalDirection * (burstPower * 20);
return AcceleratedParticle(
lifespan: 5 / burstPower,
position: Vector2.zero(),
speed: speed,
child: children[random.nextInt(children.length)],
);
},
);
}
}

@ -0,0 +1,2 @@
export 'flipper_jointing_behavior.dart';
export 'flipper_key_controlling_behavior.dart';

@ -0,0 +1,62 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Joints the [Flipper] to allow pivoting around one end.
class FlipperJointingBehavior extends Component
with ParentIsA<Flipper>, HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final anchor = _FlipperAnchor(flipper: parent);
await add(anchor);
final jointDef = _FlipperAnchorRevoluteJointDef(
flipper: parent,
anchor: anchor,
);
parent.world.createJoint(RevoluteJoint(jointDef));
}
}
/// {@template flipper_anchor}
/// [JointAnchor] positioned at the end of a [Flipper].
///
/// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate}
class _FlipperAnchor extends JointAnchor {
/// {@macro flipper_anchor}
_FlipperAnchor({
required Flipper flipper,
}) {
initialPosition = Vector2(
(Flipper.size.x * flipper.side.direction) / 2 -
(1.65 * flipper.side.direction),
-0.15,
);
}
}
/// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve a pivoting
/// motion.
/// {@endtemplate}
class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def}
_FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required _FlipperAnchor anchor,
}) {
initialize(
flipper.body,
anchor.body,
flipper.body.position + anchor.body.position,
);
enableLimit = true;
upperAngle = 0.611;
lowerAngle = -upperAngle;
}
}

@ -1,49 +1,33 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template controlled_flipper} /// Allows controlling the [Flipper]'s movement with keyboard input.
/// A [Flipper] with a [FlipperController] attached. class FlipperKeyControllingBehavior extends Component
/// {@endtemplate} with KeyboardHandler, ParentIsA<Flipper> {
class ControlledFlipper extends Flipper with Controls<FlipperController> {
/// {@macro controlled_flipper}
ControlledFlipper({
required BoardSide side,
}) : super(side: side) {
controller = FlipperController(this);
}
}
/// {@template flipper_controller}
/// A [ComponentController] that controls a [Flipper]s movement.
/// {@endtemplate}
class FlipperController extends ComponentController<Flipper>
with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro flipper_controller}
FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys,
super(flipper);
/// The [LogicalKeyboardKey]s that will control the [Flipper]. /// The [LogicalKeyboardKey]s that will control the [Flipper].
/// ///
/// [onKeyEvent] method listens to when one of these keys is pressed. /// [onKeyEvent] method listens to when one of these keys is pressed.
final List<LogicalKeyboardKey> _keys; late final List<LogicalKeyboardKey> _keys;
@override
Future<void> onLoad() async {
await super.onLoad();
_keys = parent.side.flipperKeys;
}
@override @override
bool onKeyEvent( bool onKeyEvent(
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (!bloc.state.status.isPlaying) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {
component.moveUp(); parent.moveUp();
} else if (event is RawKeyUpEvent) { } else if (event is RawKeyUpEvent) {
component.moveDown(); parent.moveDown();
} }
return false; return false;

@ -2,8 +2,11 @@ import 'dart:async';
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/foundation.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
export 'behaviors/behaviors.dart';
/// {@template flipper} /// {@template flipper}
/// A bat, typically found in pairs at the bottom of the board. /// A bat, typically found in pairs at the bottom of the board.
/// ///
@ -15,9 +18,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
required this.side, required this.side,
}) : super( }) : super(
renderBody: false, renderBody: false,
children: [_FlipperSpriteComponent(side: side)], children: [
_FlipperSpriteComponent(side: side),
FlipperJointingBehavior(),
],
); );
/// Creates a [Flipper] without any children.
///
/// This can be used for testing [Flipper]'s behaviors in isolation.
@visibleForTesting
Flipper.test({required this.side});
/// The size of the [Flipper]. /// The size of the [Flipper].
static final size = Vector2(13.5, 4.3); static final size = Vector2(13.5, 4.3);
@ -44,19 +56,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
body.linearVelocity = Vector2(0, -_speed); body.linearVelocity = Vector2(0, -_speed);
} }
/// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion.
Future<void> _anchorToJoint() async {
final anchor = _FlipperAnchor(flipper: this);
await add(anchor);
final jointDef = _FlipperAnchorRevoluteJointDef(
flipper: this,
anchor: anchor,
);
final joint = _FlipperJoint(jointDef);
world.createJoint(joint);
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final direction = side.direction; final direction = side.direction;
@ -73,7 +72,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
assetShadow, assetShadow,
0, 0,
); );
final bigCircleFixtureDef = FixtureDef(bigCircleShape);
final smallCircleShape = CircleShape()..radius = size.y * 0.23; final smallCircleShape = CircleShape()..radius = size.y * 0.23;
smallCircleShape.position.setValues( smallCircleShape.position.setValues(
@ -82,7 +80,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
assetShadow, assetShadow,
0, 0,
); );
final smallCircleFixtureDef = FixtureDef(smallCircleShape);
final trapeziumVertices = side.isLeft final trapeziumVertices = side.isLeft
? [ ? [
@ -98,26 +95,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
Vector2(smallCircleShape.position.x, -smallCircleShape.radius), Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
]; ];
final trapezium = PolygonShape()..set(trapeziumVertices); final trapezium = PolygonShape()..set(trapeziumVertices);
final trapeziumFixtureDef = FixtureDef(
return [
FixtureDef(bigCircleShape),
FixtureDef(smallCircleShape),
FixtureDef(
trapezium, trapezium,
density: 50, density: 50,
friction: .1, friction: .1,
); ),
return [
bigCircleFixtureDef,
smallCircleFixtureDef,
trapeziumFixtureDef,
]; ];
} }
@override
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
}
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef( final bodyDef = BodyDef(
@ -131,15 +120,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
return body; return body;
} }
@override
void onMount() {
super.onMount();
gameRef.ready().whenComplete(
() => body.joints.whereType<_FlipperJoint>().first.unlock(),
);
}
} }
class _FlipperSpriteComponent extends SpriteComponent with HasGameRef { class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
@ -163,73 +143,3 @@ class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
} }
} }
/// {@template flipper_anchor}
/// [JointAnchor] positioned at the end of a [Flipper].
///
/// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate}
class _FlipperAnchor extends JointAnchor {
/// {@macro flipper_anchor}
_FlipperAnchor({
required Flipper flipper,
}) {
initialPosition = Vector2(
(Flipper.size.x * flipper.side.direction) / 2 -
(1.65 * flipper.side.direction),
-0.15,
);
}
}
/// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion.
/// {@endtemplate}
class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def}
_FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required _FlipperAnchor anchor,
}) : side = flipper.side {
enableLimit = true;
initialize(
flipper.body,
anchor.body,
flipper.body.position + anchor.body.position,
);
}
final BoardSide side;
}
/// {@template flipper_joint}
/// [RevoluteJoint] that controls the arc motion of a [Flipper].
/// {@endtemplate}
class _FlipperJoint extends RevoluteJoint {
/// {@macro flipper_joint}
_FlipperJoint(_FlipperAnchorRevoluteJointDef def)
: side = def.side,
super(def) {
lock();
}
/// Half the angle of the arc motion.
static const _halfSweepingAngle = 0.611;
final BoardSide side;
/// Locks the [Flipper] to its resting position.
///
/// The joint is locked when initialized in order to force the [Flipper]
/// at its resting position.
void lock() {
final angle = _halfSweepingAngle * side.direction;
setLimits(angle, angle);
}
/// Unlocks the [Flipper] from its resting position.
void unlock() {
const angle = _halfSweepingAngle;
setLimits(-angle, angle);
}
}

@ -12,7 +12,7 @@ import 'package:pinball_components/pinball_components.dart';
/// initialize( /// initialize(
/// dynamicBody.body, /// dynamicBody.body,
/// anchor.body, /// anchor.body,
/// dynabmicBody.body + anchor.body.position, /// dynamicBody.body + anchor.body.position,
/// ); /// );
/// ``` /// ```
/// {@endtemplate} /// {@endtemplate}

@ -6,7 +6,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template plunger} /// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the /// [Plunger] serves as a spring, that shoots the ball on the right side of the
/// playfield. /// play field.
/// ///
/// [Plunger] ignores gravity so the player controls its downward [pull]. /// [Plunger] ignores gravity so the player controls its downward [pull].
/// {@endtemplate} /// {@endtemplate}

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Scales a [ScoreComponent] according to its position on the board.
class ScoreComponentScalingBehavior extends Component
with ParentIsA<SpriteComponent> {
@override
void update(double dt) {
super.update(dt);
final boardHeight = BoardDimensions.bounds.height;
const maxShrinkValue = 0.83;
final augmentedPosition = parent.position.y * 3;
final standardizedYPosition = augmentedPosition + (boardHeight / 2);
final scaleFactor = maxShrinkValue +
((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
parent.scale.setValues(
scaleFactor,
scaleFactor,
);
}
}

@ -2,7 +2,9 @@ import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/effects.dart'; import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/score_component/behaviors/score_component_scaling_behavior.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
enum Points { enum Points {
@ -26,10 +28,25 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
super( super(
position: position, position: position,
anchor: Anchor.center, anchor: Anchor.center,
children: [ScoreComponentScalingBehavior()],
) { ) {
zIndex = ZIndexes.score; zIndex = ZIndexes.score;
} }
/// Creates a [ScoreComponent] without any children.
///
/// This can be used for testing [ScoreComponent]'s behaviors in isolation.
@visibleForTesting
ScoreComponent.test({
required this.points,
required Vector2 position,
required EffectController effectController,
}) : _effectController = effectController,
super(
position: position,
anchor: Anchor.center,
);
late Points points; late Points points;
late final Effect _effect; late final Effect _effect;

@ -9,7 +9,7 @@ export 'cubit/signpost_cubit.dart';
/// {@template signpost} /// {@template signpost}
/// A sign, found in the Flutter Forest. /// A sign, found in the Flutter Forest.
/// ///
/// Lights up a new sign whenever all three [DashNestBumper]s are hit. /// Lights up a new sign whenever all three [DashBumper]s are hit.
/// {@endtemplate} /// {@endtemplate}
class Signpost extends BodyComponent with InitialPosition { class Signpost extends BodyComponent with InitialPosition {
/// {@macro signpost} /// {@macro signpost}

@ -37,7 +37,7 @@ class Slingshot extends BodyComponent with InitialPosition {
}) : _angle = angle, }) : _angle = angle,
super( super(
children: [ children: [
_SlinghsotSpriteComponent(spritePath, angle: angle), _SlingshotSpriteComponent(spritePath, angle: angle),
BumpingBehavior(strength: 20), BumpingBehavior(strength: 20),
], ],
renderBody: false, renderBody: false,
@ -90,8 +90,8 @@ class Slingshot extends BodyComponent with InitialPosition {
} }
} }
class _SlinghsotSpriteComponent extends SpriteComponent with HasGameRef { class _SlingshotSpriteComponent extends SpriteComponent with HasGameRef {
_SlinghsotSpriteComponent( _SlingshotSpriteComponent(
String path, { String path, {
required double angle, required double angle,
}) : _path = path, }) : _path = path,

@ -9,7 +9,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// the [SpaceshipRamp]. /// the [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class RampBallAscendingContactBehavior class RampBallAscendingContactBehavior
extends ContactBehavior<RampScoringSensor> { extends ContactBehavior<SpaceshipRampBoardOpening> {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);

@ -27,11 +27,6 @@ class SpaceshipRamp extends Component {
required this.bloc, required this.bloc,
}) : super( }) : super(
children: [ children: [
RampScoringSensor(
children: [
RampBallAscendingContactBehavior(),
],
)..initialPosition = Vector2(1.7, -20.4),
_SpaceshipRampOpening( _SpaceshipRampOpening(
outsideLayer: Layer.spaceship, outsideLayer: Layer.spaceship,
outsidePriority: ZIndexes.ballOnSpaceship, outsidePriority: ZIndexes.ballOnSpaceship,
@ -40,9 +35,9 @@ class SpaceshipRamp extends Component {
..initialPosition = Vector2(-13.7, -18.6) ..initialPosition = Vector2(-13.7, -18.6)
..layer = Layer.spaceshipEntranceRamp, ..layer = Layer.spaceshipEntranceRamp,
_SpaceshipRampBackground(), _SpaceshipRampBackground(),
_SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5), SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5),
_SpaceshipRampForegroundRailing(), _SpaceshipRampForegroundRailing(),
_SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5), SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5),
_SpaceshipRampBackgroundRailingSpriteComponent(), _SpaceshipRampBackgroundRailingSpriteComponent(),
SpaceshipRampArrowSpriteComponent( SpaceshipRampArrowSpriteComponent(
current: bloc.state.hits, current: bloc.state.hits,
@ -246,18 +241,24 @@ extension on SpaceshipRampArrowSpriteState {
} }
} }
class _SpaceshipRampBoardOpening extends BodyComponent class SpaceshipRampBoardOpening extends BodyComponent
with Layered, ZIndex, InitialPosition { with Layered, ZIndex, InitialPosition, ParentIsA<SpaceshipRamp> {
_SpaceshipRampBoardOpening() SpaceshipRampBoardOpening()
: super( : super(
renderBody: false, renderBody: false,
children: [ children: [
_SpaceshipRampBoardOpeningSpriteComponent(), _SpaceshipRampBoardOpeningSpriteComponent(),
RampBallAscendingContactBehavior()..applyTo(['inside']),
LayerContactBehavior(layer: Layer.spaceshipEntranceRamp) LayerContactBehavior(layer: Layer.spaceshipEntranceRamp)
..applyTo(['inside']), ..applyTo(['inside']),
LayerContactBehavior(layer: Layer.board)..applyTo(['outside']), LayerContactBehavior(
ZIndexContactBehavior(zIndex: ZIndexes.ballOnBoard) layer: Layer.board,
..applyTo(['outside']), onBegin: false,
)..applyTo(['outside']),
ZIndexContactBehavior(
zIndex: ZIndexes.ballOnBoard,
onBegin: false,
)..applyTo(['outside']),
ZIndexContactBehavior(zIndex: ZIndexes.ballOnSpaceshipRamp) ZIndexContactBehavior(zIndex: ZIndexes.ballOnSpaceshipRamp)
..applyTo(['middle', 'inside']), ..applyTo(['middle', 'inside']),
], ],
@ -266,6 +267,13 @@ class _SpaceshipRampBoardOpening extends BodyComponent
layer = Layer.opening; layer = Layer.opening;
} }
/// Creates a [SpaceshipRampBoardOpening] without any children.
///
/// This can be used for testing [SpaceshipRampBoardOpening]'s behaviors in
/// isolation.
@visibleForTesting
SpaceshipRampBoardOpening.test();
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topEdge = EdgeShape() final topEdge = EdgeShape()
..set( ..set(
@ -426,9 +434,19 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
} }
} }
class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition { @visibleForTesting
_SpaceshipRampBase() : super(renderBody: false) { class SpaceshipRampBase extends BodyComponent
layer = Layer.board; with InitialPosition, ContactCallbacks {
SpaceshipRampBase() : super(renderBody: false);
@override
void preSolve(Object other, Contact contact, Manifold oldManifold) {
super.preSolve(other, contact, oldManifold);
if (other is! Layered) return;
// Although, the Layer should already be taking care of the contact
// filtering, this is to ensure the ball doesn't collide with the ramp base
// when the filtering is calculated on different time steps.
contact.setEnabled(other.layer == Layer.board);
} }
@override @override
@ -441,7 +459,7 @@ class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition {
Vector2(4.1, 1.5), Vector2(4.1, 1.5),
], ],
); );
final bodyDef = BodyDef(position: initialPosition); final bodyDef = BodyDef(position: initialPosition, userData: this);
return world.createBody(bodyDef)..createFixtureFromShape(shape); return world.createBody(bodyDef)..createFixtureFromShape(shape);
} }
} }
@ -480,46 +498,3 @@ class _SpaceshipRampOpening extends LayerSensor {
); );
} }
} }
/// {@template ramp_scoring_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SpaceshipRamp].
/// {@endtemplate}
class RampScoringSensor extends BodyComponent
with ParentIsA<SpaceshipRamp>, InitialPosition, Layered {
/// {@macro ramp_scoring_sensor}
RampScoringSensor({
Iterable<Component>? children,
}) : super(
children: children,
renderBody: false,
) {
layer = Layer.spaceshipEntranceRamp;
}
/// Creates a [RampScoringSensor] without any children.
@visibleForTesting
RampScoringSensor.test();
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
2.6,
.5,
initialPosition,
-5 * math.pi / 180,
);
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -11,7 +11,7 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/sparky_bumper_cubit.dart'; export 'cubit/sparky_bumper_cubit.dart';
/// {@template sparky_bumper} /// {@template sparky_bumper}
/// Bumper for Sparky area. /// Bumper for the Sparky Scorch.
/// {@endtemplate} /// {@endtemplate}
class SparkyBumper extends BodyComponent with InitialPosition, ZIndex { class SparkyBumper extends BodyComponent with InitialPosition, ZIndex {
/// {@macro sparky_bumper} /// {@macro sparky_bumper}

@ -65,7 +65,7 @@ class SparkyComputer extends BodyComponent {
..setAsBox( ..setAsBox(
1, 1,
0.1, 0.1,
Vector2(-13.2, -49.9), Vector2(-13.1, -49.7),
-0.18, -0.18,
); );

@ -106,7 +106,7 @@ abstract class ZIndexes {
// Score // Score
static const score = _above + spaceshipRampForegroundRailing; static const score = _above + sparkyAnimatronic;
// Debug information // Debug information

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

Loading…
Cancel
Save