Merge branch 'main' into release

release
Tom Arra 3 years ago
commit 5be7845286

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

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

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

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

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

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

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

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

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

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

@ -2,3 +2,6 @@ include: package:very_good_analysis/analysis_options.2.4.0.yaml
analyzer: analyzer:
exclude: exclude:
- lib/**/*.gen.dart - lib/**/*.gen.dart
linter:
rules:
public_member_api_docs: false

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:authentication_repository/authentication_repository.dart'; import 'package:authentication_repository/authentication_repository.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -11,21 +9,25 @@ import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
import 'package:share_repository/share_repository.dart';
class App extends StatelessWidget { class App extends StatelessWidget {
const App({ const App({
Key? key, Key? key,
required AuthenticationRepository authenticationRepository, required AuthenticationRepository authenticationRepository,
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required PinballPlayer pinballPlayer, required ShareRepository shareRepository,
required PinballAudioPlayer pinballAudioPlayer,
}) : _authenticationRepository = authenticationRepository, }) : _authenticationRepository = authenticationRepository,
_leaderboardRepository = leaderboardRepository, _leaderboardRepository = leaderboardRepository,
_pinballPlayer = pinballPlayer, _shareRepository = shareRepository,
_pinballAudioPlayer = pinballAudioPlayer,
super(key: key); super(key: key);
final AuthenticationRepository _authenticationRepository; final AuthenticationRepository _authenticationRepository;
final LeaderboardRepository _leaderboardRepository; final LeaderboardRepository _leaderboardRepository;
final PinballPlayer _pinballPlayer; final ShareRepository _shareRepository;
final PinballAudioPlayer _pinballAudioPlayer;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -33,7 +35,8 @@ class App extends StatelessWidget {
providers: [ providers: [
RepositoryProvider.value(value: _authenticationRepository), RepositoryProvider.value(value: _authenticationRepository),
RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _pinballPlayer), RepositoryProvider.value(value: _shareRepository),
RepositoryProvider.value(value: _pinballAudioPlayer),
], ],
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [

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

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

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -16,30 +17,32 @@ class AssetsLoadingPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
final headline1 = Theme.of(context).textTheme.headline1; final headline1 = Theme.of(context).textTheme.headline1;
return Center( return Container(
child: Column( decoration: const CrtBackground(),
mainAxisSize: MainAxisSize.min, child: Center(
children: [ child: Column(
Text( mainAxisSize: MainAxisSize.min,
l10n.ioPinball, children: [
style: headline1!.copyWith(fontSize: 80), Padding(
textAlign: TextAlign.center, padding: const EdgeInsets.symmetric(horizontal: 20),
), child: Assets.images.loadingGame.ioPinball.image(),
const SizedBox(height: 40),
AnimatedEllipsisText(
l10n.loading,
style: headline1,
),
const SizedBox(height: 40),
FractionallySizedBox(
widthFactor: 0.8,
child: BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
builder: (context, state) {
return PinballLoadingIndicator(value: state.progress);
},
), ),
), const SizedBox(height: 40),
], AnimatedEllipsisText(
l10n.loading,
style: headline1,
),
const SizedBox(height: 40),
FractionallySizedBox(
widthFactor: 0.8,
child: BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
builder: (context, state) {
return PinballLoadingIndicator(value: state.progress);
},
),
),
],
),
), ),
); );
} }

@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';

@ -1,9 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// Spawns a new [Ball] into the game when all balls are lost and still /// Spawns a new [Ball] into the game when all balls are lost and still
/// [GameStatus.playing]. /// [GameStatus.playing].
@ -23,7 +23,9 @@ class BallSpawningBehavior extends Component
void onNewState(GameState state) { void onNewState(GameState state) {
final plunger = gameRef.descendants().whereType<Plunger>().single; final plunger = gameRef.descendants().whereType<Plunger>().single;
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single; final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
final characterTheme = readProvider<CharacterTheme>(); final characterTheme = readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme;
final ball = Ball(assetPath: characterTheme.ball.keyName) final ball = Ball(assetPath: characterTheme.ball.keyName)
..initialPosition = Vector2( ..initialPosition = Vector2(
plunger.body.position.x, plunger.body.position.x,

@ -1,5 +1,9 @@
export 'ball_spawning_behavior.dart'; export 'ball_spawning_behavior.dart';
export 'bonus_ball_spawning_behavior.dart';
export 'bonus_noise_behavior.dart'; export 'bonus_noise_behavior.dart';
export 'bumper_noise_behavior.dart'; export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart'; export 'camera_focusing_behavior.dart';
export 'character_selection_behavior.dart';
export 'cow_bumper_noise_behavior.dart';
export 'kicker_noise_behavior.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';

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

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

@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -8,6 +6,6 @@ class BumperNoiseBehavior extends ContactBehavior {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);
readProvider<PinballPlayer>().play(PinballAudio.bumper); readProvider<PinballAudioPlayer>().play(PinballAudio.bumper);
} }
} }

@ -0,0 +1,27 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart';
/// Updates the [ArcadeBackground] and launch [Ball] to reflect character
/// selections.
class CharacterSelectionBehavior extends Component
with
FlameBlocListenable<CharacterThemeCubit, CharacterThemeState>,
HasGameRef {
@override
void onNewState(CharacterThemeState state) {
gameRef
.descendants()
.whereType<ArcadeBackground>()
.single
.bloc
.onCharacterSelected(state.characterTheme);
gameRef
.descendants()
.whereType<Ball>()
.single
.bloc
.onCharacterSelected(state.characterTheme);
}
}

@ -0,0 +1,13 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
class CowBumperNoiseBehavior extends ContactBehavior {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
readProvider<PinballAudioPlayer>().play(PinballAudio.cowMoo);
}
}

@ -0,0 +1,11 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
class KickerNoiseBehavior extends ContactBehavior {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
readProvider<PinballAudioPlayer>().play(PinballAudio.kicker);
}
}

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

