fix: fixed merge conflicts

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

@ -21,7 +21,7 @@ jobs:
- name: Build Flutter App
run: |
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
uses: FirebaseExtended/action-hosting-deploy@v0

@ -14,3 +14,4 @@ jobs:
flutter_version: 2.10.5
coverage_excludes: "lib/gen/*.dart"
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",
"configurations": [
{
"name": "Launch development",
"name": "Launch pinball",
"request": "launch",
"type": "dart",
"program": "lib/main_development.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"]
"program": "lib/main.dart"
},
{
"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]
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_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
- staging
- production
## Getting Started 🚀
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
# Development
$ 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
$ flutter run -d chrome
```
_\*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
# Generate Coverage Report
$ genhtml coverage/lcov.info -o coverage/
# Open Coverage Report
$ 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
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
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
```
[build_status_badge]: https://github.com/flutter/pinball/actions/workflows/main.yaml/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
[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
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[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_link]: https://pub.dev/packages/very_good_analysis
[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 LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository,
required PinballPlayer pinballPlayer,
required PinballAudioPlayer pinballAudioPlayer,
}) : _authenticationRepository = authenticationRepository,
_leaderboardRepository = leaderboardRepository,
_shareRepository = shareRepository,
_pinballPlayer = pinballPlayer,
_pinballAudioPlayer = pinballAudioPlayer,
super(key: key);
final AuthenticationRepository _authenticationRepository;
final LeaderboardRepository _leaderboardRepository;
final ShareRepository _shareRepository;
final PinballPlayer _pinballPlayer;
final PinballAudioPlayer _pinballAudioPlayer;
@override
Widget build(BuildContext context) {
@ -36,7 +36,7 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _authenticationRepository),
RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _shareRepository),
RepositoryProvider.value(value: _pinballPlayer),
RepositoryProvider.value(value: _pinballAudioPlayer),
],
child: MultiBlocProvider(
providers: [

@ -1,27 +1,39 @@
import 'package:bloc/bloc.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';
/// {@template assets_manager_cubit}
/// Cubit responsable for pre loading any game assets
/// {@endtemplate}
class AssetsManagerCubit extends Cubit<AssetsManagerState> {
/// {@macro assets_manager_cubit}
AssetsManagerCubit(List<Future> loadables)
: super(
AssetsManagerState.initial(
loadables: loadables,
),
);
AssetsManagerCubit(this._game, this._audioPlayer)
: super(const AssetsManagerState.initial());
final PinballGame _game;
final PinballAudioPlayer _audioPlayer;
/// Loads the assets
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 {
await loadable;
emit(state.copyWith(loaded: [...state.loaded, loadable]));
}).toList();
await Future.wait(all);
}
}

