Merge branch 'main' into feat/google-rollovers

pull/407/head
Allison Ryan 3 years ago
commit d425b14818

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

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

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

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

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

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

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

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

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

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

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

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

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

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

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

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

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

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

@ -5,27 +5,37 @@ import 'package:flutter/material.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' hide Assets;
import 'package:platform_helper/platform_helper.dart';
/// {@template backbox}
/// The [Backbox] of the pinball machine.
/// {@endtemplate}
class Backbox extends PositionComponent with ZIndex {
class Backbox extends PositionComponent with ZIndex, HasGameRef {
/// {@macro backbox}
Backbox({
required LeaderboardRepository leaderboardRepository,
}) : _bloc = BackboxBloc(leaderboardRepository: leaderboardRepository);
required List<LeaderboardEntryData>? entries,
}) : _bloc = BackboxBloc(
leaderboardRepository: leaderboardRepository,
initialEntries: entries,
),
_platformHelper = PlatformHelper();
/// {@macro backbox}
@visibleForTesting
Backbox.test({
required BackboxBloc bloc,
}) : _bloc = bloc;
required PlatformHelper platformHelper,
}) : _bloc = bloc,
_platformHelper = platformHelper;
late final Component _display;
final BackboxBloc _bloc;
final PlatformHelper _platformHelper;
late StreamSubscription<BackboxState> _subscription;
@override
@ -34,8 +44,6 @@ class Backbox extends PositionComponent with ZIndex {
anchor = Anchor.bottomCenter;
zIndex = ZIndexes.backbox;
_bloc.add(LeaderboardRequested());
await add(_BackboxSpriteComponent());
await add(_display = Component());
_build(_bloc.state);
@ -57,7 +65,12 @@ class Backbox extends PositionComponent with ZIndex {
_display.add(LoadingDisplay());
} else if (state is LeaderboardSuccessState) {
_display.add(LeaderboardDisplay(entries: state.entries));
} else if (state is LeaderboardFailureState) {
_display.add(LeaderboardFailureDisplay());
} else if (state is InitialsFormState) {
if (_platformHelper.isMobile) {
gameRef.overlays.add(PinballGame.mobileControlsOverlay);
}
_display.add(
InitialsInputDisplay(
score: state.score,
@ -74,9 +87,26 @@ class Backbox extends PositionComponent with ZIndex {
),
);
} else if (state is InitialsSuccessState) {
_display.add(InitialsSubmissionSuccessDisplay());
_display.add(
GameOverInfoDisplay(
onShare: () {
_bloc.add(ShareScoreRequested(score: state.score));
},
),
);
} else if (state is 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}
BackboxBloc({
required LeaderboardRepository leaderboardRepository,
required List<LeaderboardEntryData>? initialEntries,
}) : _leaderboardRepository = leaderboardRepository,
super(LoadingState()) {
super(
initialEntries != null
? LeaderboardSuccessState(entries: initialEntries)
: LeaderboardFailureState(),
) {
on<PlayerInitialsRequested>(_onPlayerInitialsRequested);
on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted);
on<ShareScoreRequested>(_onScoreShareRequested);
on<LeaderboardRequested>(_onLeaderboardRequested);
}
@ -48,11 +54,31 @@ class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
character: event.character.toType,
),
);
emit(InitialsSuccessState());
emit(
InitialsSuccessState(
score: event.score,
),
);
} catch (error, stackTrace) {
addError(error, stackTrace);
emit(InitialsFailureState());
emit(
InitialsFailureState(
score: event.score,
character: event.character,
),
);
}
}
Future<void> _onScoreShareRequested(
ShareScoreRequested event,
Emitter<BackboxState> emit,
) async {
emit(
ShareState(
score: event.score,
),
);
}
Future<void> _onLeaderboardRequested(

@ -52,6 +52,22 @@ class PlayerInitialsSubmitted extends BackboxEvent {
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
class LeaderboardRequested extends BackboxEvent {
@override

@ -54,14 +54,51 @@ class InitialsFormState extends BackboxState {
List<Object?> get props => [score, character];
}
/// State when the leaderboard was successfully loaded.
/// {@template initials_success_state}
/// State when the score and initials were successfully submitted.
/// {@endtemplate}
class InitialsSuccessState extends BackboxState {
/// {@macro initials_success_state}
const InitialsSuccessState({
required this.score,
}) : super();
/// Player's score.
final int score;
@override
List<Object?> get props => [];
List<Object?> get props => [score];
}
/// State when the initials submission failed.
class InitialsFailureState extends BackboxState {
const InitialsFailureState({
required this.score,
required this.character,
});
/// Player's score.
final int score;
/// Player's character.
final CharacterTheme character;
@override
List<Object?> get props => [];
List<Object?> get props => [score, character];
}
/// {@template share_state}
/// 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,7 @@
export 'game_over_info_display.dart';
export 'initials_input_display.dart';
export 'initials_submission_failure_display.dart';
export 'initials_submission_success_display.dart';
export 'leaderboard_display.dart';
export 'leaderboard_failure_display.dart';
export 'loading_display.dart';

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

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

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

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

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

@ -7,9 +7,9 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Bonus obtained at the [FlutterForest].
///
/// When all [DashNestBumper]s are hit at least once three times, the [Signpost]
/// When all [DashBumper]s are hit at least once three times, the [Signpost]
/// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball].
/// is awarded, and the [DashBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component
with
ParentIsA<FlutterForest>,
@ -19,14 +19,14 @@ class FlutterForestBonusBehavior extends Component
void onMount() {
super.onMount();
final bumpers = parent.children.whereType<DashNestBumper>();
final bumpers = parent.children.whereType<DashBumper>();
final signpost = parent.firstChild<Signpost>()!;
final animatronic = parent.firstChild<DashAnimatronic>()!;
for (final bumper in bumpers) {
bumper.bloc.stream.listen((state) {
final activatedAllBumpers = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active,
(bumper) => bumper.bloc.state == DashBumperState.active,
);
if (activatedAllBumpers) {

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

@ -20,7 +20,7 @@ class GameBlocStatusListener extends Component
case GameStatus.waiting:
break;
case GameStatus.playing:
readProvider<PinballPlayer>().play(PinballAudio.backgroundMusic);
readProvider<PinballAudioPlayer>().play(PinballAudio.backgroundMusic);
gameRef
.descendants()
.whereType<Flipper>()
@ -29,7 +29,7 @@ class GameBlocStatusListener extends Component
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
case GameStatus.gameOver:
readProvider<PinballPlayer>().play(PinballAudio.gameOverVoiceOver);
readProvider<PinballAudioPlayer>().play(PinballAudio.gameOverVoiceOver);
gameRef.descendants().whereType<Backbox>().first.requestInitials(
score: state.displayScore,
character: readBloc<CharacterThemeCubit, CharacterThemeState>()
@ -45,8 +45,11 @@ class GameBlocStatusListener extends Component
}
}
void _addFlipperKeyControls(Flipper flipper) =>
flipper.add(FlipperKeyControllingBehavior());
void _addFlipperKeyControls(Flipper flipper) {
flipper
..add(FlipperKeyControllingBehavior())
..moveDown();
}
void _removeFlipperKeyControls(Flipper flipper) => flipper
.descendants()

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

@ -16,16 +16,16 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class PinballGame extends PinballForge2DGame
with HasKeyboardHandlerComponents, MultiTouchTapDetector {
with HasKeyboardHandlerComponents, MultiTouchTapDetector, HasTappables {
PinballGame({
required CharacterThemeCubit characterThemeBloc,
required this.leaderboardRepository,
required GameBloc gameBloc,
required AppLocalizations l10n,
required PinballPlayer player,
required PinballAudioPlayer audioPlayer,
}) : focusNode = FocusNode(),
_gameBloc = gameBloc,
_player = player,
_audioPlayer = audioPlayer,
_characterThemeBloc = characterThemeBloc,
_l10n = l10n,
super(
@ -37,6 +37,9 @@ class PinballGame extends PinballForge2DGame
/// Identifier of the play button overlay
static const playButtonOverlay = 'play_button';
/// Identifier of the mobile controls overlay
static const mobileControlsOverlay = 'mobile_controls';
@override
Color backgroundColor() => Colors.transparent;
@ -44,7 +47,7 @@ class PinballGame extends PinballForge2DGame
final CharacterThemeCubit _characterThemeBloc;
final PinballPlayer _player;
final PinballAudioPlayer _audioPlayer;
final LeaderboardRepository leaderboardRepository;
@ -52,6 +55,18 @@ class PinballGame extends PinballForge2DGame
final GameBloc _gameBloc;
List<LeaderboardEntryData>? _entries;
Future<void> preFetchLeaderboard() async {
try {
_entries = await leaderboardRepository.fetchTop10Leaderboard();
} catch (_) {
// An initial null leaderboard means that we couldn't fetch
// the entries for the [Backbox] and it will show the relevant display.
_entries = null;
}
}
@override
Future<void> onLoad() async {
await add(
@ -67,7 +82,7 @@ class PinballGame extends PinballForge2DGame
children: [
MultiFlameProvider(
providers: [
FlameProvider<PinballPlayer>.value(_player),
FlameProvider<PinballAudioPlayer>.value(_audioPlayer),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<AppLocalizations>.value(_l10n),
],
@ -88,7 +103,10 @@ class PinballGame extends PinballForge2DGame
children: [
BoardBackgroundSpriteComponent(),
Boundaries(),
Backbox(leaderboardRepository: leaderboardRepository),
Backbox(
leaderboardRepository: leaderboardRepository,
entries: _entries,
),
GoogleGallery(),
Multipliers(),
Multiballs(),
@ -125,7 +143,7 @@ class PinballGame extends PinballForge2DGame
final rocket = descendants().whereType<RocketSpriteComponent>().first;
final bounds = rocket.topLeftPosition & rocket.size;
// NOTE(wolfen): As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually.
// NOTE: As long as Flame does not have https://github.com/flame-engine/flame/issues/1586 we need to check it at the highest level manually.
if (bounds.contains(info.eventPosition.game.toOffset())) {
descendants().whereType<Plunger>().single.pullFor(2);
} else {
@ -170,11 +188,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
required CharacterThemeCubit characterThemeBloc,
required LeaderboardRepository leaderboardRepository,
required AppLocalizations l10n,
required PinballPlayer player,
required PinballAudioPlayer audioPlayer,
required GameBloc gameBloc,
}) : super(
characterThemeBloc: characterThemeBloc,
player: player,
audioPlayer: audioPlayer,
leaderboardRepository: leaderboardRepository,
l10n: l10n,
gameBloc: gameBloc,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

@ -3,6 +3,8 @@
/// FlutterGen
/// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
@ -12,13 +14,19 @@ class $AssetsImagesGen {
$AssetsImagesBackboxGen get backbox => const $AssetsImagesBackboxGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen();
/// File path: assets/images/board-background.png
AssetGenImage get boardBackground =>
const AssetGenImage('assets/images/board-background.png');
$AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen();
$AssetsImagesDashGen get dash => const $AssetsImagesDashGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
/// File path: assets/images/error_background.png
AssetGenImage get errorBackground =>
const AssetGenImage('assets/images/error_background.png');
$AssetsImagesFlapperGen get flapper => const $AssetsImagesFlapperGen();
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
$AssetsImagesGoogleRolloverGen get googleRollover =>
@ -53,8 +61,15 @@ class $AssetsImagesAndroidGen {
class $AssetsImagesBackboxGen {
const $AssetsImagesBackboxGen();
/// File path: assets/images/backbox/display-divider.png
AssetGenImage get displayDivider =>
const AssetGenImage('assets/images/backbox/display-divider.png');
/// File path: assets/images/backbox/display_title_decoration.png
AssetGenImage get displayTitleDecoration =>
const AssetGenImage('assets/images/backbox/display_title_decoration.png');
/// File path: assets/images/backbox/marquee.png
AssetGenImage get marquee =>
const AssetGenImage('assets/images/backbox/marquee.png');
}
@ -62,6 +77,7 @@ class $AssetsImagesBackboxGen {
class $AssetsImagesBallGen {
const $AssetsImagesBallGen();
/// File path: assets/images/ball/flame_effect.png
AssetGenImage get flameEffect =>
const AssetGenImage('assets/images/ball/flame_effect.png');
}
@ -69,8 +85,11 @@ class $AssetsImagesBallGen {
class $AssetsImagesBaseboardGen {
const $AssetsImagesBaseboardGen();
/// File path: assets/images/baseboard/left.png
AssetGenImage get left =>
const AssetGenImage('assets/images/baseboard/left.png');
/// File path: assets/images/baseboard/right.png
AssetGenImage get right =>
const AssetGenImage('assets/images/baseboard/right.png');
}
@ -78,10 +97,15 @@ class $AssetsImagesBaseboardGen {
class $AssetsImagesBoundaryGen {
const $AssetsImagesBoundaryGen();
/// File path: assets/images/boundary/bottom.png
AssetGenImage get bottom =>
const AssetGenImage('assets/images/boundary/bottom.png');
/// File path: assets/images/boundary/outer-bottom.png
AssetGenImage get outerBottom =>
const AssetGenImage('assets/images/boundary/outer-bottom.png');
/// File path: assets/images/boundary/outer.png
AssetGenImage get outer =>
const AssetGenImage('assets/images/boundary/outer.png');
}
@ -89,8 +113,10 @@ class $AssetsImagesBoundaryGen {
class $AssetsImagesDashGen {
const $AssetsImagesDashGen();
/// File path: assets/images/dash/animatronic.png
AssetGenImage get animatronic =>
const AssetGenImage('assets/images/dash/animatronic.png');
$AssetsImagesDashBumperGen get bumper => const $AssetsImagesDashBumperGen();
}
@ -99,10 +125,16 @@ class $AssetsImagesDinoGen {
$AssetsImagesDinoAnimatronicGen get animatronic =>
const $AssetsImagesDinoAnimatronicGen();
/// File path: assets/images/dino/bottom-wall.png
AssetGenImage get bottomWall =>
const AssetGenImage('assets/images/dino/bottom-wall.png');
/// File path: assets/images/dino/top-wall-tunnel.png
AssetGenImage get topWallTunnel =>
const AssetGenImage('assets/images/dino/top-wall-tunnel.png');
/// File path: assets/images/dino/top-wall.png
AssetGenImage get topWall =>
const AssetGenImage('assets/images/dino/top-wall.png');
}
@ -110,10 +142,15 @@ class $AssetsImagesDinoGen {
class $AssetsImagesFlapperGen {
const $AssetsImagesFlapperGen();
/// File path: assets/images/flapper/back-support.png
AssetGenImage get backSupport =>
const AssetGenImage('assets/images/flapper/back-support.png');
/// File path: assets/images/flapper/flap.png
AssetGenImage get flap =>
const AssetGenImage('assets/images/flapper/flap.png');
/// File path: assets/images/flapper/front-support.png
AssetGenImage get frontSupport =>
const AssetGenImage('assets/images/flapper/front-support.png');
}
@ -121,8 +158,11 @@ class $AssetsImagesFlapperGen {
class $AssetsImagesFlipperGen {
const $AssetsImagesFlipperGen();
/// File path: assets/images/flipper/left.png
AssetGenImage get left =>
const AssetGenImage('assets/images/flipper/left.png');
/// File path: assets/images/flipper/right.png
AssetGenImage get right =>
const AssetGenImage('assets/images/flipper/right.png');
}
@ -163,10 +203,15 @@ class $AssetsImagesKickerGen {
class $AssetsImagesLaunchRampGen {
const $AssetsImagesLaunchRampGen();
/// File path: assets/images/launch_ramp/background-railing.png
AssetGenImage get backgroundRailing =>
const AssetGenImage('assets/images/launch_ramp/background-railing.png');
/// File path: assets/images/launch_ramp/foreground-railing.png
AssetGenImage get foregroundRailing =>
const AssetGenImage('assets/images/launch_ramp/foreground-railing.png');
/// File path: assets/images/launch_ramp/ramp.png
AssetGenImage get ramp =>
const AssetGenImage('assets/images/launch_ramp/ramp.png');
}
@ -174,8 +219,11 @@ class $AssetsImagesLaunchRampGen {
class $AssetsImagesMultiballGen {
const $AssetsImagesMultiballGen();
/// File path: assets/images/multiball/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiball/dimmed.png');
/// File path: assets/images/multiball/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiball/lit.png');
}
@ -193,8 +241,11 @@ class $AssetsImagesMultiplierGen {
class $AssetsImagesPlungerGen {
const $AssetsImagesPlungerGen();
/// File path: assets/images/plunger/plunger.png
AssetGenImage get plunger =>
const AssetGenImage('assets/images/plunger/plunger.png');
/// File path: assets/images/plunger/rocket.png
AssetGenImage get rocket =>
const AssetGenImage('assets/images/plunger/rocket.png');
}
@ -202,12 +253,19 @@ class $AssetsImagesPlungerGen {
class $AssetsImagesScoreGen {
const $AssetsImagesScoreGen();
/// File path: assets/images/score/five-thousand.png
AssetGenImage get fiveThousand =>
const AssetGenImage('assets/images/score/five-thousand.png');
/// File path: assets/images/score/one-million.png
AssetGenImage get oneMillion =>
const AssetGenImage('assets/images/score/one-million.png');
/// File path: assets/images/score/twenty-thousand.png
AssetGenImage get twentyThousand =>
const AssetGenImage('assets/images/score/twenty-thousand.png');
/// File path: assets/images/score/two-hundred-thousand.png
AssetGenImage get twoHundredThousand =>
const AssetGenImage('assets/images/score/two-hundred-thousand.png');
}
@ -215,12 +273,19 @@ class $AssetsImagesScoreGen {
class $AssetsImagesSignpostGen {
const $AssetsImagesSignpostGen();
/// File path: assets/images/signpost/active1.png
AssetGenImage get active1 =>
const AssetGenImage('assets/images/signpost/active1.png');
/// File path: assets/images/signpost/active2.png
AssetGenImage get active2 =>
const AssetGenImage('assets/images/signpost/active2.png');
/// File path: assets/images/signpost/active3.png
AssetGenImage get active3 =>
const AssetGenImage('assets/images/signpost/active3.png');
/// File path: assets/images/signpost/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/signpost/inactive.png');
}
@ -228,12 +293,19 @@ class $AssetsImagesSignpostGen {
class $AssetsImagesSkillShotGen {
const $AssetsImagesSkillShotGen();
/// File path: assets/images/skill_shot/decal.png
AssetGenImage get decal =>
const AssetGenImage('assets/images/skill_shot/decal.png');
/// File path: assets/images/skill_shot/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/skill_shot/dimmed.png');
/// File path: assets/images/skill_shot/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/skill_shot/lit.png');
/// File path: assets/images/skill_shot/pin.png
AssetGenImage get pin =>
const AssetGenImage('assets/images/skill_shot/pin.png');
}
@ -241,8 +313,11 @@ class $AssetsImagesSkillShotGen {
class $AssetsImagesSlingshotGen {
const $AssetsImagesSlingshotGen();
/// File path: assets/images/slingshot/lower.png
AssetGenImage get lower =>
const AssetGenImage('assets/images/slingshot/lower.png');
/// File path: assets/images/slingshot/upper.png
AssetGenImage get upper =>
const AssetGenImage('assets/images/slingshot/upper.png');
}
@ -250,8 +325,10 @@ class $AssetsImagesSlingshotGen {
class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyGen();
/// File path: assets/images/sparky/animatronic.png
AssetGenImage get animatronic =>
const AssetGenImage('assets/images/sparky/animatronic.png');
$AssetsImagesSparkyBumperGen get bumper =>
const $AssetsImagesSparkyBumperGen();
$AssetsImagesSparkyComputerGen get computer =>
@ -272,8 +349,11 @@ class $AssetsImagesAndroidBumperGen {
class $AssetsImagesAndroidRailGen {
const $AssetsImagesAndroidRailGen();
/// File path: assets/images/android/rail/exit.png
AssetGenImage get exit =>
const AssetGenImage('assets/images/android/rail/exit.png');
/// File path: assets/images/android/rail/main.png
AssetGenImage get main =>
const AssetGenImage('assets/images/android/rail/main.png');
}
@ -283,12 +363,20 @@ class $AssetsImagesAndroidRampGen {
$AssetsImagesAndroidRampArrowGen get arrow =>
const $AssetsImagesAndroidRampArrowGen();
/// File path: assets/images/android/ramp/board-opening.png
AssetGenImage get boardOpening =>
const AssetGenImage('assets/images/android/ramp/board-opening.png');
/// File path: assets/images/android/ramp/main.png
AssetGenImage get main =>
const AssetGenImage('assets/images/android/ramp/main.png');
/// File path: assets/images/android/ramp/railing-background.png
AssetGenImage get railingBackground =>
const AssetGenImage('assets/images/android/ramp/railing-background.png');
/// File path: assets/images/android/ramp/railing-foreground.png
AssetGenImage get railingForeground =>
const AssetGenImage('assets/images/android/ramp/railing-foreground.png');
}
@ -296,10 +384,15 @@ class $AssetsImagesAndroidRampGen {
class $AssetsImagesAndroidSpaceshipGen {
const $AssetsImagesAndroidSpaceshipGen();
/// File path: assets/images/android/spaceship/animatronic.png
AssetGenImage get animatronic =>
const AssetGenImage('assets/images/android/spaceship/animatronic.png');
/// File path: assets/images/android/spaceship/light-beam.png
AssetGenImage get lightBeam =>
const AssetGenImage('assets/images/android/spaceship/light-beam.png');
/// File path: assets/images/android/spaceship/saucer.png
AssetGenImage get saucer =>
const AssetGenImage('assets/images/android/spaceship/saucer.png');
}
@ -316,8 +409,11 @@ class $AssetsImagesDashBumperGen {
class $AssetsImagesDinoAnimatronicGen {
const $AssetsImagesDinoAnimatronicGen();
/// File path: assets/images/dino/animatronic/head.png
AssetGenImage get head =>
const AssetGenImage('assets/images/dino/animatronic/head.png');
/// File path: assets/images/dino/animatronic/mouth.png
AssetGenImage get mouth =>
const AssetGenImage('assets/images/dino/animatronic/mouth.png');
}
@ -343,8 +439,11 @@ class $AssetsImagesGoogleRolloverRightGen {
class $AssetsImagesGoogleWordLetter1Gen {
const $AssetsImagesGoogleWordLetter1Gen();
/// File path: assets/images/google_word/letter1/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/google_word/letter1/dimmed.png');
/// File path: assets/images/google_word/letter1/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/google_word/letter1/lit.png');
}
@ -352,8 +451,11 @@ class $AssetsImagesGoogleWordLetter1Gen {
class $AssetsImagesGoogleWordLetter2Gen {
const $AssetsImagesGoogleWordLetter2Gen();
/// File path: assets/images/google_word/letter2/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/google_word/letter2/dimmed.png');
/// File path: assets/images/google_word/letter2/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/google_word/letter2/lit.png');
}
@ -361,8 +463,11 @@ class $AssetsImagesGoogleWordLetter2Gen {
class $AssetsImagesGoogleWordLetter3Gen {
const $AssetsImagesGoogleWordLetter3Gen();
/// File path: assets/images/google_word/letter3/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/google_word/letter3/dimmed.png');
/// File path: assets/images/google_word/letter3/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/google_word/letter3/lit.png');
}
@ -370,8 +475,11 @@ class $AssetsImagesGoogleWordLetter3Gen {
class $AssetsImagesGoogleWordLetter4Gen {
const $AssetsImagesGoogleWordLetter4Gen();
/// File path: assets/images/google_word/letter4/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/google_word/letter4/dimmed.png');
/// File path: assets/images/google_word/letter4/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/google_word/letter4/lit.png');
}
@ -379,8 +487,11 @@ class $AssetsImagesGoogleWordLetter4Gen {
class $AssetsImagesGoogleWordLetter5Gen {
const $AssetsImagesGoogleWordLetter5Gen();
/// File path: assets/images/google_word/letter5/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/google_word/letter5/dimmed.png');
/// File path: assets/images/google_word/letter5/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/google_word/letter5/lit.png');
}
@ -388,8 +499,11 @@ class $AssetsImagesGoogleWordLetter5Gen {
class $AssetsImagesGoogleWordLetter6Gen {
const $AssetsImagesGoogleWordLetter6Gen();
/// File path: assets/images/google_word/letter6/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/google_word/letter6/dimmed.png');
/// File path: assets/images/google_word/letter6/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/google_word/letter6/lit.png');
}
@ -397,8 +511,11 @@ class $AssetsImagesGoogleWordLetter6Gen {
class $AssetsImagesKickerLeftGen {
const $AssetsImagesKickerLeftGen();
/// File path: assets/images/kicker/left/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/kicker/left/dimmed.png');
/// File path: assets/images/kicker/left/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/kicker/left/lit.png');
}
@ -406,8 +523,11 @@ class $AssetsImagesKickerLeftGen {
class $AssetsImagesKickerRightGen {
const $AssetsImagesKickerRightGen();
/// File path: assets/images/kicker/right/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/kicker/right/dimmed.png');
/// File path: assets/images/kicker/right/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/kicker/right/lit.png');
}
@ -415,8 +535,11 @@ class $AssetsImagesKickerRightGen {
class $AssetsImagesMultiplierX2Gen {
const $AssetsImagesMultiplierX2Gen();
/// File path: assets/images/multiplier/x2/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiplier/x2/dimmed.png');
/// File path: assets/images/multiplier/x2/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiplier/x2/lit.png');
}
@ -424,8 +547,11 @@ class $AssetsImagesMultiplierX2Gen {
class $AssetsImagesMultiplierX3Gen {
const $AssetsImagesMultiplierX3Gen();
/// File path: assets/images/multiplier/x3/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiplier/x3/dimmed.png');
/// File path: assets/images/multiplier/x3/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiplier/x3/lit.png');
}
@ -433,8 +559,11 @@ class $AssetsImagesMultiplierX3Gen {
class $AssetsImagesMultiplierX4Gen {
const $AssetsImagesMultiplierX4Gen();
/// File path: assets/images/multiplier/x4/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiplier/x4/dimmed.png');
/// File path: assets/images/multiplier/x4/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiplier/x4/lit.png');
}
@ -442,8 +571,11 @@ class $AssetsImagesMultiplierX4Gen {
class $AssetsImagesMultiplierX5Gen {
const $AssetsImagesMultiplierX5Gen();
/// File path: assets/images/multiplier/x5/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiplier/x5/dimmed.png');
/// File path: assets/images/multiplier/x5/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiplier/x5/lit.png');
}
@ -451,8 +583,11 @@ class $AssetsImagesMultiplierX5Gen {
class $AssetsImagesMultiplierX6Gen {
const $AssetsImagesMultiplierX6Gen();
/// File path: assets/images/multiplier/x6/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/multiplier/x6/dimmed.png');
/// File path: assets/images/multiplier/x6/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/multiplier/x6/lit.png');
}
@ -468,10 +603,15 @@ class $AssetsImagesSparkyBumperGen {
class $AssetsImagesSparkyComputerGen {
const $AssetsImagesSparkyComputerGen();
/// File path: assets/images/sparky/computer/base.png
AssetGenImage get base =>
const AssetGenImage('assets/images/sparky/computer/base.png');
/// File path: assets/images/sparky/computer/glow.png
AssetGenImage get glow =>
const AssetGenImage('assets/images/sparky/computer/glow.png');
/// File path: assets/images/sparky/computer/top.png
AssetGenImage get top =>
const AssetGenImage('assets/images/sparky/computer/top.png');
}
@ -479,8 +619,11 @@ class $AssetsImagesSparkyComputerGen {
class $AssetsImagesAndroidBumperAGen {
const $AssetsImagesAndroidBumperAGen();
/// File path: assets/images/android/bumper/a/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/android/bumper/a/dimmed.png');
/// File path: assets/images/android/bumper/a/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/android/bumper/a/lit.png');
}
@ -488,8 +631,11 @@ class $AssetsImagesAndroidBumperAGen {
class $AssetsImagesAndroidBumperBGen {
const $AssetsImagesAndroidBumperBGen();
/// File path: assets/images/android/bumper/b/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/android/bumper/b/dimmed.png');
/// File path: assets/images/android/bumper/b/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/android/bumper/b/lit.png');
}
@ -497,8 +643,11 @@ class $AssetsImagesAndroidBumperBGen {
class $AssetsImagesAndroidBumperCowGen {
const $AssetsImagesAndroidBumperCowGen();
/// File path: assets/images/android/bumper/cow/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/android/bumper/cow/dimmed.png');
/// File path: assets/images/android/bumper/cow/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/android/bumper/cow/lit.png');
}
@ -506,16 +655,27 @@ class $AssetsImagesAndroidBumperCowGen {
class $AssetsImagesAndroidRampArrowGen {
const $AssetsImagesAndroidRampArrowGen();
/// File path: assets/images/android/ramp/arrow/active1.png
AssetGenImage get active1 =>
const AssetGenImage('assets/images/android/ramp/arrow/active1.png');
/// File path: assets/images/android/ramp/arrow/active2.png
AssetGenImage get active2 =>
const AssetGenImage('assets/images/android/ramp/arrow/active2.png');
/// File path: assets/images/android/ramp/arrow/active3.png
AssetGenImage get active3 =>
const AssetGenImage('assets/images/android/ramp/arrow/active3.png');
/// File path: assets/images/android/ramp/arrow/active4.png
AssetGenImage get active4 =>
const AssetGenImage('assets/images/android/ramp/arrow/active4.png');
/// File path: assets/images/android/ramp/arrow/active5.png
AssetGenImage get active5 =>
const AssetGenImage('assets/images/android/ramp/arrow/active5.png');
/// File path: assets/images/android/ramp/arrow/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/android/ramp/arrow/inactive.png');
}
@ -523,8 +683,11 @@ class $AssetsImagesAndroidRampArrowGen {
class $AssetsImagesDashBumperAGen {
const $AssetsImagesDashBumperAGen();
/// File path: assets/images/dash/bumper/a/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash/bumper/a/active.png');
/// File path: assets/images/dash/bumper/a/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash/bumper/a/inactive.png');
}
@ -532,8 +695,11 @@ class $AssetsImagesDashBumperAGen {
class $AssetsImagesDashBumperBGen {
const $AssetsImagesDashBumperBGen();
/// File path: assets/images/dash/bumper/b/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash/bumper/b/active.png');
/// File path: assets/images/dash/bumper/b/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash/bumper/b/inactive.png');
}
@ -541,8 +707,11 @@ class $AssetsImagesDashBumperBGen {
class $AssetsImagesDashBumperMainGen {
const $AssetsImagesDashBumperMainGen();
/// File path: assets/images/dash/bumper/main/active.png
AssetGenImage get active =>
const AssetGenImage('assets/images/dash/bumper/main/active.png');
/// File path: assets/images/dash/bumper/main/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/dash/bumper/main/inactive.png');
}
@ -550,8 +719,11 @@ class $AssetsImagesDashBumperMainGen {
class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen();
/// File path: assets/images/sparky/bumper/a/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/sparky/bumper/a/dimmed.png');
/// File path: assets/images/sparky/bumper/a/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/sparky/bumper/a/lit.png');
}
@ -559,8 +731,11 @@ class $AssetsImagesSparkyBumperAGen {
class $AssetsImagesSparkyBumperBGen {
const $AssetsImagesSparkyBumperBGen();
/// File path: assets/images/sparky/bumper/b/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/sparky/bumper/b/dimmed.png');
/// File path: assets/images/sparky/bumper/b/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/sparky/bumper/b/lit.png');
}
@ -568,8 +743,11 @@ class $AssetsImagesSparkyBumperBGen {
class $AssetsImagesSparkyBumperCGen {
const $AssetsImagesSparkyBumperCGen();
/// File path: assets/images/sparky/bumper/c/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/sparky/bumper/c/dimmed.png');
/// File path: assets/images/sparky/bumper/c/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/sparky/bumper/c/lit.png');
}

@ -3,9 +3,14 @@
/// FlutterGen
/// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
class FontFamily {
FontFamily._();
/// Font family: PixeloidMono
static const String pixeloidMono = 'PixeloidMono';
/// Font family: PixeloidSans
static const String pixeloidSans = 'PixeloidSans';
}

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

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

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

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

@ -10,10 +10,9 @@ export 'boundaries.dart';
export 'camera_zoom.dart';
export 'chrome_dino/chrome_dino.dart';
export 'dash_animatronic.dart';
export 'dash_nest_bumper/dash_nest_bumper.dart';
export 'dash_bumper/dash_bumper.dart';
export 'dino_walls.dart';
export 'error_component.dart';
export 'fire_effect.dart';
export 'flapper/flapper.dart';
export 'flipper/flipper.dart';
export 'google_letter.dart';
@ -28,7 +27,7 @@ export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart';
export 'plunger.dart';
export 'rocket.dart';
export 'score_component.dart';
export 'score_component/score_component.dart';
export 'shapes/shapes.dart';
export 'signpost/signpost.dart';
export 'skill_shot/skill_shot.dart';

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

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

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

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

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

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

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

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

@ -6,8 +6,6 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Joints the [Flipper] to allow pivoting around one end.
class FlipperJointingBehavior extends Component
with ParentIsA<Flipper>, HasGameRef {
late final RevoluteJoint _joint;
@override
Future<void> onLoad() async {
await super.onLoad();
@ -19,15 +17,7 @@ class FlipperJointingBehavior extends Component
flipper: parent,
anchor: anchor,
);
_joint = _FlipperJoint(jointDef);
parent.world.createJoint(_joint);
}
@override
void onMount() {
gameRef.ready().whenComplete(
() => parent.body.joints.whereType<_FlipperJoint>().first.unlock(),
);
parent.world.createJoint(RevoluteJoint(jointDef));
}
}
@ -50,7 +40,7 @@ class _FlipperAnchor extends JointAnchor {
}
/// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve a potivoting
/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve a pivoting
/// motion.
/// {@endtemplate}
class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
@ -58,46 +48,15 @@ class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
_FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required _FlipperAnchor anchor,
}) : side = flipper.side {
enableLimit = true;
}) {
initialize(
flipper.body,
anchor.body,
flipper.body.position + anchor.body.position,
);
}
final BoardSide side;
}
/// {@template flipper_joint}
/// [RevoluteJoint] that controls the pivoting motion of a [Flipper].
/// {@endtemplate}
class _FlipperJoint extends RevoluteJoint {
/// {@macro flipper_joint}
_FlipperJoint(_FlipperAnchorRevoluteJointDef def)
: side = def.side,
super(def) {
lock();
}
/// Half the angle of the arc motion.
static const _halfSweepingAngle = 0.611;
final BoardSide side;
/// Locks the [Flipper] to its resting position.
///
/// The joint is locked when initialized in order to force the [Flipper]
/// at its resting position.
void lock() {
final angle = _halfSweepingAngle * side.direction;
setLimits(angle, angle);
}
/// Unlocks the [Flipper] from its resting position.
void unlock() {
const angle = _halfSweepingAngle;
setLimits(-angle, angle);
enableLimit = true;
upperAngle = 0.611;
lowerAngle = -upperAngle;
}
}

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

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

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

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

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

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

@ -27,11 +27,6 @@ class SpaceshipRamp extends Component {
required this.bloc,
}) : super(
children: [
RampScoringSensor(
children: [
RampBallAscendingContactBehavior(),
],
)..initialPosition = Vector2(1.7, -20.4),
_SpaceshipRampOpening(
outsideLayer: Layer.spaceship,
outsidePriority: ZIndexes.ballOnSpaceship,
@ -40,7 +35,7 @@ class SpaceshipRamp extends Component {
..initialPosition = Vector2(-13.7, -18.6)
..layer = Layer.spaceshipEntranceRamp,
_SpaceshipRampBackground(),
_SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5),
SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5),
_SpaceshipRampForegroundRailing(),
SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5),
_SpaceshipRampBackgroundRailingSpriteComponent(),
@ -246,13 +241,14 @@ extension on SpaceshipRampArrowSpriteState {
}
}
class _SpaceshipRampBoardOpening extends BodyComponent
with Layered, ZIndex, InitialPosition {
_SpaceshipRampBoardOpening()
class SpaceshipRampBoardOpening extends BodyComponent
with Layered, ZIndex, InitialPosition, ParentIsA<SpaceshipRamp> {
SpaceshipRampBoardOpening()
: super(
renderBody: false,
children: [
_SpaceshipRampBoardOpeningSpriteComponent(),
RampBallAscendingContactBehavior()..applyTo(['inside']),
LayerContactBehavior(layer: Layer.spaceshipEntranceRamp)
..applyTo(['inside']),
LayerContactBehavior(
@ -271,6 +267,13 @@ class _SpaceshipRampBoardOpening extends BodyComponent
layer = Layer.opening;
}
/// Creates a [SpaceshipRampBoardOpening] without any children.
///
/// This can be used for testing [SpaceshipRampBoardOpening]'s behaviors in
/// isolation.
@visibleForTesting
SpaceshipRampBoardOpening.test();
List<FixtureDef> _createFixtureDefs() {
final topEdge = EdgeShape()
..set(
@ -495,46 +498,3 @@ class _SpaceshipRampOpening extends LayerSensor {
);
}
}
/// {@template ramp_scoring_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SpaceshipRamp].
/// {@endtemplate}
class RampScoringSensor extends BodyComponent
with ParentIsA<SpaceshipRamp>, InitialPosition, Layered {
/// {@macro ramp_scoring_sensor}
RampScoringSensor({
Iterable<Component>? children,
}) : super(
children: children,
renderBody: false,
) {
layer = Layer.spaceshipEntranceRamp;
}
/// Creates a [RampScoringSensor] without any children.
@visibleForTesting
RampScoringSensor.test();
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
2.6,
.5,
initialPosition,
-5 * math.pi / 180,
);
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

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

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

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

@ -1,4 +1,4 @@
# Sandbox
# Pinball Components Sandbox
![coverage][coverage_badge]
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
@ -6,7 +6,7 @@
Generated by the [Very Good CLI][very_good_cli_link] 🤖
A sanbox application where components are showcased and developed in an isolated way
A sandbox application where components are showcased and developed in an isolated way
---

@ -1,48 +0,0 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class FireEffectGame extends LineGame {
static const description = '''
Shows how the FireEffect renders.
- Drag a line to define the trail direction.
''';
@override
void onLine(Vector2 line) {
add(_EffectEmitter(line));
}
}
class _EffectEmitter extends Component {
_EffectEmitter(this.line) {
_direction = line.normalized();
_force = line.length;
}
static const _timerLimit = 2.0;
var _timer = _timerLimit;
final Vector2 line;
late Vector2 _direction;
late double _force;
@override
void update(double dt) {
super.update(dt);
if (_timer > 0) {
add(
FireEffect(
burstPower: (_timer / _timerLimit) * _force,
direction: _direction,
),
);
_timer -= dt;
} else {
removeFromParent();
}
}
}

@ -1,16 +1,9 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/effects/camera_zoom_game.dart';
import 'package:sandbox/stories/effects/fire_effect_game.dart';
void addEffectsStories(Dashbook dashbook) {
dashbook.storiesOf('Effects')
..addGame(
title: 'Fire',
description: FireEffectGame.description,
gameBuilder: (_) => FireEffectGame(),
)
..addGame(
dashbook.storiesOf('Effects').addGame(
title: 'CameraZoom',
description: CameraZoomGame.description,
gameBuilder: (_) => CameraZoomGame(),

@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SmallDashNestBumperAGame extends BallGame {
SmallDashNestBumperAGame()
class DashBumperAGame extends BallGame {
DashBumperAGame()
: super(
imagesFileNames: [
Assets.images.dash.bumper.a.active.keyName,
@ -14,7 +14,7 @@ class SmallDashNestBumperAGame extends BallGame {
);
static const description = '''
Shows how a SmallDashNestBumper ("a") is rendered.
Shows how the "a" DashBumper is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@ -24,7 +24,7 @@ class SmallDashNestBumperAGame extends BallGame {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(DashNestBumper.a()..priority = 1);
await add(DashBumper.a()..priority = 1);
await traceAllBodies();
}
}

@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SmallDashNestBumperBGame extends BallGame {
SmallDashNestBumperBGame()
class DashBumperBGame extends BallGame {
DashBumperBGame()
: super(
imagesFileNames: [
Assets.images.dash.bumper.b.active.keyName,
@ -14,7 +14,7 @@ class SmallDashNestBumperBGame extends BallGame {
);
static const description = '''
Shows how a SmallDashNestBumper ("b") is rendered.
Shows how the "b" DashBumper is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@ -24,7 +24,7 @@ class SmallDashNestBumperBGame extends BallGame {
await super.onLoad();
camera.followVector2(Vector2.zero());
await add(DashNestBumper.b()..priority = 1);
await add(DashBumper.b()..priority = 1);
await traceAllBodies();
}
}

@ -4,8 +4,8 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class BigDashNestBumperGame extends BallGame {
BigDashNestBumperGame()
class DashBumperMainGame extends BallGame {
DashBumperMainGame()
: super(
imagesFileNames: [
Assets.images.dash.bumper.main.active.keyName,
@ -14,7 +14,7 @@ class BigDashNestBumperGame extends BallGame {
);
static const description = '''
Shows how a BigDashNestBumper is rendered.
Shows how the "main" DashBumper is rendered.
- Activate the "trace" parameter to overlay the body.
''';
@ -25,7 +25,7 @@ class BigDashNestBumperGame extends BallGame {
camera.followVector2(Vector2.zero());
await add(
DashNestBumper.main()..priority = 1,
DashBumper.main()..priority = 1,
);
await traceAllBodies();
}

@ -1,9 +1,9 @@
import 'package:dashbook/dashbook.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/flutter_forest/big_dash_nest_bumper_game.dart';
import 'package:sandbox/stories/flutter_forest/dash_bumper_a_game.dart';
import 'package:sandbox/stories/flutter_forest/dash_bumper_b_game.dart';
import 'package:sandbox/stories/flutter_forest/dash_bumper_main_game.dart';
import 'package:sandbox/stories/flutter_forest/signpost_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_a_game.dart';
import 'package:sandbox/stories/flutter_forest/small_dash_nest_bumper_b_game.dart';
void addFlutterForestStories(Dashbook dashbook) {
dashbook.storiesOf('Flutter Forest')
@ -13,18 +13,18 @@ void addFlutterForestStories(Dashbook dashbook) {
gameBuilder: (_) => SignpostGame(),
)
..addGame(
title: 'Big Dash Nest Bumper',
description: BigDashNestBumperGame.description,
gameBuilder: (_) => BigDashNestBumperGame(),
title: 'Main Dash Bumper',
description: DashBumperMainGame.description,
gameBuilder: (_) => DashBumperMainGame(),
)
..addGame(
title: 'Small Dash Nest Bumper A',
description: SmallDashNestBumperAGame.description,
gameBuilder: (_) => SmallDashNestBumperAGame(),
title: 'Dash Bumper A',
description: DashBumperAGame.description,
gameBuilder: (_) => DashBumperAGame(),
)
..addGame(
title: 'Small Dash Nest Bumper B',
description: SmallDashNestBumperBGame.description,
gameBuilder: (_) => SmallDashNestBumperBGame(),
title: 'Dash Bumper B',
description: DashBumperBGame.description,
gameBuilder: (_) => DashBumperBGame(),
);
}

@ -1,7 +1,7 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@ -21,9 +21,18 @@ void main() {
Assets.images.android.spaceship.lightBeam.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
late AndroidSpaceshipCubit bloc;
setUp(() {
bloc = _MockAndroidSpaceshipCubit();
});
flameTester.test('loads correctly', (game) async {
final component = AndroidSpaceship(position: Vector2.zero());
final component =
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>.value(
value: bloc,
children: [AndroidSpaceship(position: Vector2.zero())],
);
await game.ensureAdd(component);
expect(game.contains(component), isTrue);
});
@ -33,7 +42,13 @@ void main() {
setUp: (game, tester) async {
await game.images.loadAll(assets);
final canvas = ZCanvasComponent(
children: [
FlameBlocProvider<AndroidSpaceshipCubit,
AndroidSpaceshipState>.value(
value: bloc,
children: [AndroidSpaceship(position: Vector2.zero())],
),
],
);
await game.ensureAdd(canvas);
game.camera.followVector2(Vector2.zero());
@ -70,28 +85,16 @@ void main() {
},
);
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockAndroidSpaceshipCubit();
whenListen(
bloc,
const Stream<AndroidSpaceshipState>.empty(),
initialState: AndroidSpaceshipState.withoutBonus,
);
when(bloc.close).thenAnswer((_) async {});
final androidSpaceship = AndroidSpaceship.test(bloc: bloc);
await game.ensureAdd(androidSpaceship);
game.remove(androidSpaceship);
await game.ready();
verify(bloc.close).called(1);
});
flameTester.test(
'AndroidSpaceshipEntrance has an '
'AndroidSpaceshipEntranceBallContactBehavior', (game) async {
final androidSpaceship = AndroidSpaceship(position: Vector2.zero());
await game.ensureAdd(androidSpaceship);
final provider =
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>.value(
value: bloc,
children: [androidSpaceship],
);
await game.ensureAdd(provider);
final androidSpaceshipEntrance =
androidSpaceship.firstChild<AndroidSpaceshipEntrance>();

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
@ -43,16 +44,19 @@ void main() {
);
final entrance = AndroidSpaceshipEntrance();
final androidSpaceship = AndroidSpaceship.test(
bloc: bloc,
children: [entrance],
final androidSpaceship = FlameBlocProvider<AndroidSpaceshipCubit,
AndroidSpaceshipState>.value(
value: bloc,
children: [
AndroidSpaceship.test(children: [entrance])
],
);
await entrance.add(behavior);
await game.ensureAdd(androidSpaceship);
behavior.beginContact(_MockBall(), _MockContact());
verify(androidSpaceship.bloc.onBallEntered).called(1);
verify(bloc.onBallEntered).called(1);
},
);
},

@ -10,8 +10,9 @@ import '../../../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset]));
final flameTester = FlameTester(
() => TestGame([theme.Assets.images.dash.ball.keyName]),
);
group('BallScalingBehavior', () {
test('can be instantiated', () {

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

Loading…
Cancel
Save