@ -1,4 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:math' as math; import 'dart:math' as math;
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
@ -13,11 +12,12 @@ class GameBloc extends Bloc<GameEvent, GameState> {
on<Scored>(_onScored); on<Scored>(_onScored);
on<MultiplierIncreased>(_onIncreasedMultiplier); on<MultiplierIncreased>(_onIncreasedMultiplier);
on<BonusActivated>(_onBonusActivated); on<BonusActivated>(_onBonusActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
on<GameOver>(_onGameOver); on<GameOver>(_onGameOver);
on<GameStarted>(_onGameStarted); on<GameStarted>(_onGameStarted);
} }
static const _maxScore = 9999999999;
void _onGameStarted(GameStarted _, Emitter emit) { void _onGameStarted(GameStarted _, Emitter emit) {
emit(state.copyWith(status: GameStatus.playing)); emit(state.copyWith(status: GameStatus.playing));
} }
@ -27,7 +27,10 @@ class GameBloc extends Bloc<GameEvent, GameState> {
} }
void _onRoundLost(RoundLost event, Emitter emit) { void _onRoundLost(RoundLost event, Emitter emit) {
final score = state.totalScore + state.roundScore * state.multiplier; final score = math.min(
state.totalScore + state.roundScore * state.multiplier,
_maxScore,
);
final roundsLeft = math.max(state.rounds - 1, 0); final roundsLeft = math.max(state.rounds - 1, 0);
emit( emit(
@ -43,9 +46,11 @@ class GameBloc extends Bloc<GameEvent, GameState> {
void _onScored(Scored event, Emitter emit) { void _onScored(Scored event, Emitter emit) {
if (state.status.isPlaying) { if (state.status.isPlaying) {
emit( final combinedScore = math.min(
state.copyWith(roundScore: state.roundScore + event.points), state.totalScore + state.roundScore + event.points,
_maxScore,
); );
emit(state.copyWith(roundScore: combinedScore - state.totalScore));
} }
} }
@ -66,18 +71,4 @@ class GameBloc extends Bloc<GameEvent, GameState> {
), ),
); );
} }
Future<void> _onSparkyTurboChargeActivated(
SparkyTurboChargeActivated event,
Emitter emit,
) async {
emit(
state.copyWith(
bonusHistory: [
...state.bonusHistory,
GameBonus.sparkyTurboCharge,
],
),
);
}
} }

@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
part of 'game_bloc.dart'; part of 'game_bloc.dart';
@immutable @immutable
@ -42,13 +40,6 @@ class BonusActivated extends GameEvent {
List<Object?> get props => [bonus]; List<Object?> get props => [bonus];
} }
class SparkyTurboChargeActivated extends GameEvent {
const SparkyTurboChargeActivated();
@override
List<Object?> get props => [];
}
/// {@template multiplier_increased_game_event} /// {@template multiplier_increased_game_event}
/// Added when a multiplier is gained. /// Added when a multiplier is gained.
/// {@endtemplate} /// {@endtemplate}