@ -11,9 +11,8 @@ class AssetsManagerState extends Equatable {
});
/// {@macro assets_manager_state}
const AssetsManagerState.initial({
required List<Future> loadables,
}) : this(loadables: loadables, loaded: const []);
const AssetsManagerState.initial()
: this(loadables: const [], loaded: const []);
/// List of futures to load
final List<Future> loadables;
@ -22,7 +21,11 @@ class AssetsManagerState extends Equatable {
final List<Future> loaded;
/// 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
/// updated

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

@ -1,5 +1,6 @@
export 'ball_spawning_behavior.dart';
export 'ball_theming_behavior.dart';
export 'bonus_ball_spawning_behavior.dart';
export 'bonus_noise_behavior.dart';
export 'bumper_noise_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) {
final bonus = state.bonusHistory.last;
final audioPlayer = readProvider<PinballPlayer>();
final audioPlayer = readProvider<PinballAudioPlayer>();
switch (bonus) {
case GameBonus.googleWord:
@ -25,10 +25,13 @@ class BonusNoiseBehavior extends Component {
audioPlayer.play(PinballAudio.sparky);
break;
case GameBonus.dinoChomp:
audioPlayer.play(PinballAudio.dino);
break;
case GameBonus.androidSpaceship:
audioPlayer.play(PinballAudio.android);
break;
case GameBonus.dashNest:
audioPlayer.play(PinballAudio.dash);
break;
}
},

@ -6,6 +6,6 @@ class BumperNoiseBehavior extends ContactBehavior {
@override
void beginContact(Object other, Contact 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}
class ScoringBehavior extends Component
with HasGameRef, FlameBlocReader<GameBloc, GameState> {
/// {@macto scoring_behavior}
/// {@macro scoring_behavior}
ScoringBehavior({
required Points points,
required Vector2 position,

@ -12,7 +12,6 @@ class GameBloc extends Bloc<GameEvent, GameState> {
on<Scored>(_onScored);
on<MultiplierIncreased>(_onIncreasedMultiplier);
on<BonusActivated>(_onBonusActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
on<GameOver>(_onGameOver);
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];
}
class SparkyTurboChargeActivated extends GameEvent {
const SparkyTurboChargeActivated();
@override
List<Object?> get props => [];
}
/// {@template multiplier_increased_game_event}
/// Added when a multiplier is gained.
/// {@endtemplate}

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

@ -1,6 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
@ -15,42 +16,43 @@ class AndroidAcres extends Component {
AndroidAcres()
: super(
children: [
SpaceshipRamp(
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>(
create: AndroidSpaceshipCubit.new,
children: [
RampShotBehavior(
points: Points.fiveThousand,
),
RampBonusBehavior(
points: Points.oneMillion,
SpaceshipRamp(
children: [
RampShotBehavior(points: Points.fiveThousand),
RampBonusBehavior(points: Points.oneMillion),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(),
],
);

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

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

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

@ -54,16 +54,37 @@ class InitialsFormState extends BackboxState {
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 {
/// {@macro initials_success_state}
const InitialsSuccessState({
required this.score,
}) : super();
/// Player's score.
final int score;
@override
List<Object?> get props => [];
List<Object?> get props => [score];
}
/// State when the initials submission failed.
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
List<Object?> get props => [];
List<Object?> get props => [score, character];
}
/// {@template share_state}
@ -73,19 +94,11 @@ class ShareState extends BackboxState {
/// {@macro share_state}
const ShareState({
required this.score,
required this.initials,
required this.character,
}) : super();
/// Player's score.
final int score;
/// Player's initials.
final String initials;
/// Player's character.
final CharacterTheme character;
@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_submission_failure_display.dart';
export 'initials_submission_success_display.dart';
export 'leaderboard_display.dart';
export 'leaderboard_failure_display.dart';
export 'loading_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
.whereType<InitialsLetterPrompt>()
.map((prompt) => prompt.char)

@ -1,27 +1,47 @@
import 'package:flame/components.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';
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 3,
fontSize: 1.8,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
fontWeight: FontWeight.w400,
),
);
/// {@template initials_submission_failure_display}
/// [Backbox] display for when a failure occurs during initials submission.
/// {@endtemplate}
class InitialsSubmissionFailureDisplay extends TextComponent {
class InitialsSubmissionFailureDisplay extends Component {
/// {@macro initials_submission_failure_display}
InitialsSubmissionFailureDisplay({
required this.onDismissed,
});
final VoidCallback onDismissed;
@override
Future<void> onLoad() async {
await super.onLoad();
position = Vector2(0, -10);
anchor = Anchor.center;
text = 'Failure!';
textRenderer = _bodyTextPaint;
final l10n = readProvider<AppLocalizations>();
await addAll([
ErrorComponent.bold(
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:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -40,7 +39,7 @@ class _BottomGroupSide extends Component {
final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? -0.45 : -6.8;
final flipper = ControlledFlipper(
final flipper = Flipper(
side: _side,
)..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side)

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

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

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

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

@ -3,6 +3,7 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Listens to the [GameBloc] and updates the game accordingly.
@ -19,18 +20,39 @@ class GameBlocStatusListener extends Component
case GameStatus.waiting:
break;
case GameStatus.playing:
readProvider<PinballPlayer>().play(PinballAudio.backgroundMusic);
readProvider<PinballAudioPlayer>().play(PinballAudio.backgroundMusic);
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_addFlipperKeyControls);
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
case GameStatus.gameOver:
readProvider<PinballPlayer>().play(PinballAudio.gameOverVoiceOver);
readProvider<PinballAudioPlayer>().play(PinballAudio.gameOverVoiceOver);
gameRef.descendants().whereType<Backbox>().first.requestInitials(
score: state.displayScore,
character: readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme,
);
gameRef
.descendants()
.whereType<Flipper>()
.forEach(_removeFlipperKeyControls);
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_components/pinball_components.dart' as components;
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.
extension PinballGameAssetsX on PinballGame {
/// Returns a list of assets to be loaded
List<Future> preLoadAssets() {
List<Future<Image>> preLoadAssets() {
const dashTheme = DashTheme();
const sparkyTheme = SparkyTheme();
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.button.facebook.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.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter2.lit.keyName),

@ -24,10 +24,10 @@ class PinballGame extends PinballForge2DGame
required this.shareRepository,
required GameBloc gameBloc,
required AppLocalizations l10n,
required PinballPlayer player,
required PinballAudioPlayer audioPlayer,
}) : focusNode = FocusNode(),
_gameBloc = gameBloc,
_player = player,
_audioPlayer = audioPlayer,
_characterThemeBloc = characterThemeBloc,
_l10n = l10n,
super(
@ -39,6 +39,9 @@ class PinballGame extends PinballForge2DGame
/// Identifier of the play button overlay
static const playButtonOverlay = 'play_button';
/// Identifier of the mobile controls overlay
static const mobileControlsOverlay = 'mobile_controls';
@override
Color backgroundColor() => Colors.transparent;
@ -46,7 +49,7 @@ class PinballGame extends PinballForge2DGame
final CharacterThemeCubit _characterThemeBloc;
final PinballPlayer _player;
final PinballAudioPlayer _audioPlayer;
final LeaderboardRepository leaderboardRepository;
@ -56,6 +59,18 @@ class PinballGame extends PinballForge2DGame
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
Future<void> onLoad() async {
await add(
@ -71,7 +86,7 @@ class PinballGame extends PinballForge2DGame
children: [
MultiFlameProvider(
providers: [
FlameProvider<PinballPlayer>.value(_player),
FlameProvider<PinballAudioPlayer>.value(_audioPlayer),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<ShareRepository>.value(shareRepository),
FlameProvider<AppLocalizations>.value(_l10n),
@ -96,6 +111,7 @@ class PinballGame extends PinballForge2DGame
Backbox(
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: _entries,
),
GoogleWord(position: Vector2(-4.45, 1.8)),
Multipliers(),
@ -133,7 +149,7 @@ class PinballGame extends PinballForge2DGame
final rocket = descendants().whereType<RocketSpriteComponent>().first;
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())) {
descendants().whereType<Plunger>().single.pullFor(2);
} else {
@ -179,11 +195,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository,
required AppLocalizations l10n,
required PinballPlayer player,
required PinballAudioPlayer audioPlayer,
required GameBloc gameBloc,
}) : super(
characterThemeBloc: characterThemeBloc,
player: player,
audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
l10n: l10n,

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

@ -15,6 +15,8 @@ class $AssetsImagesGen {
$AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen();
$AssetsImagesLoadingGameGen get loadingGame =>
const $AssetsImagesLoadingGameGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
}
@ -62,6 +64,14 @@ class $AssetsImagesLinkBoxGen {
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 {
const $AssetsImagesScoreGen();

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

@ -148,9 +148,57 @@
"@loading": {
"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": {
"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",

@ -14,7 +14,7 @@ void main() {
const shareRepository =
ShareRepository(appUrl: ShareRepository.googleIOEvent);
final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballPlayer = PinballPlayer();
final pinballAudioPlayer = PinballAudioPlayer();
unawaited(
Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(),
@ -24,7 +24,7 @@ void main() {
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository,
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
Widget build(BuildContext context) {
final l10n = context.l10n;
final indent = MediaQuery.of(context).size.width / 5;
return Column(
children: [
Text(
@ -68,11 +66,9 @@ class _LinkBoxHeader extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Divider(
color: PinballColors.white,
endIndent: indent,
indent: indent,
thickness: 2,
const SizedBox(
width: 200,
child: Divider(color: PinballColors.white, thickness: 2),
),
],
);

@ -11,6 +11,7 @@ class StartGameBloc extends Bloc<StartGameEvent, StartGameState> {
/// {@macro start_game_bloc}
StartGameBloc() : super(const StartGameState.initial()) {
on<PlayTapped>(_onPlayTapped);
on<ReplayTapped>(_onReplayTapped);
on<CharacterSelected>(_onCharacterSelected);
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(
CharacterSelected event,
Emitter<StartGameState> emit,

@ -19,6 +19,17 @@ class PlayTapped extends StartGameEvent {
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}
/// Character selected event.
/// {@endtemplate}

@ -14,8 +14,11 @@ class $AssetsMusicGen {
class $AssetsSfxGen {
const $AssetsSfxGen();
String get android => 'assets/sfx/android.mp3';
String get bumperA => 'assets/sfx/bumper_a.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 google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';

@ -28,6 +28,15 @@ enum PinballAudio {
/// Sparky
sparky,
/// Android
android,
/// Dino
dino,
/// Dash
dash,
}
/// 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
/// {@endtemplate}
class PinballPlayer {
/// {@macro pinball_player}
PinballPlayer({
class PinballAudioPlayer {
/// {@macro pinball_audio_player}
PinballAudioPlayer({
CreateAudioPool? createAudioPool,
PlaySingleAudio? playSingleAudio,
LoopSingleAudio? loopSingleAudio,
@ -169,6 +178,21 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio,
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(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
@ -220,7 +244,7 @@ class PinballPlayer {
return audios.values.map((a) => a.load()).toList();
}
/// Plays the received auido
/// Plays the received audio
void play(PinballAudio audio) {
assert(
audios.containsKey(audio),

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

@ -11,10 +11,8 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_spaceship_cubit.dart';
class AndroidSpaceship extends Component {
AndroidSpaceship({
required Vector2 position,
}) : bloc = AndroidSpaceshipCubit(),
super(
AndroidSpaceship({required Vector2 position})
: super(
children: [
_SpaceshipSaucer()..initialPosition = position,
_SpaceshipSaucerSpriteAnimationComponent()..position = position,
@ -38,17 +36,8 @@ class AndroidSpaceship extends Component {
/// This can be used for testing [AndroidSpaceship]'s behaviors in isolation.
@visibleForTesting
AndroidSpaceship.test({
required this.bloc,
Iterable<Component>? children,
}) : super(children: children);
final AndroidSpaceshipCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
}
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:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class AndroidSpaceshipEntranceBallContactBehavior
extends ContactBehavior<AndroidSpaceshipEntrance> {
extends ContactBehavior<AndroidSpaceshipEntrance>
with FlameBlocReader<AndroidSpaceshipCubit, AndroidSpaceshipState> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
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);
}
/// 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
/// 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_impulsing_behavior.dart';
export 'ball_scaling_behavior.dart';
export 'ball_turbo_charging_behavior.dart';

@ -12,7 +12,7 @@ class BumpingBehavior extends ContactBehavior {
/// Determines how strong the bump is.
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.
@visibleForTesting
final WorldManifold worldManifold = WorldManifold();

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

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

@ -2,8 +2,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class DashNestBumperBallContactBehavior
extends ContactBehavior<DashNestBumper> {
class DashBumperBallContactBehavior extends ContactBehavior<DashBumper> {
@override
void beginContact(Object other, Contact 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:pinball_components/pinball_components.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';
export 'cubit/dash_nest_bumper_cubit.dart';
export 'cubit/dash_bumper_cubit.dart';
/// {@template dash_nest_bumper}
/// Bumper with a nest appearance.
/// {@template dash_bumper}
/// Bumper for the flutter forest.
/// {@endtemplate}
class DashNestBumper extends BodyComponent with InitialPosition {
/// {@macro dash_nest_bumper}
DashNestBumper._({
class DashBumper extends BodyComponent with InitialPosition {
/// {@macro dash_bumper}
DashBumper._({
required double majorRadius,
required double minorRadius,
required String activeAssetPath,
@ -28,19 +28,22 @@ class DashNestBumper extends BodyComponent with InitialPosition {
super(
renderBody: false,
children: [
_DashNestBumperSpriteGroupComponent(
_DashBumperSpriteGroupComponent(
activeAssetPath: activeAssetPath,
inactiveAssetPath: inactiveAssetPath,
position: spritePosition,
current: bloc.state,
),
DashNestBumperBallContactBehavior(),
DashBumperBallContactBehavior(),
...?children,
],
);
/// {@macro dash_nest_bumper}
DashNestBumper.main({
/// {@macro dash_bumper}
///
/// [DashBumper.main], usually positioned with a [DashAnimatronic] on top of
/// it.
DashBumper.main({
Iterable<Component>? children,
}) : this._(
majorRadius: 5.1,
@ -48,15 +51,18 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.main.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.main.inactive.keyName,
spritePosition: Vector2(0, -0.3),
bloc: DashNestBumperCubit(),
bloc: DashBumperCubit(),
children: [
...?children,
BumpingBehavior(strength: 20),
],
);
/// {@macro dash_nest_bumper}
DashNestBumper.a({
/// {@macro dash_bumper}
///
/// [DashBumper.a] is positioned at the right side of the [DashBumper.main] in
/// the flutter forest.
DashBumper.a({
Iterable<Component>? children,
}) : this._(
majorRadius: 3,
@ -64,15 +70,18 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spritePosition: Vector2(0.3, -1.3),
bloc: DashNestBumperCubit(),
bloc: DashBumperCubit(),
children: [
...?children,
BumpingBehavior(strength: 20),
],
);
/// {@macro dash_nest_bumper}
DashNestBumper.b({
/// {@macro dash_bumper}
///
/// [DashBumper.b] is positioned at the left side of the [DashBumper.main] in
/// the flutter forest.
DashBumper.b({
Iterable<Component>? children,
}) : this._(
majorRadius: 3.1,
@ -80,25 +89,26 @@ class DashNestBumper extends BodyComponent with InitialPosition {
activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spritePosition: Vector2(0.4, -1.2),
bloc: DashNestBumperCubit(),
bloc: DashBumperCubit(),
children: [
...?children,
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
DashNestBumper.test({required this.bloc})
DashBumper.test({required this.bloc})
: _majorRadius = 3,
_minorRadius = 2.5;
final double _majorRadius;
final double _minorRadius;
final DashNestBumperCubit bloc;
// ignore: public_member_api_docs
final DashBumperCubit bloc;
@override
void onRemove() {
@ -121,14 +131,14 @@ class DashNestBumper extends BodyComponent with InitialPosition {
}
}
class _DashNestBumperSpriteGroupComponent
extends SpriteGroupComponent<DashNestBumperState>
with HasGameRef, ParentIsA<DashNestBumper> {
_DashNestBumperSpriteGroupComponent({
class _DashBumperSpriteGroupComponent
extends SpriteGroupComponent<DashBumperState>
with HasGameRef, ParentIsA<DashBumper> {
_DashBumperSpriteGroupComponent({
required String activeAssetPath,
required String inactiveAssetPath,
required Vector2 position,
required DashNestBumperState current,
required DashBumperState current,
}) : _activeAssetPath = activeAssetPath,
_inactiveAssetPath = inactiveAssetPath,
super(
@ -146,9 +156,9 @@ class _DashNestBumperSpriteGroupComponent
parent.bloc.stream.listen((state) => current = state);
final sprites = {
DashNestBumperState.active:
DashBumperState.active:
Sprite(gameRef.images.fromCache(_activeAssetPath)),
DashNestBumperState.inactive:
DashBumperState.inactive:
Sprite(gameRef.images.fromCache(_inactiveAssetPath)),
};
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_bloc/flame_bloc.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template controlled_flipper}
/// A [Flipper] with a [FlipperController] attached.
/// {@endtemplate}
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);
/// Allows controlling the [Flipper]'s movement with keyboard input.
class FlipperKeyControllingBehavior extends Component
with KeyboardHandler, ParentIsA<Flipper> {
/// The [LogicalKeyboardKey]s that will control the [Flipper].
///
/// [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
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (!bloc.state.status.isPlaying) return true;
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
component.moveUp();
parent.moveUp();
} else if (event is RawKeyUpEvent) {
component.moveDown();
parent.moveDown();
}
return false;

@ -2,8 +2,11 @@ import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'package:pinball_components/pinball_components.dart';
export 'behaviors/behaviors.dart';
/// {@template flipper}
/// 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,
}) : super(
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].
static final size = Vector2(13.5, 4.3);
@ -44,19 +56,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
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() {
final direction = side.direction;
@ -73,7 +72,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
assetShadow,
0,
);
final bigCircleFixtureDef = FixtureDef(bigCircleShape);
final smallCircleShape = CircleShape()..radius = size.y * 0.23;
smallCircleShape.position.setValues(
@ -82,7 +80,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
assetShadow,
0,
);
final smallCircleFixtureDef = FixtureDef(smallCircleShape);
final trapeziumVertices = side.isLeft
? [
@ -98,26 +95,18 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
];
final trapezium = PolygonShape()..set(trapeziumVertices);
final trapeziumFixtureDef = FixtureDef(
trapezium,
density: 50,
friction: .1,
);
return [
bigCircleFixtureDef,
smallCircleFixtureDef,
trapeziumFixtureDef,
FixtureDef(bigCircleShape),
FixtureDef(smallCircleShape),
FixtureDef(
trapezium,
density: 50,
friction: .1,
),
];
}
@override
Future<void> onLoad() async {
await super.onLoad();
await _anchorToJoint();
}
@override
Body createBody() {
final bodyDef = BodyDef(
@ -131,15 +120,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
return body;
}
@override
void onMount() {
super.onMount();
gameRef.ready().whenComplete(
() => body.joints.whereType<_FlipperJoint>().first.unlock(),
);
}
}
class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
@ -163,73 +143,3 @@ class _FlipperSpriteComponent extends SpriteComponent with HasGameRef {
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(
/// dynamicBody.body,
/// anchor.body,
/// dynabmicBody.body + anchor.body.position,
/// dynamicBody.body + anchor.body.position,
/// );
/// ```
/// {@endtemplate}

@ -6,7 +6,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// {@template plunger}
/// [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].
/// {@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/effects.dart';
import 'package:flutter/material.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';
enum Points {
@ -26,10 +28,25 @@ class ScoreComponent extends SpriteComponent with HasGameRef, ZIndex {
super(
position: position,
anchor: Anchor.center,
children: [ScoreComponentScalingBehavior()],
) {
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 final Effect _effect;

@ -9,7 +9,7 @@ export 'cubit/signpost_cubit.dart';
/// {@template signpost}
/// 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}
class Signpost extends BodyComponent with InitialPosition {
/// {@macro signpost}

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

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

@ -27,11 +27,6 @@ class SpaceshipRamp extends Component {
required this.bloc,
}) : super(
children: [
RampScoringSensor(
children: [
RampBallAscendingContactBehavior(),
],
)..initialPosition = Vector2(1.7, -20.4),
_SpaceshipRampOpening(
outsideLayer: Layer.spaceship,
outsidePriority: ZIndexes.ballOnSpaceship,
@ -40,9 +35,9 @@ class SpaceshipRamp extends Component {
..initialPosition = Vector2(-13.7, -18.6)
..layer = Layer.spaceshipEntranceRamp,
_SpaceshipRampBackground(),
_SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5),
SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5),
_SpaceshipRampForegroundRailing(),
_SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5),
SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5),
_SpaceshipRampBackgroundRailingSpriteComponent(),
SpaceshipRampArrowSpriteComponent(
current: bloc.state.hits,
@ -246,18 +241,24 @@ extension on SpaceshipRampArrowSpriteState {
}
}
class _SpaceshipRampBoardOpening extends BodyComponent
with Layered, ZIndex, InitialPosition {
_SpaceshipRampBoardOpening()
class SpaceshipRampBoardOpening extends BodyComponent
with Layered, ZIndex, InitialPosition, ParentIsA<SpaceshipRamp> {
SpaceshipRampBoardOpening()
: super(
renderBody: false,
children: [
_SpaceshipRampBoardOpeningSpriteComponent(),
RampBallAscendingContactBehavior()..applyTo(['inside']),
LayerContactBehavior(layer: Layer.spaceshipEntranceRamp)
..applyTo(['inside']),
LayerContactBehavior(layer: Layer.board)..applyTo(['outside']),
ZIndexContactBehavior(zIndex: ZIndexes.ballOnBoard)
..applyTo(['outside']),
LayerContactBehavior(
layer: Layer.board,
onBegin: false,
)..applyTo(['outside']),
ZIndexContactBehavior(
zIndex: ZIndexes.ballOnBoard,
onBegin: false,
)..applyTo(['outside']),
ZIndexContactBehavior(zIndex: ZIndexes.ballOnSpaceshipRamp)
..applyTo(['middle', 'inside']),
],
@ -266,6 +267,13 @@ class _SpaceshipRampBoardOpening extends BodyComponent
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() {
final topEdge = EdgeShape()
..set(
@ -426,9 +434,19 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
}
}
class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition {
_SpaceshipRampBase() : super(renderBody: false) {
layer = Layer.board;
@visibleForTesting
class SpaceshipRampBase extends BodyComponent
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
@ -441,7 +459,7 @@ class _SpaceshipRampBase extends BodyComponent with Layered, InitialPosition {
Vector2(4.1, 1.5),
],
);
final bodyDef = BodyDef(position: initialPosition);
final bodyDef = BodyDef(position: initialPosition, userData: this);
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';
/// {@template sparky_bumper}
/// Bumper for Sparky area.
/// Bumper for the Sparky Scorch.
/// {@endtemplate}
class SparkyBumper extends BodyComponent with InitialPosition, ZIndex {
/// {@macro sparky_bumper}

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

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

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

Loading…
Cancel
Save