@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
part of 'game_bloc.dart'; part of 'game_bloc.dart';
/// Defines bonuses that a player can gain during a PinballGame. /// Defines bonuses that a player can gain during a PinballGame.
@ -7,7 +5,7 @@ enum GameBonus {
/// Bonus achieved when the ball activates all Google letters. /// Bonus achieved when the ball activates all Google letters.
googleWord, googleWord,
/// Bonus achieved when the user activates all dash nest bumpers. /// Bonus achieved when the user activates all dash bumpers.
dashNest, dashNest,
/// Bonus achieved when a ball enters Sparky's computer. /// Bonus achieved when a ball enters Sparky's computer.

@ -1,6 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
@ -15,42 +16,44 @@ class AndroidAcres extends Component {
AndroidAcres() AndroidAcres()
: super( : super(
children: [ children: [
SpaceshipRamp( FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>(
create: AndroidSpaceshipCubit.new,
children: [ children: [
RampShotBehavior( SpaceshipRamp(
points: Points.fiveThousand, children: [
), RampShotBehavior(points: Points.fiveThousand),
RampBonusBehavior( RampBonusBehavior(points: Points.oneMillion),
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(),
CowBumperNoiseBehavior(),
],
)..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,22 +5,21 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus. /// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus.
class AndroidSpaceshipBonusBehavior extends Component class AndroidSpaceshipBonusBehavior extends Component {
with ParentIsA<AndroidAcres>, FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { Future<void> onLoad() async {
super.onMount(); await super.onLoad();
final androidSpaceship = parent.firstChild<AndroidSpaceship>()!; await add(
FlameBlocListener<AndroidSpaceshipCubit, AndroidSpaceshipState>(
// TODO(alestiago): Refactor subscription management once the following is listenWhen: (_, state) => state == AndroidSpaceshipState.withBonus,
// merged: onNewState: (state) {
// https://github.com/flame-engine/flame/pull/1538 readBloc<GameBloc, GameState>().add(
androidSpaceship.bloc.stream.listen((state) { const BonusActivated(GameBonus.androidSpaceship),
final listenWhen = state == AndroidSpaceshipState.withBonus; );
if (!listenWhen) return; readBloc<AndroidSpaceshipCubit, AndroidSpaceshipState>()
.onBonusAwarded();
bloc.add(const BonusActivated(GameBonus.androidSpaceship)); },
androidSpaceship.bloc.onBonusAwarded(); ),
}); );
} }
} }

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

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

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

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

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

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

@ -32,7 +32,6 @@ final _subtitleTextPaint = TextPaint(
/// {@template initials_input_display} /// {@template initials_input_display}
/// Display that handles the user input on the game over view. /// Display that handles the user input on the game over view.
/// {@endtemplate} /// {@endtemplate}
// TODO(allisonryan0002): add mobile input buttons.
class InitialsInputDisplay extends Component with HasGameRef { class InitialsInputDisplay extends Component with HasGameRef {
/// {@macro initials_input_display} /// {@macro initials_input_display}
InitialsInputDisplay({ InitialsInputDisplay({
@ -78,7 +77,7 @@ class InitialsInputDisplay extends Component with HasGameRef {
); );
} }
/// Returns the current inputed initials /// Returns the current entered initials
String get initials => children String get initials => children
.whereType<InitialsLetterPrompt>() .whereType<InitialsLetterPrompt>()
.map((prompt) => prompt.char) .map((prompt) => prompt.char)

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

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

@ -0,0 +1,189 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.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
/// on the [ShareDisplay].
typedef OnSocialShareTap = void Function(SharePlatform);
final _descriptionTextPaint = TextPaint(
style: const TextStyle(
fontSize: 1.6,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template share_display}
/// Display that allows users to share their score to social networks.
/// {@endtemplate}
class ShareDisplay extends Component with HasGameRef {
/// {@macro share_display}
ShareDisplay({
OnSocialShareTap? onShare,
}) : super(
children: [
_ShareInstructionsComponent(
onShare: onShare,
),
],
);
}
class _ShareInstructionsComponent extends PositionComponent with HasGameRef {
_ShareInstructionsComponent({
OnSocialShareTap? onShare,
}) : super(
anchor: Anchor.center,
position: Vector2(0, -25),
children: [
_DescriptionComponent(),
_SocialNetworksComponent(
onShare: onShare,
),
],
);
}
class _DescriptionComponent extends PositionComponent with HasGameRef {
_DescriptionComponent()
: super(
anchor: Anchor.center,
position: Vector2.zero(),
children: [
_LetEveryoneTextComponent(),
_SharingYourScoreTextComponent(),
_SocialMediaTextComponent(),
],
);
}
class _LetEveryoneTextComponent extends TextComponent with HasGameRef {
_LetEveryoneTextComponent()
: super(
anchor: Anchor.center,
position: Vector2.zero(),
textRenderer: _descriptionTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().letEveryone;
}
}
class _SharingYourScoreTextComponent extends TextComponent with HasGameRef {
_SharingYourScoreTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 2.5),
textRenderer: _descriptionTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().bySharingYourScore;
}
}
class _SocialMediaTextComponent extends TextComponent with HasGameRef {
_SocialMediaTextComponent()
: super(
anchor: Anchor.center,
position: Vector2(0, 5),
textRenderer: _descriptionTextPaint,
);
@override
Future<void> onLoad() async {
await super.onLoad();
text = readProvider<AppLocalizations>().socialMediaAccount;
}
}
class _SocialNetworksComponent extends PositionComponent with HasGameRef {
_SocialNetworksComponent({
OnSocialShareTap? onShare,
}) : super(
anchor: Anchor.center,
position: Vector2(0, 12),
children: [
FacebookButtonComponent(onTap: onShare),
TwitterButtonComponent(onTap: onShare),
],
);
}
/// {@template facebook_button_component}
/// Button for sharing on Facebook.
/// {@endtemplate}
class FacebookButtonComponent extends SpriteComponent
with HasGameRef, Tappable {
/// {@macro facebook_button_component}
FacebookButtonComponent({
OnSocialShareTap? onTap,
}) : _onTap = onTap,
super(
anchor: Anchor.center,
position: Vector2(-5, 0),
);
final OnSocialShareTap? _onTap;
@override
bool onTapDown(TapDownInfo info) {
_onTap?.call(SharePlatform.facebook);
return true;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(Assets.images.backbox.button.facebook.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 25;
}
}
/// {@template twitter_button_component}
/// Button for sharing on Twitter.
/// {@endtemplate}
class TwitterButtonComponent extends SpriteComponent with HasGameRef, Tappable {
/// {@macro twitter_button_component}
TwitterButtonComponent({
OnSocialShareTap? onTap,
}) : _onTap = onTap,
super(
anchor: Anchor.center,
position: Vector2(5, 0),
);
final OnSocialShareTap? _onTap;
@override
bool onTapDown(TapDownInfo info) {
_onTap?.call(SharePlatform.twitter);
return true;
}
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(Assets.images.backbox.button.twitter.keyName),
);
this.sprite = sprite;
size = sprite.originalSize / 25;
}
}

@ -1,6 +1,5 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -9,7 +8,6 @@ import 'package:pinball_flame/pinball_flame.dart';
/// ///
/// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s. /// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s.
/// {@endtemplate} /// {@endtemplate}
// TODO(allisonryan0002): Consider renaming.
class BottomGroup extends Component with ZIndex { class BottomGroup extends Component with ZIndex {
/// {@macro bottom_group} /// {@macro bottom_group}
BottomGroup() BottomGroup()
@ -41,7 +39,7 @@ class _BottomGroupSide extends Component {
final direction = _side.direction; final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? -0.45 : -6.8; final centerXAdjustment = _side.isLeft ? -0.45 : -6.8;
final flipper = ControlledFlipper( final flipper = Flipper(
side: _side, side: _side,
)..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6); )..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side) final baseboard = Baseboard(side: _side)
@ -54,6 +52,7 @@ class _BottomGroupSide extends Component {
children: [ children: [
ScoringContactBehavior(points: Points.fiveThousand) ScoringContactBehavior(points: Points.fiveThousand)
..applyTo(['bouncy_edge']), ..applyTo(['bouncy_edge']),
KickerNoiseBehavior()..applyTo(['bouncy_edge']),
], ],
)..initialPosition = Vector2( )..initialPosition = Vector2(
(22.44 * direction) + centerXAdjustment, (22.44 * direction) + centerXAdjustment,

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

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

@ -11,10 +11,6 @@ class ChromeDinoBonusBehavior extends Component
void onMount() { void onMount() {
super.onMount(); super.onMount();
final chromeDino = parent.firstChild<ChromeDino>()!; final chromeDino = parent.firstChild<ChromeDino>()!;
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
chromeDino.bloc.stream.listen((state) { chromeDino.bloc.stream.listen((state) {
final listenWhen = state.status == ChromeDinoStatus.chomping; final listenWhen = state.status == ChromeDinoStatus.chomping;
if (!listenWhen) return; if (!listenWhen) return;

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

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

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

@ -13,9 +13,6 @@ class GoogleWordBonusBehavior extends Component
final googleLetters = parent.children.whereType<GoogleLetter>(); final googleLetters = parent.children.whereType<GoogleLetter>();
for (final letter in googleLetters) { for (final letter in googleLetters) {
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
letter.bloc.stream.listen((_) { letter.bloc.stream.listen((_) {
final achievedBonus = googleLetters final achievedBonus = googleLetters
.every((letter) => letter.bloc.state == GoogleLetterState.lit); .every((letter) => letter.bloc.state == GoogleLetterState.lit);

@ -13,10 +13,6 @@ class SparkyComputerBonusBehavior extends Component
super.onMount(); super.onMount();
final sparkyComputer = parent.firstChild<SparkyComputer>()!; final sparkyComputer = parent.firstChild<SparkyComputer>()!;
final animatronic = parent.firstChild<SparkyAnimatronic>()!; final animatronic = parent.firstChild<SparkyAnimatronic>()!;
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
sparkyComputer.bloc.stream.listen((state) async { sparkyComputer.bloc.stream.listen((state) async {
final listenWhen = state == SparkyComputerState.withBall; final listenWhen = state == SparkyComputerState.withBall;
if (!listenWhen) return; if (!listenWhen) return;

@ -1,3 +1,4 @@
import 'package:flame/extensions.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart' as components; import 'package:pinball_components/pinball_components.dart' as components;
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
@ -5,7 +6,7 @@ import 'package:pinball_theme/pinball_theme.dart' hide Assets;
/// Add methods to help loading and caching game assets. /// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame { extension PinballGameAssetsX on PinballGame {
/// Returns a list of assets to be loaded /// Returns a list of assets to be loaded
List<Future> preLoadAssets() { List<Future<Image>> preLoadAssets() {
const dashTheme = DashTheme(); const dashTheme = DashTheme();
const sparkyTheme = SparkyTheme(); const sparkyTheme = SparkyTheme();
const androidTheme = AndroidTheme(); const androidTheme = AndroidTheme();
@ -100,6 +101,11 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName), images.load(components.Assets.images.sparky.bumper.c.dimmed.keyName),
images.load(components.Assets.images.backbox.marquee.keyName), images.load(components.Assets.images.backbox.marquee.keyName),
images.load(components.Assets.images.backbox.displayDivider.keyName), images.load(components.Assets.images.backbox.displayDivider.keyName),
images.load(components.Assets.images.backbox.button.facebook.keyName),
images.load(components.Assets.images.backbox.button.twitter.keyName),
images.load(
components.Assets.images.backbox.displayTitleDecoration.keyName,
),
images.load(components.Assets.images.googleWord.letter1.lit.keyName), images.load(components.Assets.images.googleWord.letter1.lit.keyName),
images.load(components.Assets.images.googleWord.letter1.dimmed.keyName), images.load(components.Assets.images.googleWord.letter1.dimmed.keyName),
images.load(components.Assets.images.googleWord.letter2.lit.keyName), images.load(components.Assets.images.googleWord.letter2.lit.keyName),
@ -135,13 +141,17 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.skillShot.pin.keyName), images.load(components.Assets.images.skillShot.pin.keyName),
images.load(components.Assets.images.skillShot.lit.keyName), images.load(components.Assets.images.skillShot.lit.keyName),
images.load(components.Assets.images.skillShot.dimmed.keyName), images.load(components.Assets.images.skillShot.dimmed.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName),
images.load(dinoTheme.leaderboardIcon.keyName), images.load(androidTheme.background.keyName),
images.load(androidTheme.ball.keyName), images.load(androidTheme.ball.keyName),
images.load(dashTheme.leaderboardIcon.keyName),
images.load(dashTheme.background.keyName),
images.load(dashTheme.ball.keyName), images.load(dashTheme.ball.keyName),
images.load(dinoTheme.leaderboardIcon.keyName),
images.load(dinoTheme.background.keyName),
images.load(dinoTheme.ball.keyName), images.load(dinoTheme.ball.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.background.keyName),
images.load(sparkyTheme.ball.keyName), images.load(sparkyTheme.ball.keyName),
]; ];
} }

@ -1,4 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:async'; import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
@ -11,23 +10,25 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:share_repository/share_repository.dart';
class PinballGame extends PinballForge2DGame class PinballGame extends PinballForge2DGame
with HasKeyboardHandlerComponents, MultiTouchTapDetector { with HasKeyboardHandlerComponents, MultiTouchTapDetector, HasTappables {
PinballGame({ PinballGame({
required CharacterTheme characterTheme, required CharacterThemeCubit characterThemeBloc,
required this.leaderboardRepository, required this.leaderboardRepository,
required this.shareRepository,
required GameBloc gameBloc, required GameBloc gameBloc,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballPlayer player, required PinballAudioPlayer audioPlayer,
}) : focusNode = FocusNode(), }) : focusNode = FocusNode(),
_gameBloc = gameBloc, _gameBloc = gameBloc,
_player = player, _audioPlayer = audioPlayer,
_characterTheme = characterTheme, _characterThemeBloc = characterThemeBloc,
_l10n = l10n, _l10n = l10n,
super( super(
gravity: Vector2(0, 30), gravity: Vector2(0, 30),
@ -38,38 +39,63 @@ class PinballGame extends PinballForge2DGame
/// Identifier of the play button overlay /// Identifier of the play button overlay
static const playButtonOverlay = 'play_button'; static const playButtonOverlay = 'play_button';
/// Identifier of the mobile controls overlay
static const mobileControlsOverlay = 'mobile_controls';
@override @override
Color backgroundColor() => Colors.transparent; Color backgroundColor() => Colors.transparent;
final FocusNode focusNode; final FocusNode focusNode;
final CharacterTheme _characterTheme; final CharacterThemeCubit _characterThemeBloc;
final PinballPlayer _player; final PinballAudioPlayer _audioPlayer;
final LeaderboardRepository leaderboardRepository; final LeaderboardRepository leaderboardRepository;
final ShareRepository shareRepository;
final AppLocalizations _l10n; final AppLocalizations _l10n;
final GameBloc _gameBloc; final GameBloc _gameBloc;
List<LeaderboardEntryData>? _entries;
Future<void> preFetchLeaderboard() async {
try {
_entries = await leaderboardRepository.fetchTop10Leaderboard();
} catch (_) {
// An initial null leaderboard means that we couldn't fetch
// the entries for the [Backbox] and it will show the relevant display.
_entries = null;
}
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await add( await add(
FlameBlocProvider<GameBloc, GameState>.value( FlameMultiBlocProvider(
value: _gameBloc, providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: _gameBloc,
),
FlameBlocProvider<CharacterThemeCubit, CharacterThemeState>.value(
value: _characterThemeBloc,
),
],
children: [ children: [
MultiFlameProvider( MultiFlameProvider(
providers: [ providers: [
FlameProvider<PinballPlayer>.value(_player), FlameProvider<PinballAudioPlayer>.value(_audioPlayer),
FlameProvider<CharacterTheme>.value(_characterTheme),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository), FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<ShareRepository>.value(shareRepository),
FlameProvider<AppLocalizations>.value(_l10n), FlameProvider<AppLocalizations>.value(_l10n),
], ],
children: [ children: [
BonusNoiseBehavior(), BonusNoiseBehavior(),
GameBlocStatusListener(), GameBlocStatusListener(),
BallSpawningBehavior(), BallSpawningBehavior(),
CharacterSelectionBehavior(),
CameraFocusingBehavior(), CameraFocusingBehavior(),
CanvasComponent( CanvasComponent(
onSpritePainted: (paint) { onSpritePainted: (paint) {
@ -80,9 +106,14 @@ class PinballGame extends PinballForge2DGame
children: [ children: [
ZCanvasComponent( ZCanvasComponent(
children: [ children: [
ArcadeBackground(),
BoardBackgroundSpriteComponent(), BoardBackgroundSpriteComponent(),
Boundaries(), Boundaries(),
Backbox(leaderboardRepository: leaderboardRepository), Backbox(
leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
entries: _entries,
),
GoogleWord(position: Vector2(-4.45, 1.8)), GoogleWord(position: Vector2(-4.45, 1.8)),
Multipliers(), Multipliers(),
Multiballs(), Multiballs(),
@ -119,7 +150,7 @@ class PinballGame extends PinballForge2DGame
final rocket = descendants().whereType<RocketSpriteComponent>().first; final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size; final bounds = rocket.topLeftPosition & rocket.size;
// NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually. // NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually.
if (bounds.contains(info.eventPosition.game.toOffset())) { if (bounds.contains(info.eventPosition.game.toOffset())) {
descendants().whereType<Plunger>().single.pullFor(2); descendants().whereType<Plunger>().single.pullFor(2);
} else { } else {
@ -161,15 +192,17 @@ class PinballGame extends PinballForge2DGame
class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({ DebugPinballGame({
required CharacterTheme characterTheme, required CharacterThemeCubit characterThemeBloc,
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required ShareRepository shareRepository,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballPlayer player, required PinballAudioPlayer audioPlayer,
required GameBloc gameBloc, required GameBloc gameBloc,
}) : super( }) : super(
characterTheme: characterTheme, characterThemeBloc: characterThemeBloc,
player: player, audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
l10n: l10n, l10n: l10n,
gameBloc: gameBloc, gameBloc: gameBloc,
); );
@ -246,7 +279,6 @@ class PreviewLine extends PositionComponent with HasGameRef<DebugPinballGame> {
} }
} }
// TODO(wolfenrain): investigate this CI failure.
class _DebugInformation extends Component with HasGameRef<DebugPinballGame> { class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
@override @override
PositionType get positionType => PositionType.widget; PositionType get positionType => PositionType.widget;

@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -7,13 +5,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/more_information/more_information.dart'; import 'package:pinball/more_information/more_information.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
import 'package:share_repository/share_repository.dart';
class PinballGamePage extends StatelessWidget { class PinballGamePage extends StatelessWidget {
const PinballGamePage({ const PinballGamePage({
@ -23,80 +21,61 @@ class PinballGamePage extends StatelessWidget {
final bool isDebugMode; final bool isDebugMode;
static Route route({bool isDebugMode = kDebugMode}) {
return MaterialPageRoute<void>(
builder: (_) => PinballGamePage(isDebugMode: isDebugMode),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final characterTheme = final characterThemeBloc = context.read<CharacterThemeCubit>();
context.read<CharacterThemeCubit>().state.characterTheme; final audioPlayer = context.read<PinballAudioPlayer>();
final player = context.read<PinballPlayer>();
final leaderboardRepository = context.read<LeaderboardRepository>(); final leaderboardRepository = context.read<LeaderboardRepository>();
final shareRepository = context.read<ShareRepository>();
final gameBloc = context.read<GameBloc>(); final gameBloc = context.read<GameBloc>();
final game = isDebugMode final game = isDebugMode
? DebugPinballGame( ? DebugPinballGame(
characterTheme: characterTheme, characterThemeBloc: characterThemeBloc,
player: player, audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
l10n: context.l10n, l10n: context.l10n,
gameBloc: gameBloc, gameBloc: gameBloc,
) )
: PinballGame( : PinballGame(
characterTheme: characterTheme, characterThemeBloc: characterThemeBloc,
player: player, audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
shareRepository: shareRepository,
l10n: context.l10n, l10n: context.l10n,
gameBloc: gameBloc, gameBloc: gameBloc,
); );
final loadables = [ return Scaffold(
...game.preLoadAssets(), backgroundColor: PinballColors.black,
...player.load(), body: BlocProvider(
...BonusAnimation.loadAssets(), create: (_) => AssetsManagerCubit(game, audioPlayer)..load(),
...SelectedCharacter.loadAssets(), child: PinballGameView(game),
]; ),
return BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
child: PinballGameView(game: game),
); );
} }
} }
class PinballGameView extends StatelessWidget { class PinballGameView extends StatelessWidget {
const PinballGameView({ const PinballGameView(this.game, {Key? key}) : super(key: key);
Key? key,
required this.game,
}) : super(key: key);
final PinballGame game; final PinballGame game;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLoading = context.select( return BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
(AssetsManagerCubit bloc) => bloc.state.progress != 1, builder: (context, state) {
); return state.isLoading
return Container(
decoration: const CrtBackground(),
child: Scaffold(
backgroundColor: PinballColors.transparent,
body: isLoading
? const AssetsLoadingPage() ? const AssetsLoadingPage()
: PinballGameLoadedView(game: game), : PinballGameLoadedView(game);
), },
); );
} }
} }
@visibleForTesting @visibleForTesting
class PinballGameLoadedView extends StatelessWidget { class PinballGameLoadedView extends StatelessWidget {
const PinballGameLoadedView({ const PinballGameLoadedView(this.game, {Key? key}) : super(key: key);
Key? key,
required this.game,
}) : super(key: key);
final PinballGame game; final PinballGame game;
@ -125,6 +104,14 @@ class PinballGameLoadedView extends StatelessWidget {
child: PlayButtonOverlay(), child: PlayButtonOverlay(),
); );
}, },
PinballGame.mobileControlsOverlay: (context, game) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: MobileControls(game: game),
);
},
}, },
), ),
), ),
@ -179,7 +166,7 @@ class _PositionedInfoIcon extends StatelessWidget {
visible: state.status.isGameOver, visible: state.status.isGameOver,
child: IconButton( child: IconButton(
iconSize: 50, iconSize: 50,
icon: Assets.images.linkBox.infoIcon.image(), icon: const Icon(Icons.info, color: PinballColors.white),
onPressed: () => showMoreInformationDialog(context), onPressed: () => showMoreInformationDialog(context),
), ),
); );

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

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

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

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

@ -14,7 +14,8 @@ class $AssetsImagesGen {
const $AssetsImagesBonusAnimationGen(); const $AssetsImagesBonusAnimationGen();
$AssetsImagesComponentsGen get components => $AssetsImagesComponentsGen get components =>
const $AssetsImagesComponentsGen(); const $AssetsImagesComponentsGen();
$AssetsImagesLinkBoxGen get linkBox => const $AssetsImagesLinkBoxGen(); $AssetsImagesLoadingGameGen get loadingGame =>
const $AssetsImagesLoadingGameGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
} }
@ -54,12 +55,12 @@ class $AssetsImagesComponentsGen {
const AssetGenImage('assets/images/components/space.png'); const AssetGenImage('assets/images/components/space.png');
} }
class $AssetsImagesLinkBoxGen { class $AssetsImagesLoadingGameGen {
const $AssetsImagesLinkBoxGen(); const $AssetsImagesLoadingGameGen();
/// File path: assets/images/link_box/info_icon.png /// File path: assets/images/loading_game/io_pinball.png
AssetGenImage get infoIcon => AssetGenImage get ioPinball =>
const AssetGenImage('assets/images/link_box/info_icon.png'); const AssetGenImage('assets/images/loading_game/io_pinball.png');
} }
class $AssetsImagesScoreGen { class $AssetsImagesScoreGen {

@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -93,7 +91,9 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
return WillPopScope( return WillPopScope(
onWillPop: () { onWillPop: () {
widget.onDismissCallback.call(); widget.onDismissCallback.call();
context.read<PinballPlayer>().play(PinballAudio.ioPinballVoiceOver); context
.read<PinballAudioPlayer>()
.play(PinballAudio.ioPinballVoiceOver);
return Future.value(true); return Future.value(true);
}, },
child: PinballDialog( child: PinballDialog(
@ -242,7 +242,7 @@ class _DesktopFlipperControls extends StatelessWidget {
children: [ children: [
Text( Text(
l10n.flipperControls, l10n.flipperControls,
style: Theme.of(context).textTheme.subtitle2, style: Theme.of(context).textTheme.headline4,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Column( Column(

@ -148,8 +148,78 @@
"@loading": { "@loading": {
"description": "Text shown to indicate loading times" "description": "Text shown to indicate loading times"
}, },
"replay": "Replay",
"@replay": {
"description": "Text displayed on the share page for replay button"
},
"ioPinball": "I/O Pinball", "ioPinball": "I/O Pinball",
"@ioPinball": { "@ioPinball": {
"description": "I/O Pinball - Name of the game" "description": "I/O Pinball - Name of the game"
},
"shareYourScore": "Share your score",
"@shareYourScore": {
"description": "Text shown on title of info screen"
},
"andChallengeYourFriends": "AND CHALLENGE YOUR FRIENDS",
"@challengeYourFriends": {
"description": "Text shown on title of info screen"
},
"share": "SHARE",
"@share": {
"description": "Text for share link on info screen."
},
"gotoIO": "GO TO I/O",
"@gotoIO": {
"description": "Text for going to I/O site link on info screen."
},
"learnMore": "Learn more about building games in Flutter with",
"@learnMore": {
"description": "Text shown on description of info screen"
},
"firebaseOr": "Firebase or dive right into the",
"@firebaseOr": {
"description": "Text shown on description of info screen"
},
"openSourceCode": "open source code.",
"@openSourceCode": {
"description": "Text shown on description of info screen"
},
"enter": "Enter",
"@enter": {
"description": "Text shown on the mobile controls enter button"
},
"initialsErrorTitle": "Uh-oh... well, that didnt work",
"@initialsErrorTitle": {
"description": "Title shown when the initials submission fails"
},
"initialsErrorMessage": "Please try a different combination of letters",
"@initialsErrorMessage": {
"description": "Message on shown when the initials submission fails"
},
"leaderboardErrorMessage": "No connection. Leaderboard and sharing functionality is unavailable.",
"@leaderboardErrorMessage": {
"description": "Text shown when the leaderboard had an error while loading"
}
,
"letEveryone": "Let everyone know about I/O Pinball",
"@letEveryone": {
"description": "Text displayed on share screen for description"
},
"bySharingYourScore": "by sharing your score to your preferred",
"@bySharingYourScore": {
"description": "Text displayed on share screen for description"
},
"socialMediaAccount": "social media account!",
"@socialMediaAccount": {
"description": "Text displayed on share screen for description"
},
"iGotScoreAtPinball": "I got {score} at the #IOPinball machine, can you beat my score? See you at #GoogleIO!",
"@iGotScoreAtPinball": {
"description": "Text to share score on Social Network",
"placeholders": {
"score": {
"type": "int"
}
}
} }
} }

@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';

@ -6,12 +6,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart'; import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart'; import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:share_repository/share_repository.dart';
void main() { void main() {
bootstrap((firestore, firebaseAuth) async { bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore); final leaderboardRepository = LeaderboardRepository(firestore);
const shareRepository =
ShareRepository(appUrl: ShareRepository.pinballGameUrl);
final authenticationRepository = AuthenticationRepository(firebaseAuth); final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballPlayer = PinballPlayer(); final pinballAudioPlayer = PinballAudioPlayer();
unawaited( unawaited(
Firebase.initializeApp().then( Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(), (_) => authenticationRepository.authenticateAnonymously(),
@ -20,7 +23,8 @@ void main() {
return App( return App(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
pinballPlayer: pinballPlayer, shareRepository: shareRepository,
pinballAudioPlayer: pinballAudioPlayer,
); );
}); });
} }

@ -1,26 +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';
void main() {
bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore);
final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballPlayer = PinballPlayer();
unawaited(
Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(),
),
);
return App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository,
pinballPlayer: pinballPlayer,
);
});
}

@ -1,26 +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';
void main() {
bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore);
final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballPlayer = PinballPlayer();
unawaited(
Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(),
),
);
return App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository,
pinballPlayer: pinballPlayer,
);
});
}

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

@ -1,6 +1,3 @@
// ignore_for_file: public_member_api_docs
// TODO(allisonryan0002): Document this section when the API is stable.
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';

@ -1,6 +1,3 @@
// ignore_for_file: public_member_api_docs
// TODO(allisonryan0002): Document this section when the API is stable.
part of 'character_theme_cubit.dart'; part of 'character_theme_cubit.dart';
class CharacterThemeState extends Equatable { class CharacterThemeState extends Equatable {

@ -69,9 +69,9 @@ class _CharacterGrid extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
_Character( _Character(
key: const Key('sparky_character_selection'), key: const Key('dash_character_selection'),
character: const SparkyTheme(), character: const DashTheme(),
isSelected: state.isSparkySelected, isSelected: state.isDashSelected,
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
_Character( _Character(
@ -87,9 +87,9 @@ class _CharacterGrid extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
_Character( _Character(
key: const Key('dash_character_selection'), key: const Key('sparky_character_selection'),
character: const DashTheme(), character: const SparkyTheme(),
isSelected: state.isDashSelected, isSelected: state.isSparkySelected,
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
_Character( _Character(

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

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

@ -14,11 +14,17 @@ class $AssetsMusicGen {
class $AssetsSfxGen { class $AssetsSfxGen {
const $AssetsSfxGen(); const $AssetsSfxGen();
String get android => 'assets/sfx/android.mp3';
String get bumperA => 'assets/sfx/bumper_a.mp3'; String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3'; String get bumperB => 'assets/sfx/bumper_b.mp3';
String get cowMoo => 'assets/sfx/cow_moo.mp3';
String get dash => 'assets/sfx/dash.mp3';
String get dino => 'assets/sfx/dino.mp3';
String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3'; String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3';
String get google => 'assets/sfx/google.mp3'; String get google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';
String get kickerA => 'assets/sfx/kicker_a.mp3';
String get kickerB => 'assets/sfx/kicker_b.mp3';
String get launcher => 'assets/sfx/launcher.mp3'; String get launcher => 'assets/sfx/launcher.mp3';
String get sparky => 'assets/sfx/sparky.mp3'; String get sparky => 'assets/sfx/sparky.mp3';
} }

@ -1,33 +1,49 @@
import 'dart:math'; import 'dart:math';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:clock/clock.dart';
import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/gen/assets.gen.dart';
/// Sounds available for play /// Sounds available to play.
enum PinballAudio { enum PinballAudio {
/// Google /// Google.
google, google,
/// Bumper /// Bumper.
bumper, bumper,
/// Background music /// Cow moo.
cowMoo,
/// Background music.
backgroundMusic, backgroundMusic,
/// IO Pinball voice over /// IO Pinball voice over.
ioPinballVoiceOver, ioPinballVoiceOver,
/// Game over /// Game over.
gameOverVoiceOver, gameOverVoiceOver,
/// Launcher /// Launcher.
launcher, launcher,
/// Sparky /// Kicker.
kicker,
/// Sparky.
sparky, sparky,
/// Android
android,
/// Dino
dino,
/// Dash
dash,
} }
/// Defines the contract of the creation of an [AudioPool]. /// Defines the contract of the creation of an [AudioPool].
@ -100,48 +116,83 @@ class _LoopAudio extends _Audio {
} }
} }
class _BumperAudio extends _Audio { class _RandomABAudio extends _Audio {
_BumperAudio({ _RandomABAudio({
required this.createAudioPool, required this.createAudioPool,
required this.seed, required this.seed,
required this.audioAssetA,
required this.audioAssetB,
this.volume,
}); });
final CreateAudioPool createAudioPool; final CreateAudioPool createAudioPool;
final Random seed; final Random seed;
final String audioAssetA;
final String audioAssetB;
final double? volume;
late AudioPool bumperA; late AudioPool audioA;
late AudioPool bumperB; late AudioPool audioB;
@override @override
Future<void> load() async { Future<void> load() async {
await Future.wait( await Future.wait(
[ [
createAudioPool( createAudioPool(
prefixFile(Assets.sfx.bumperA), prefixFile(audioAssetA),
maxPlayers: 4, maxPlayers: 4,
prefix: '', prefix: '',
).then((pool) => bumperA = pool), ).then((pool) => audioA = pool),
createAudioPool( createAudioPool(
prefixFile(Assets.sfx.bumperB), prefixFile(audioAssetB),
maxPlayers: 4, maxPlayers: 4,
prefix: '', prefix: '',
).then((pool) => bumperB = pool), ).then((pool) => audioB = pool),
], ],
); );
} }
@override @override
void play() { void play() {
(seed.nextBool() ? bumperA : bumperB).start(volume: 0.6); (seed.nextBool() ? audioA : audioB).start(volume: volume ?? 1);
}
}
class _ThrottledAudio extends _Audio {
_ThrottledAudio({
required this.preCacheSingleAudio,
required this.playSingleAudio,
required this.path,
required this.duration,
});
final PreCacheSingleAudio preCacheSingleAudio;
final PlaySingleAudio playSingleAudio;
final String path;
final Duration duration;
DateTime? _lastPlayed;
@override
Future<void> load() => preCacheSingleAudio(prefixFile(path));
@override
void play() {
final now = clock.now();
if (_lastPlayed == null ||
(_lastPlayed != null && now.difference(_lastPlayed!) > duration)) {
_lastPlayed = now;
playSingleAudio(prefixFile(path));
}
} }
} }
/// {@template pinball_player} /// {@template pinball_audio_player}
/// Sound manager for the pinball game /// Sound manager for the pinball game.
/// {@endtemplate} /// {@endtemplate}
class PinballPlayer { class PinballAudioPlayer {
/// {@macro pinball_player} /// {@macro pinball_audio_player}
PinballPlayer({ PinballAudioPlayer({
CreateAudioPool? createAudioPool, CreateAudioPool? createAudioPool,
PlaySingleAudio? playSingleAudio, PlaySingleAudio? playSingleAudio,
LoopSingleAudio? loopSingleAudio, LoopSingleAudio? loopSingleAudio,
@ -169,6 +220,21 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.sparky, path: Assets.sfx.sparky,
), ),
PinballAudio.dino: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.dino,
),
PinballAudio.dash: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.dash,
),
PinballAudio.android: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.android,
),
PinballAudio.launcher: _SimplePlayAudio( PinballAudio.launcher: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
@ -184,9 +250,25 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.gameOverVoiceOver, path: Assets.sfx.gameOverVoiceOver,
), ),
PinballAudio.bumper: _BumperAudio( PinballAudio.bumper: _RandomABAudio(
createAudioPool: _createAudioPool, createAudioPool: _createAudioPool,
seed: _seed, seed: _seed,
audioAssetA: Assets.sfx.bumperA,
audioAssetB: Assets.sfx.bumperB,
volume: 0.6,
),
PinballAudio.kicker: _RandomABAudio(
createAudioPool: _createAudioPool,
seed: _seed,
audioAssetA: Assets.sfx.kickerA,
audioAssetB: Assets.sfx.kickerB,
volume: 0.6,
),
PinballAudio.cowMoo: _ThrottledAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.cowMoo,
duration: const Duration(seconds: 2),
), ),
PinballAudio.backgroundMusic: _LoopAudio( PinballAudio.backgroundMusic: _LoopAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
@ -208,19 +290,19 @@ class PinballPlayer {
final Random _seed; final Random _seed;
/// Registered audios on the Player /// Registered audios on the Player.
@visibleForTesting @visibleForTesting
// ignore: library_private_types_in_public_api // ignore: library_private_types_in_public_api
late final Map<PinballAudio, _Audio> audios; late final Map<PinballAudio, _Audio> audios;
/// Loads the sounds effects into the memory /// Loads the sounds effects into the memory.
List<Future<void>> load() { List<Future<void>> load() {
_configureAudioCache(FlameAudio.audioCache); _configureAudioCache(FlameAudio.audioCache);
return audios.values.map((a) => a.load()).toList(); return audios.values.map((a) => a.load()).toList();
} }
/// Plays the received auido /// Plays the received audio.
void play(PinballAudio audio) { void play(PinballAudio audio) {
assert( assert(
audios.containsKey(audio), audios.containsKey(audio),

@ -8,6 +8,7 @@ environment:
dependencies: dependencies:
audioplayers: ^0.20.1 audioplayers: ^0.20.1
clock: ^1.1.0
flame_audio: ^1.0.1 flame_audio: ^1.0.1
flutter: flutter:
sdk: flutter sdk: flutter

@ -2,6 +2,7 @@
import 'dart:math'; import 'dart:math';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:clock/clock.dart';
import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -43,6 +44,8 @@ class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {}
class _MockRandom extends Mock implements Random {} class _MockRandom extends Mock implements Random {}
class _MockClock extends Mock implements Clock {}
void main() { void main() {
group('PinballAudio', () { group('PinballAudio', () {
late _MockCreateAudioPool createAudioPool; late _MockCreateAudioPool createAudioPool;
@ -51,7 +54,7 @@ void main() {
late _MockLoopSingleAudio loopSingleAudio; late _MockLoopSingleAudio loopSingleAudio;
late _PreCacheSingleAudio preCacheSingleAudio; late _PreCacheSingleAudio preCacheSingleAudio;
late Random seed; late Random seed;
late PinballPlayer player; late PinballAudioPlayer audioPlayer;
setUpAll(() { setUpAll(() {
registerFallbackValue(_MockAudioCache()); registerFallbackValue(_MockAudioCache());
@ -81,7 +84,7 @@ void main() {
seed = _MockRandom(); seed = _MockRandom();
player = PinballPlayer( audioPlayer = PinballAudioPlayer(
configureAudioCache: configureAudioCache.onCall, configureAudioCache: configureAudioCache.onCall,
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
@ -92,12 +95,12 @@ void main() {
}); });
test('can be instantiated', () { test('can be instantiated', () {
expect(PinballPlayer(), isNotNull); expect(PinballAudioPlayer(), isNotNull);
}); });
group('load', () { group('load', () {
test('creates the bumpers pools', () async { test('creates the bumpers pools', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
verify( verify(
() => createAudioPool.onCall( () => createAudioPool.onCall(
@ -116,26 +119,46 @@ void main() {
).called(1); ).called(1);
}); });
test('creates the kicker pools', () async {
await Future.wait(audioPlayer.load());
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.kickerA}',
maxPlayers: 4,
prefix: '',
),
).called(1);
verify(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.kickerB}',
maxPlayers: 4,
prefix: '',
),
).called(1);
});
test('configures the audio cache instance', () async { test('configures the audio cache instance', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
verify(() => configureAudioCache.onCall(FlameAudio.audioCache)) verify(() => configureAudioCache.onCall(FlameAudio.audioCache))
.called(1); .called(1);
}); });
test('sets the correct prefix', () async { test('sets the correct prefix', () async {
player = PinballPlayer( audioPlayer = PinballAudioPlayer(
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall,
); );
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
expect(FlameAudio.audioCache.prefix, equals('')); expect(FlameAudio.audioCache.prefix, equals(''));
}); });
test('pre cache the assets', () async { test('pre cache the assets', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
@ -145,6 +168,18 @@ void main() {
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/sparky.mp3'), .onCall('packages/pinball_audio/assets/sfx/sparky.mp3'),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/dino.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/android.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/dash.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio.onCall( () => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3', 'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
@ -159,6 +194,10 @@ void main() {
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/launcher.mp3'), .onCall('packages/pinball_audio/assets/sfx/launcher.mp3'),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/cow_moo.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'), .onCall('packages/pinball_audio/assets/music/background.mp3'),
@ -197,8 +236,8 @@ void main() {
group('when seed is true', () { group('when seed is true', () {
test('plays the bumper A sound pool', () async { test('plays the bumper A sound pool', () async {
when(seed.nextBool).thenReturn(true); when(seed.nextBool).thenReturn(true);
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.bumper); audioPlayer.play(PinballAudio.bumper);
verify(() => bumperAPool.start(volume: 0.6)).called(1); verify(() => bumperAPool.start(volume: 0.6)).called(1);
}); });
@ -207,18 +246,103 @@ void main() {
group('when seed is false', () { group('when seed is false', () {
test('plays the bumper B sound pool', () async { test('plays the bumper B sound pool', () async {
when(seed.nextBool).thenReturn(false); when(seed.nextBool).thenReturn(false);
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.bumper); audioPlayer.play(PinballAudio.bumper);
verify(() => bumperBPool.start(volume: 0.6)).called(1); verify(() => bumperBPool.start(volume: 0.6)).called(1);
}); });
}); });
}); });
group('kicker', () {
late AudioPool kickerAPool;
late AudioPool kickerBPool;
setUp(() {
kickerAPool = _MockAudioPool();
when(() => kickerAPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.kickerA}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => kickerAPool);
kickerBPool = _MockAudioPool();
when(() => kickerBPool.start(volume: any(named: 'volume')))
.thenAnswer((_) async => () {});
when(
() => createAudioPool.onCall(
'packages/pinball_audio/${Assets.sfx.kickerB}',
maxPlayers: any(named: 'maxPlayers'),
prefix: any(named: 'prefix'),
),
).thenAnswer((_) async => kickerBPool);
});
group('when seed is true', () {
test('plays the kicker A sound pool', () async {
when(seed.nextBool).thenReturn(true);
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.kicker);
verify(() => kickerAPool.start(volume: 0.6)).called(1);
});
});
group('when seed is false', () {
test('plays the kicker B sound pool', () async {
when(seed.nextBool).thenReturn(false);
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.kicker);
verify(() => kickerBPool.start(volume: 0.6)).called(1);
});
});
});
group('cow moo', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.cowMoo);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'),
).called(1);
});
test('only plays the sound again after 2 seconds', () async {
final clock = _MockClock();
await withClock(clock, () async {
when(clock.now).thenReturn(DateTime(2022));
await Future.wait(audioPlayer.load());
audioPlayer
..play(PinballAudio.cowMoo)
..play(PinballAudio.cowMoo);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'),
).called(1);
when(clock.now).thenReturn(DateTime(2022, 1, 1, 1, 2));
audioPlayer.play(PinballAudio.cowMoo);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.cowMoo}'),
).called(1);
});
});
});
group('google', () { group('google', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.google); audioPlayer.play(PinballAudio.google);
verify( verify(
() => playSingleAudio () => playSingleAudio
@ -229,8 +353,8 @@ void main() {
group('sparky', () { group('sparky', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.sparky); audioPlayer.play(PinballAudio.sparky);
verify( verify(
() => playSingleAudio () => playSingleAudio
@ -239,10 +363,46 @@ void main() {
}); });
}); });
group('dino', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.dino);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.dino}'),
).called(1);
});
});
group('android', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.android);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.android}'),
).called(1);
});
});
group('dash', () {
test('plays the correct file', () async {
await Future.wait(audioPlayer.load());
audioPlayer.play(PinballAudio.dash);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.dash}'),
).called(1);
});
});
group('launcher', () { group('launcher', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.launcher); audioPlayer.play(PinballAudio.launcher);
verify( verify(
() => playSingleAudio () => playSingleAudio
@ -253,8 +413,8 @@ void main() {
group('ioPinballVoiceOver', () { group('ioPinballVoiceOver', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.ioPinballVoiceOver); audioPlayer.play(PinballAudio.ioPinballVoiceOver);
verify( verify(
() => playSingleAudio.onCall( () => playSingleAudio.onCall(
@ -266,8 +426,8 @@ void main() {
group('gameOverVoiceOver', () { group('gameOverVoiceOver', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.gameOverVoiceOver); audioPlayer.play(PinballAudio.gameOverVoiceOver);
verify( verify(
() => playSingleAudio.onCall( () => playSingleAudio.onCall(
@ -279,8 +439,8 @@ void main() {
group('backgroundMusic', () { group('backgroundMusic', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
player.play(PinballAudio.backgroundMusic); audioPlayer.play(PinballAudio.backgroundMusic);
verify( verify(
() => loopSingleAudio () => loopSingleAudio
@ -292,10 +452,13 @@ void main() {
test( test(
'throws assertions error when playing an unregistered audio', 'throws assertions error when playing an unregistered audio',
() async { () async {
player.audios.remove(PinballAudio.google); audioPlayer.audios.remove(PinballAudio.google);
await Future.wait(player.load()); await Future.wait(audioPlayer.load());
expect(() => player.play(PinballAudio.google), throwsAssertionError); expect(
() => audioPlayer.play(PinballAudio.google),
throwsAssertionError,
);
}, },
); );
}); });

@ -2,3 +2,6 @@ include: package:very_good_analysis/analysis_options.2.4.0.yaml
analyzer: analyzer:
exclude: exclude:
- lib/**/*.gen.dart - lib/**/*.gen.dart
linter:
rules:
public_member_api_docs: false

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 637 KiB

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

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

Loading…
Cancel
Save