Merge branch 'main' into refactor/sparky-behaviors

pull/268/head
Allison Ryan 3 years ago
commit 934a8db423

@ -0,0 +1,22 @@
name: authentication_repository
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
paths:
- "packages/authentication_repository/**"
- ".github/workflows/authentication_repository.yaml"
pull_request:
paths:
- "packages/authentication_repository/**"
- ".github/workflows/authentication_repository.yaml"
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
working_directory: packages/authentication_repository

@ -7,6 +7,7 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'package:authentication_repository/authentication_repository.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -15,16 +16,20 @@ import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
class App extends StatelessWidget { class App extends StatelessWidget {
const App({ const App({
Key? key, Key? key,
required AuthenticationRepository authenticationRepository,
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required PinballAudio pinballAudio, required PinballAudio pinballAudio,
}) : _leaderboardRepository = leaderboardRepository, }) : _authenticationRepository = authenticationRepository,
_leaderboardRepository = leaderboardRepository,
_pinballAudio = pinballAudio, _pinballAudio = pinballAudio,
super(key: key); super(key: key);
final AuthenticationRepository _authenticationRepository;
final LeaderboardRepository _leaderboardRepository; final LeaderboardRepository _leaderboardRepository;
final PinballAudio _pinballAudio; final PinballAudio _pinballAudio;
@ -32,19 +37,21 @@ class App extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiRepositoryProvider( return MultiRepositoryProvider(
providers: [ providers: [
RepositoryProvider.value(value: _authenticationRepository),
RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _pinballAudio), RepositoryProvider.value(value: _pinballAudio),
], ],
child: BlocProvider( child: BlocProvider(
create: (context) => CharacterThemeCubit(), create: (context) => CharacterThemeCubit(),
child: const MaterialApp( child: MaterialApp(
title: 'I/O Pinball', title: 'I/O Pinball',
localizationsDelegates: [ theme: PinballTheme.standard,
localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
], ],
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: PinballGamePage(), home: const PinballGamePage(),
), ),
), ),
); );

@ -12,6 +12,7 @@ import 'dart:developer';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class AppBlocObserver extends BlocObserver { class AppBlocObserver extends BlocObserver {
@ -28,9 +29,12 @@ class AppBlocObserver extends BlocObserver {
} }
} }
Future<void> bootstrap( typedef BootstrapBuilder = Future<Widget> Function(
Future<Widget> Function(FirebaseFirestore firestore) builder, FirebaseFirestore firestore,
) async { FirebaseAuth firebaseAuth,
);
Future<void> bootstrap(BootstrapBuilder builder) async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (details) { FlutterError.onError = (details) {
log(details.exceptionAsString(), stackTrace: details.stack); log(details.exceptionAsString(), stackTrace: details.stack);
@ -39,7 +43,12 @@ Future<void> bootstrap(
await runZonedGuarded( await runZonedGuarded(
() async { () async {
await BlocOverrides.runZoned( await BlocOverrides.runZoned(
() async => runApp(await builder(FirebaseFirestore.instance)), () async => runApp(
await builder(
FirebaseFirestore.instance,
FirebaseAuth.instance,
),
),
blocObserver: AppBlocObserver(), blocObserver: AppBlocObserver(),
); );
}, },

@ -0,0 +1,76 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template footer}
/// Footer widget with links to the main tech stack.
/// {@endtemplate}
class Footer extends StatelessWidget {
/// {@macro footer}
const Footer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
_MadeWithFlutterAndFirebase(),
_GoogleIO(),
],
),
);
}
}
class _GoogleIO extends StatelessWidget {
const _GoogleIO({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return Text(
l10n.footerGoogleIOText,
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
);
}
}
class _MadeWithFlutterAndFirebase extends StatelessWidget {
const _MadeWithFlutterAndFirebase({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: l10n.footerMadeWithText,
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
children: <TextSpan>[
TextSpan(
text: l10n.footerFlutterLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink('https://flutter.dev'),
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ' & '),
TextSpan(
text: l10n.footerFirebaseLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink('https://firebase.google.com'),
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
],
),
);
}
}

@ -6,8 +6,8 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template android_acres} /// {@template android_acres}
/// Area positioned on the left side of the board containing the [Spaceship], /// Area positioned on the left side of the board containing the
/// [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s. /// [AndroidSpaceship], [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s.
/// {@endtemplate} /// {@endtemplate}
class AndroidAcres extends Blueprint { class AndroidAcres extends Blueprint {
/// {@macro android_acres} /// {@macro android_acres}
@ -23,7 +23,7 @@ class AndroidAcres extends Blueprint {
children: [ children: [
ScoringBehavior(points: 20000), ScoringBehavior(points: 20000),
], ],
)..initialPosition = Vector2(-32.6, -9.2), )..initialPosition = Vector2(-32.8, -9.2),
AndroidBumper.cow( AndroidBumper.cow(
children: [ children: [
ScoringBehavior(points: 20), ScoringBehavior(points: 20),
@ -32,7 +32,7 @@ class AndroidAcres extends Blueprint {
], ],
blueprints: [ blueprints: [
SpaceshipRamp(), SpaceshipRamp(),
Spaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
SpaceshipRail(), SpaceshipRail(),
], ],
); );

@ -16,6 +16,7 @@ class BottomGroup extends Component {
_BottomGroupSide(side: BoardSide.right), _BottomGroupSide(side: BoardSide.right),
_BottomGroupSide(side: BoardSide.left), _BottomGroupSide(side: BoardSide.left),
], ],
priority: RenderPriority.bottomGroup,
); );
} }
@ -28,8 +29,7 @@ class _BottomGroupSide extends Component {
/// {@macro bottom_group_side} /// {@macro bottom_group_side}
_BottomGroupSide({ _BottomGroupSide({
required BoardSide side, required BoardSide side,
}) : _side = side, }) : _side = side;
super(priority: RenderPriority.bottomGroup);
final BoardSide _side; final BoardSide _side;

@ -13,6 +13,7 @@ extension PinballGameAssetsX on PinballGame {
const dinoTheme = DinoTheme(); const dinoTheme = DinoTheme();
return [ return [
images.load(components.Assets.images.boardBackground.keyName),
images.load(components.Assets.images.ball.ball.keyName), images.load(components.Assets.images.ball.ball.keyName),
images.load(components.Assets.images.ball.flameEffect.keyName), images.load(components.Assets.images.ball.flameEffect.keyName),
images.load(components.Assets.images.signpost.inactive.keyName), images.load(components.Assets.images.signpost.inactive.keyName),
@ -50,41 +51,42 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.boundary.bottom.keyName), images.load(components.Assets.images.boundary.bottom.keyName),
images.load(components.Assets.images.boundary.outer.keyName), images.load(components.Assets.images.boundary.outer.keyName),
images.load(components.Assets.images.boundary.outerBottom.keyName), images.load(components.Assets.images.boundary.outerBottom.keyName),
images.load(components.Assets.images.spaceship.saucer.keyName), images.load(components.Assets.images.android.spaceship.saucer.keyName),
images.load(components.Assets.images.spaceship.bridge.keyName), images
images.load(components.Assets.images.spaceship.ramp.boardOpening.keyName), .load(components.Assets.images.android.spaceship.animatronic.keyName),
images.load(components.Assets.images.android.spaceship.lightBeam.keyName),
images.load(components.Assets.images.android.ramp.boardOpening.keyName),
images.load( images.load(
components.Assets.images.spaceship.ramp.railingForeground.keyName, components.Assets.images.android.ramp.railingForeground.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.railingBackground.keyName, components.Assets.images.android.ramp.railingBackground.keyName,
), ),
images.load(components.Assets.images.spaceship.ramp.main.keyName), images.load(components.Assets.images.android.ramp.main.keyName),
images images.load(components.Assets.images.android.ramp.arrow.inactive.keyName),
.load(components.Assets.images.spaceship.ramp.arrow.inactive.keyName),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active1.keyName, components.Assets.images.android.ramp.arrow.active1.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active2.keyName, components.Assets.images.android.ramp.arrow.active2.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active3.keyName, components.Assets.images.android.ramp.arrow.active3.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active4.keyName, components.Assets.images.android.ramp.arrow.active4.keyName,
), ),
images.load( images.load(
components.Assets.images.spaceship.ramp.arrow.active5.keyName, components.Assets.images.android.ramp.arrow.active5.keyName,
), ),
images.load(components.Assets.images.spaceship.rail.main.keyName), images.load(components.Assets.images.android.rail.main.keyName),
images.load(components.Assets.images.spaceship.rail.exit.keyName), images.load(components.Assets.images.android.rail.exit.keyName),
images.load(components.Assets.images.androidBumper.a.lit.keyName), images.load(components.Assets.images.android.bumper.a.lit.keyName),
images.load(components.Assets.images.androidBumper.a.dimmed.keyName), images.load(components.Assets.images.android.bumper.a.dimmed.keyName),
images.load(components.Assets.images.androidBumper.b.lit.keyName), images.load(components.Assets.images.android.bumper.b.lit.keyName),
images.load(components.Assets.images.androidBumper.b.dimmed.keyName), images.load(components.Assets.images.android.bumper.b.dimmed.keyName),
images.load(components.Assets.images.androidBumper.cow.lit.keyName), images.load(components.Assets.images.android.bumper.cow.lit.keyName),
images.load(components.Assets.images.androidBumper.cow.dimmed.keyName), images.load(components.Assets.images.android.bumper.cow.dimmed.keyName),
images.load(components.Assets.images.sparky.computer.top.keyName), images.load(components.Assets.images.sparky.computer.top.keyName),
images.load(components.Assets.images.sparky.computer.base.keyName), images.load(components.Assets.images.sparky.computer.base.keyName),
images.load(components.Assets.images.sparky.computer.glow.keyName), images.load(components.Assets.images.sparky.computer.glow.keyName),

@ -9,11 +9,10 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends Forge2DGame class PinballGame extends Forge2DGame
with with
@ -46,10 +45,10 @@ class PinballGame extends Forge2DGame
unawaited(add(gameFlowController = GameFlowController(this))); unawaited(add(gameFlowController = GameFlowController(this)));
unawaited(add(CameraController(this))); unawaited(add(CameraController(this)));
unawaited(add(Backboard.waiting(position: Vector2(0, -88)))); unawaited(add(Backboard.waiting(position: Vector2(0, -88))));
await add(BoardBackgroundSpriteComponent());
await add(Drain()); await add(Drain());
await add(BottomGroup()); await add(BottomGroup());
unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp()));
final launcher = Launcher(); final launcher = Launcher();
unawaited(addFromBlueprint(launcher)); unawaited(addFromBlueprint(launcher));
@ -68,7 +67,7 @@ class PinballGame extends Forge2DGame
), ),
); );
controller.attachTo(launcher.components.whereType<Plunger>().first); controller.attachTo(launcher.components.whereType<Plunger>().single);
await super.onLoad(); await super.onLoad();
} }
@ -186,26 +185,25 @@ class DebugPinballGame extends PinballGame with FPSCounter {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await _loadBackground();
await add(_DebugInformation()); await add(_DebugInformation());
} }
// TODO(alestiago): Move to PinballGame once we have the real background // TODO(allisonryan0002): Remove after google letters have been correctly
// component. // placed.
Future<void> _loadBackground() async { // Future<void> _loadBackground() async {
final sprite = await loadSprite( // final sprite = await loadSprite(
Assets.images.components.background.path, // Assets.images.components.background.path,
); // );
final spriteComponent = SpriteComponent( // final spriteComponent = SpriteComponent(
sprite: sprite, // sprite: sprite,
size: Vector2(120, 160), // size: Vector2(120, 160),
anchor: Anchor.center, // anchor: Anchor.center,
) // )
..position = Vector2(0, -7.8) // ..position = Vector2(0, -7.8)
..priority = RenderPriority.background; // ..priority = RenderPriority.boardBackground;
await add(spriteComponent); // await add(spriteComponent);
} // }
@override @override
void onTapUp(TapUpInfo info) { void onTapUp(TapUpInfo info) {

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart'; import 'package:pinball/gen/gen.dart';
import 'package:pinball/theme/app_colors.dart'; import 'package:pinball_ui/pinball_ui.dart';
/// {@template game_hud} /// {@template game_hud}
/// Overlay on the [PinballGame]. /// Overlay on the [PinballGame].
@ -72,7 +72,7 @@ class _ScoreViewDecoration extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: radius, borderRadius: radius,
border: Border.all( border: Border.all(
color: AppColors.white, color: PinballColors.white,
width: borderWidth, width: borderWidth,
), ),
image: DecorationImage( image: DecorationImage(

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart'; import 'package:pinball_ui/pinball_ui.dart';
/// {@template round_count_display} /// {@template round_count_display}
/// Colored square indicating if a round is available. /// Colored square indicating if a round is available.
@ -20,9 +20,7 @@ class RoundCountDisplay extends StatelessWidget {
children: [ children: [
Text( Text(
l10n.rounds, l10n.rounds,
style: AppTextStyle.subtitle1.copyWith( style: Theme.of(context).textTheme.subtitle1,
color: AppColors.yellow,
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Row( Row(
@ -53,9 +51,9 @@ class RoundIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color = isActive ? AppColors.yellow : AppColors.yellow.withAlpha(128); final color =
isActive ? PinballColors.yellow : PinballColors.yellow.withAlpha(128);
const size = 8.0; const size = 8.0;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Container( child: Container(

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template score_view} /// {@template score_view}
@ -38,9 +37,7 @@ class _GameOver extends StatelessWidget {
return Text( return Text(
l10n.gameOver, l10n.gameOver,
style: AppTextStyle.headline1.copyWith( style: Theme.of(context).textTheme.headline1,
color: AppColors.white,
),
); );
} }
} }
@ -58,9 +55,7 @@ class _ScoreDisplay extends StatelessWidget {
children: [ children: [
Text( Text(
l10n.score.toLowerCase(), l10n.score.toLowerCase(),
style: AppTextStyle.subtitle1.copyWith( style: Theme.of(context).textTheme.subtitle1,
color: AppColors.yellow,
),
), ),
const _ScoreText(), const _ScoreText(),
const RoundCountDisplay(), const RoundCountDisplay(),
@ -78,9 +73,7 @@ class _ScoreText extends StatelessWidget {
return Text( return Text(
score.formatScore(), score.formatScore(),
style: AppTextStyle.headline1.copyWith( style: Theme.of(context).textTheme.headline1,
color: AppColors.white,
),
); );
} }
} }

@ -107,5 +107,21 @@
"rounds": "Ball Ct:", "rounds": "Ball Ct:",
"@rounds": { "@rounds": {
"description": "Text displayed on the scoreboard widget to indicate rounds left" "description": "Text displayed on the scoreboard widget to indicate rounds left"
},
"footerMadeWithText": "Made with ",
"@footerMadeWithText": {
"description": "Text shown on the footer which mentions technologies used to build the app."
},
"footerFlutterLinkText": "Flutter",
"@footerFlutterLinkText": {
"description": "Text on the link shown on the footer which navigates to the Flutter page"
},
"footerFirebaseLinkText": "Firebase",
"@footerFirebaseLinkText": {
"description": "Text on the link shown on the footer which navigates to the Firebase page"
},
"footerGoogleIOText": "Google I/O",
"@footerGoogleIOText": {
"description": "Text shown on the footer which mentions Google I/O"
} }
} }

@ -1,15 +0,0 @@
{
"@@locale": "es",
"play": "Jugar",
"@play": {
"description": "Text displayed on the landing page play button"
},
"start": "Comienzo",
"@start": {
"description": "Text displayed on the character selection page start button"
},
"characterSelectionTitle": "¡Elige a tu personaje!",
"@characterSelectionTitle": {
"description": "Title text displayed on the character selection page"
}
}

@ -5,16 +5,27 @@
// license that can be found in the LICENSE file or at // license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
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:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart'; import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart'; import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
void main() { void main() {
bootstrap((firestore) async { bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore); final leaderboardRepository = LeaderboardRepository(firestore);
final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudio = PinballAudio(); final pinballAudio = PinballAudio();
unawaited(
Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(),
),
);
return App( return App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
pinballAudio: pinballAudio, pinballAudio: pinballAudio,
); );

@ -5,16 +5,27 @@
// license that can be found in the LICENSE file or at // license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
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:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart'; import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart'; import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
void main() { void main() {
bootstrap((firestore) async { bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore); final leaderboardRepository = LeaderboardRepository(firestore);
final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudio = PinballAudio(); final pinballAudio = PinballAudio();
unawaited(
Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(),
),
);
return App( return App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
pinballAudio: pinballAudio, pinballAudio: pinballAudio,
); );

@ -5,16 +5,27 @@
// license that can be found in the LICENSE file or at // license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
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:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart'; import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart'; import 'package:pinball/bootstrap.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
void main() { void main() {
bootstrap((firestore) async { bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore); final leaderboardRepository = LeaderboardRepository(firestore);
final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudio = PinballAudio(); final pinballAudio = PinballAudio();
unawaited(
Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(),
),
);
return App( return App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
pinballAudio: pinballAudio, pinballAudio: pinballAudio,
); );

@ -5,7 +5,6 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/gen/gen.dart'; import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
import 'package:platform_helper/platform_helper.dart'; import 'package:platform_helper/platform_helper.dart';
@ -122,7 +121,7 @@ class _MobileLaunchControls extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
const textStyle = AppTextStyle.subtitle3; final textStyle = Theme.of(context).textTheme.headline3;
return Column( return Column(
children: [ children: [
Text( Text(
@ -138,9 +137,7 @@ class _MobileLaunchControls extends StatelessWidget {
), ),
TextSpan( TextSpan(
text: l10n.launch, text: l10n.launch,
style: textStyle.copyWith( style: textStyle?.copyWith(color: PinballColors.blue),
color: AppColors.blue,
),
), ),
], ],
), ),
@ -156,7 +153,7 @@ class _MobileFlipperControls extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
const textStyle = AppTextStyle.subtitle3; final textStyle = Theme.of(context).textTheme.headline3;
return Column( return Column(
children: [ children: [
Text( Text(
@ -172,9 +169,7 @@ class _MobileFlipperControls extends StatelessWidget {
), ),
TextSpan( TextSpan(
text: l10n.flip, text: l10n.flip,
style: textStyle.copyWith( style: textStyle?.copyWith(color: PinballColors.orange),
color: AppColors.orange,
),
), ),
], ],
), ),
@ -207,21 +202,22 @@ class _HowToPlayHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
const headerTextStyle = AppTextStyle.title; final textStyle = Theme.of(context).textTheme.headline3?.copyWith(
color: PinballColors.darkBlue,
);
return FittedBox( return FittedBox(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
l10n.howToPlay, l10n.howToPlay,
style: headerTextStyle.copyWith( style: textStyle?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
Text( Text(
l10n.tipsForFlips, l10n.tipsForFlips,
style: headerTextStyle, style: textStyle,
), ),
], ],
), ),
@ -241,7 +237,7 @@ class _DesktopLaunchControls extends StatelessWidget {
children: [ children: [
Text( Text(
l10n.launchControls, l10n.launchControls,
style: AppTextStyle.headline4, style: Theme.of(context).textTheme.headline4,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Wrap( Wrap(
@ -270,7 +266,7 @@ class _DesktopFlipperControls extends StatelessWidget {
children: [ children: [
Text( Text(
l10n.flipperControls, l10n.flipperControls,
style: AppTextStyle.subtitle2, style: Theme.of(context).textTheme.subtitle2,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Column( Column(
@ -311,8 +307,9 @@ class KeyButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final textStyle = final textStyle =
_control.isArrow ? AppTextStyle.headline1 : AppTextStyle.headline3; _control.isArrow ? textTheme.headline1 : textTheme.headline3;
const height = 60.0; const height = 60.0;
final width = _control.isSpace ? height * 2.83 : height; final width = _control.isSpace ? height * 2.83 : height;
return DecoratedBox( return DecoratedBox(
@ -334,7 +331,7 @@ class KeyButton extends StatelessWidget {
quarterTurns: _control.isDown ? 1 : 0, quarterTurns: _control.isDown ? 1 : 0,
child: Text( child: Text(
_control.getCharacter(context), _control.getCharacter(context),
style: textStyle.copyWith(color: AppColors.white), style: textStyle?.copyWith(color: PinballColors.white),
), ),
), ),
), ),

@ -1,2 +0,0 @@
export 'app_colors.dart';
export 'app_text_style.dart';

@ -0,0 +1,39 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# VSCode related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json

@ -0,0 +1,11 @@
# authentication_repository
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
Repository to manage user authentication.
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[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

@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml

@ -0,0 +1,3 @@
library authentication_repository;
export 'src/authentication_repository.dart';

@ -0,0 +1,36 @@
import 'package:firebase_auth/firebase_auth.dart';
/// {@template authentication_exception}
/// Exception for authentication repository failures.
/// {@endtemplate}
class AuthenticationException implements Exception {
/// {@macro authentication_exception}
const AuthenticationException(this.error, this.stackTrace);
/// The error that was caught.
final Object error;
/// The Stacktrace associated with the [error].
final StackTrace stackTrace;
}
/// {@template authentication_repository}
/// Repository to manage user authentication.
/// {@endtemplate}
class AuthenticationRepository {
/// {@macro authentication_repository}
AuthenticationRepository(this._firebaseAuth);
final FirebaseAuth _firebaseAuth;
/// Sign in the existing user anonymously using [FirebaseAuth]. If the
/// authentication process can't be completed, it will throw an
/// [AuthenticationException].
Future<void> authenticateAnonymously() async {
try {
await _firebaseAuth.signInAnonymously();
} on Exception catch (error, stackTrace) {
throw AuthenticationException(error, stackTrace);
}
}
}

@ -0,0 +1,18 @@
name: authentication_repository
description: Repository to manage user authentication.
version: 1.0.0+1
publish_to: none
environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
firebase_auth: ^3.3.16
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^0.2.0
very_good_analysis: ^2.4.0

@ -0,0 +1,40 @@
import 'package:authentication_repository/authentication_repository.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockFirebaseAuth extends Mock implements FirebaseAuth {}
class MockUserCredential extends Mock implements UserCredential {}
void main() {
late FirebaseAuth firebaseAuth;
late UserCredential userCredential;
late AuthenticationRepository authenticationRepository;
group('AuthenticationRepository', () {
setUp(() {
firebaseAuth = MockFirebaseAuth();
userCredential = MockUserCredential();
authenticationRepository = AuthenticationRepository(firebaseAuth);
});
group('authenticateAnonymously', () {
test('completes if no exception is thrown', () async {
when(() => firebaseAuth.signInAnonymously())
.thenAnswer((_) async => userCredential);
await authenticationRepository.authenticateAnonymously();
verify(() => firebaseAuth.signInAnonymously()).called(1);
});
test('throws AuthenticationException when firebase auth fails', () async {
when(() => firebaseAuth.signInAnonymously())
.thenThrow(Exception('oops'));
expect(
() => authenticationRepository.authenticateAnonymously(),
throwsA(isA<AuthenticationException>()),
);
});
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

@ -10,11 +10,14 @@ import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
$AssetsImagesAndroidBumperGen get androidBumper => $AssetsImagesAndroidGen get android => const $AssetsImagesAndroidGen();
const $AssetsImagesAndroidBumperGen();
$AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen(); $AssetsImagesBackboardGen get backboard => const $AssetsImagesBackboardGen();
$AssetsImagesBallGen get ball => const $AssetsImagesBallGen(); $AssetsImagesBallGen get ball => const $AssetsImagesBallGen();
$AssetsImagesBaseboardGen get baseboard => const $AssetsImagesBaseboardGen(); $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(); $AssetsImagesBoundaryGen get boundary => const $AssetsImagesBoundaryGen();
$AssetsImagesDashGen get dash => const $AssetsImagesDashGen(); $AssetsImagesDashGen get dash => const $AssetsImagesDashGen();
$AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen(); $AssetsImagesDinoGen get dino => const $AssetsImagesDinoGen();
@ -29,19 +32,18 @@ class $AssetsImagesGen {
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSpaceshipGen get spaceship => const $AssetsImagesSpaceshipGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
} }
class $AssetsImagesAndroidBumperGen { class $AssetsImagesAndroidGen {
const $AssetsImagesAndroidBumperGen(); const $AssetsImagesAndroidGen();
$AssetsImagesAndroidBumperAGen get a => $AssetsImagesAndroidBumperGen get bumper =>
const $AssetsImagesAndroidBumperAGen(); const $AssetsImagesAndroidBumperGen();
$AssetsImagesAndroidBumperBGen get b => $AssetsImagesAndroidRailGen get rail => const $AssetsImagesAndroidRailGen();
const $AssetsImagesAndroidBumperBGen(); $AssetsImagesAndroidRampGen get ramp => const $AssetsImagesAndroidRampGen();
$AssetsImagesAndroidBumperCowGen get cow => $AssetsImagesAndroidSpaceshipGen get spaceship =>
const $AssetsImagesAndroidBumperCowGen(); const $AssetsImagesAndroidSpaceshipGen();
} }
class $AssetsImagesBackboardGen { class $AssetsImagesBackboardGen {
@ -246,23 +248,6 @@ class $AssetsImagesSlingshotGen {
const AssetGenImage('assets/images/slingshot/upper.png'); const AssetGenImage('assets/images/slingshot/upper.png');
} }
class $AssetsImagesSpaceshipGen {
const $AssetsImagesSpaceshipGen();
/// File path: assets/images/spaceship/bridge.png
AssetGenImage get bridge =>
const AssetGenImage('assets/images/spaceship/bridge.png');
$AssetsImagesSpaceshipRailGen get rail =>
const $AssetsImagesSpaceshipRailGen();
$AssetsImagesSpaceshipRampGen get ramp =>
const $AssetsImagesSpaceshipRampGen();
/// File path: assets/images/spaceship/saucer.png
AssetGenImage get saucer =>
const AssetGenImage('assets/images/spaceship/saucer.png');
}
class $AssetsImagesSparkyGen { class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyGen(); const $AssetsImagesSparkyGen();
@ -276,40 +261,66 @@ class $AssetsImagesSparkyGen {
const $AssetsImagesSparkyComputerGen(); const $AssetsImagesSparkyComputerGen();
} }
class $AssetsImagesAndroidBumperAGen { class $AssetsImagesAndroidBumperGen {
const $AssetsImagesAndroidBumperGen();
$AssetsImagesAndroidBumperAGen get a =>
const $AssetsImagesAndroidBumperAGen(); const $AssetsImagesAndroidBumperAGen();
$AssetsImagesAndroidBumperBGen get b =>
const $AssetsImagesAndroidBumperBGen();
$AssetsImagesAndroidBumperCowGen get cow =>
const $AssetsImagesAndroidBumperCowGen();
}
/// File path: assets/images/android_bumper/a/dimmed.png class $AssetsImagesAndroidRailGen {
AssetGenImage get dimmed => const $AssetsImagesAndroidRailGen();
const AssetGenImage('assets/images/android_bumper/a/dimmed.png');
/// File path: assets/images/android_bumper/a/lit.png /// File path: assets/images/android/rail/exit.png
AssetGenImage get lit => AssetGenImage get exit =>
const AssetGenImage('assets/images/android_bumper/a/lit.png'); 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');
} }
class $AssetsImagesAndroidBumperBGen { class $AssetsImagesAndroidRampGen {
const $AssetsImagesAndroidBumperBGen(); const $AssetsImagesAndroidRampGen();
/// File path: assets/images/android_bumper/b/dimmed.png $AssetsImagesAndroidRampArrowGen get arrow =>
AssetGenImage get dimmed => const $AssetsImagesAndroidRampArrowGen();
const AssetGenImage('assets/images/android_bumper/b/dimmed.png');
/// File path: assets/images/android_bumper/b/lit.png /// File path: assets/images/android/ramp/board-opening.png
AssetGenImage get lit => AssetGenImage get boardOpening =>
const AssetGenImage('assets/images/android_bumper/b/lit.png'); 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');
} }
class $AssetsImagesAndroidBumperCowGen { class $AssetsImagesAndroidSpaceshipGen {
const $AssetsImagesAndroidBumperCowGen(); const $AssetsImagesAndroidSpaceshipGen();
/// File path: assets/images/android_bumper/cow/dimmed.png /// File path: assets/images/android/spaceship/animatronic.png
AssetGenImage get dimmed => AssetGenImage get animatronic =>
const AssetGenImage('assets/images/android_bumper/cow/dimmed.png'); const AssetGenImage('assets/images/android/spaceship/animatronic.png');
/// File path: assets/images/android_bumper/cow/lit.png /// File path: assets/images/android/spaceship/light-beam.png
AssetGenImage get lit => AssetGenImage get lightBeam =>
const AssetGenImage('assets/images/android_bumper/cow/lit.png'); 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');
} }
class $AssetsImagesDashBumperGen { class $AssetsImagesDashBumperGen {
@ -393,41 +404,6 @@ class $AssetsImagesMultiplierX6Gen {
const AssetGenImage('assets/images/multiplier/x6/lit.png'); const AssetGenImage('assets/images/multiplier/x6/lit.png');
} }
class $AssetsImagesSpaceshipRailGen {
const $AssetsImagesSpaceshipRailGen();
/// File path: assets/images/spaceship/rail/exit.png
AssetGenImage get exit =>
const AssetGenImage('assets/images/spaceship/rail/exit.png');
/// File path: assets/images/spaceship/rail/main.png
AssetGenImage get main =>
const AssetGenImage('assets/images/spaceship/rail/main.png');
}
class $AssetsImagesSpaceshipRampGen {
const $AssetsImagesSpaceshipRampGen();
$AssetsImagesSpaceshipRampArrowGen get arrow =>
const $AssetsImagesSpaceshipRampArrowGen();
/// File path: assets/images/spaceship/ramp/board-opening.png
AssetGenImage get boardOpening =>
const AssetGenImage('assets/images/spaceship/ramp/board-opening.png');
/// File path: assets/images/spaceship/ramp/main.png
AssetGenImage get main =>
const AssetGenImage('assets/images/spaceship/ramp/main.png');
/// File path: assets/images/spaceship/ramp/railing-background.png
AssetGenImage get railingBackground => const AssetGenImage(
'assets/images/spaceship/ramp/railing-background.png');
/// File path: assets/images/spaceship/ramp/railing-foreground.png
AssetGenImage get railingForeground => const AssetGenImage(
'assets/images/spaceship/ramp/railing-foreground.png');
}
class $AssetsImagesSparkyBumperGen { class $AssetsImagesSparkyBumperGen {
const $AssetsImagesSparkyBumperGen(); const $AssetsImagesSparkyBumperGen();
@ -452,6 +428,70 @@ class $AssetsImagesSparkyComputerGen {
const AssetGenImage('assets/images/sparky/computer/top.png'); const AssetGenImage('assets/images/sparky/computer/top.png');
} }
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');
}
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');
}
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');
}
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');
}
class $AssetsImagesDashBumperAGen { class $AssetsImagesDashBumperAGen {
const $AssetsImagesDashBumperAGen(); const $AssetsImagesDashBumperAGen();
@ -488,34 +528,6 @@ class $AssetsImagesDashBumperMainGen {
const AssetGenImage('assets/images/dash/bumper/main/inactive.png'); const AssetGenImage('assets/images/dash/bumper/main/inactive.png');
} }
class $AssetsImagesSpaceshipRampArrowGen {
const $AssetsImagesSpaceshipRampArrowGen();
/// File path: assets/images/spaceship/ramp/arrow/active1.png
AssetGenImage get active1 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active1.png');
/// File path: assets/images/spaceship/ramp/arrow/active2.png
AssetGenImage get active2 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active2.png');
/// File path: assets/images/spaceship/ramp/arrow/active3.png
AssetGenImage get active3 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active3.png');
/// File path: assets/images/spaceship/ramp/arrow/active4.png
AssetGenImage get active4 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active4.png');
/// File path: assets/images/spaceship/ramp/arrow/active5.png
AssetGenImage get active5 =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/active5.png');
/// File path: assets/images/spaceship/ramp/arrow/inactive.png
AssetGenImage get inactive =>
const AssetGenImage('assets/images/spaceship/ramp/arrow/inactive.png');
}
class $AssetsImagesSparkyBumperAGen { class $AssetsImagesSparkyBumperAGen {
const $AssetsImagesSparkyBumperAGen(); const $AssetsImagesSparkyBumperAGen();

@ -10,7 +10,7 @@ import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/android_bumper_cubit.dart'; export 'cubit/android_bumper_cubit.dart';
/// {@template android_bumper} /// {@template android_bumper}
/// Bumper for area under the [Spaceship]. /// Bumper for area under the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class AndroidBumper extends BodyComponent with InitialPosition { class AndroidBumper extends BodyComponent with InitialPosition {
/// {@macro android_bumper} /// {@macro android_bumper}
@ -46,8 +46,8 @@ class AndroidBumper extends BodyComponent with InitialPosition {
}) : this._( }) : this._(
majorRadius: 3.52, majorRadius: 3.52,
minorRadius: 2.97, minorRadius: 2.97,
litAssetPath: Assets.images.androidBumper.a.lit.keyName, litAssetPath: Assets.images.android.bumper.a.lit.keyName,
dimmedAssetPath: Assets.images.androidBumper.a.dimmed.keyName, dimmedAssetPath: Assets.images.android.bumper.a.dimmed.keyName,
spritePosition: Vector2(0, -0.1), spritePosition: Vector2(0, -0.1),
bloc: AndroidBumperCubit(), bloc: AndroidBumperCubit(),
children: children, children: children,
@ -59,8 +59,8 @@ class AndroidBumper extends BodyComponent with InitialPosition {
}) : this._( }) : this._(
majorRadius: 3.19, majorRadius: 3.19,
minorRadius: 2.79, minorRadius: 2.79,
litAssetPath: Assets.images.androidBumper.b.lit.keyName, litAssetPath: Assets.images.android.bumper.b.lit.keyName,
dimmedAssetPath: Assets.images.androidBumper.b.dimmed.keyName, dimmedAssetPath: Assets.images.android.bumper.b.dimmed.keyName,
spritePosition: Vector2(0, -0.1), spritePosition: Vector2(0, -0.1),
bloc: AndroidBumperCubit(), bloc: AndroidBumperCubit(),
children: children, children: children,
@ -72,8 +72,8 @@ class AndroidBumper extends BodyComponent with InitialPosition {
}) : this._( }) : this._(
majorRadius: 3.4, majorRadius: 3.4,
minorRadius: 2.9, minorRadius: 2.9,
litAssetPath: Assets.images.androidBumper.cow.lit.keyName, litAssetPath: Assets.images.android.bumper.cow.lit.keyName,
dimmedAssetPath: Assets.images.androidBumper.cow.dimmed.keyName, dimmedAssetPath: Assets.images.android.bumper.cow.dimmed.keyName,
spritePosition: Vector2(0, -0.68), spritePosition: Vector2(0, -0.68),
bloc: AndroidBumperCubit(), bloc: AndroidBumperCubit(),
children: children, children: children,

@ -0,0 +1,209 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
class AndroidSpaceship extends Blueprint {
AndroidSpaceship({required Vector2 position})
: super(
components: [
_SpaceshipSaucer()..initialPosition = position,
_SpaceshipSaucerSpriteAnimationComponent()..position = position,
_LightBeamSpriteComponent()..position = position + Vector2(2.5, 5),
_AndroidHead()..initialPosition = position + Vector2(0.5, 0.25),
_SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail,
outsidePriority: RenderPriority.ballOnSpaceshipRail,
)..initialPosition = position - Vector2(5.3, -5.4),
_SpaceshipHole(
outsideLayer: Layer.board,
outsidePriority: RenderPriority.ballOnBoard,
)..initialPosition = position - Vector2(-7.5, -1.1),
],
);
}
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
_SpaceshipSaucer() : super(renderBody: false) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final shape = _SpaceshipSaucerShape();
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
angle: -1.7,
);
return world.createBody(bodyDef)..createFixtureFromShape(shape);
}
}
class _SpaceshipSaucerShape extends ChainShape {
_SpaceshipSaucerShape() {
const minorRadius = 9.75;
const majorRadius = 11.9;
createChain(
[
for (var angle = 0.2618; angle <= 6.0214; angle += math.pi / 180)
Vector2(
minorRadius * math.cos(angle),
majorRadius * math.sin(angle),
),
],
);
}
}
class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
_SpaceshipSaucerSpriteAnimationComponent()
: super(
anchor: Anchor.center,
priority: RenderPriority.spaceshipSaucer,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.android.spaceship.saucer.keyName,
);
const amountPerRow = 5;
const amountPerColumn = 3;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
),
);
}
}
// TODO(allisonryan0002): add pulsing behavior.
class _LightBeamSpriteComponent extends SpriteComponent with HasGameRef {
_LightBeamSpriteComponent()
: super(
anchor: Anchor.center,
priority: RenderPriority.spaceshipLightBeam,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.android.spaceship.lightBeam.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}
class _AndroidHead extends BodyComponent with InitialPosition, Layered {
_AndroidHead()
: super(
priority: RenderPriority.androidHead,
children: [_AndroidHeadSpriteAnimationComponent()],
renderBody: false,
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final shape = EllipseShape(
center: Vector2.zero(),
majorRadius: 3.1,
minorRadius: 2,
)..rotate(1.4);
// TODO(allisonryan0002): use bumping behavior.
final fixtureDef = FixtureDef(
shape,
restitution: 0.1,
);
final bodyDef = BodyDef(position: initialPosition);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
_AndroidHeadSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(-0.24, -2.6),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.android.spaceship.animatronic.keyName,
);
const amountPerRow = 18;
const amountPerColumn = 4;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
),
);
}
}
class _SpaceshipHole extends LayerSensor {
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
: super(
insideLayer: Layer.spaceship,
outsideLayer: outsideLayer,
orientation: LayerEntranceOrientation.down,
insidePriority: RenderPriority.ballOnSpaceship,
outsidePriority: outsidePriority,
) {
layer = Layer.spaceship;
}
@override
Shape get shape {
return ArcShape(
center: Vector2(0, -3.2),
arcRadius: 5,
angle: 1,
rotation: -2,
);
}
}

@ -115,7 +115,7 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2), math.pow(defaultGravity, 2) - math.pow(positionalXForce, 2),
); );
body.gravityOverride = Vector2(positionalXForce, positionalYForce); body.gravityOverride = Vector2(-positionalXForce, positionalYForce);
} }
} }

@ -0,0 +1,26 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
class BoardBackgroundSpriteComponent extends SpriteComponent with HasGameRef {
BoardBackgroundSpriteComponent()
: super(
anchor: Anchor.center,
priority: RenderPriority.boardBackground,
position: Vector2(0, -1),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.boardBackground.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 10;
}
}

@ -1,7 +1,9 @@
export 'android_bumper/android_bumper.dart'; export 'android_bumper/android_bumper.dart';
export 'android_spaceship.dart';
export 'backboard/backboard.dart'; export 'backboard/backboard.dart';
export 'ball.dart'; export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board_background_sprite_component.dart';
export 'board_dimensions.dart'; export 'board_dimensions.dart';
export 'board_side.dart'; export 'board_side.dart';
export 'boundaries.dart'; export 'boundaries.dart';
@ -27,7 +29,6 @@ export 'score_text.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'signpost.dart'; export 'signpost.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export 'spaceship_ramp.dart'; export 'spaceship_ramp.dart';
export 'sparky_animatronic.dart'; export 'sparky_animatronic.dart';

@ -20,7 +20,7 @@ abstract class RenderPriority {
static const int ballOnSpaceshipRamp = static const int ballOnSpaceshipRamp =
_above + spaceshipRampBackgroundRailing; _above + spaceshipRampBackgroundRailing;
/// Render priority for the [Ball] while it's on the [Spaceship]. /// Render priority for the [Ball] while it's on the [AndroidSpaceship].
static const int ballOnSpaceship = _above + spaceshipSaucer; static const int ballOnSpaceship = _above + spaceshipSaucer;
/// Render priority for the [Ball] while it's on the [SpaceshipRail]. /// Render priority for the [Ball] while it's on the [SpaceshipRail].
@ -33,13 +33,13 @@ abstract class RenderPriority {
// TODO(allisonryan0002): fix this magic priority. Could bump all priorities // TODO(allisonryan0002): fix this magic priority. Could bump all priorities
// so there are no negatives. // so there are no negatives.
static const int background = 3 * _below + _base; static const int boardBackground = 3 * _below + _base;
// Boundaries // Boundaries
static const int bottomBoundary = _above + dinoBottomWall; static const int bottomBoundary = _above + dinoBottomWall;
static const int outerBoundary = _above + background; static const int outerBoundary = _above + boardBackground;
static const int outerBottomBoundary = _above + rocket; static const int outerBottomBoundary = _above + rocket;
@ -93,7 +93,7 @@ abstract class RenderPriority {
static const int spaceshipSaucer = _above + ballOnSpaceshipRail; static const int spaceshipSaucer = _above + ballOnSpaceshipRail;
static const int spaceshipSaucerWall = _above + spaceshipSaucer; static const int spaceshipLightBeam = _below + spaceshipSaucer;
static const int androidHead = _above + spaceshipSaucer; static const int androidHead = _above + spaceshipSaucer;

@ -1,246 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship}
/// A [Blueprint] which creates the spaceship feature.
/// {@endtemplate}
class Spaceship extends Blueprint {
/// {@macro spaceship}
Spaceship({required Vector2 position})
: super(
components: [
SpaceshipSaucer()..initialPosition = position,
_SpaceshipEntrance()..initialPosition = position,
AndroidHead()..initialPosition = position,
_SpaceshipHole(
outsideLayer: Layer.spaceshipExitRail,
outsidePriority: RenderPriority.ballOnSpaceshipRail,
)..initialPosition = position - Vector2(5.2, -4.8),
_SpaceshipHole(
outsideLayer: Layer.board,
outsidePriority: RenderPriority.ballOnBoard,
)..initialPosition = position - Vector2(-7.2, -0.8),
SpaceshipWall()..initialPosition = position,
],
);
/// Total size of the spaceship.
static final size = Vector2(25, 19);
}
/// {@template spaceship_saucer}
/// A [BodyComponent] for the base, or the saucer of the spaceship
/// {@endtemplate}
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_saucer}
SpaceshipSaucer()
: super(
priority: RenderPriority.spaceshipSaucer,
renderBody: false,
children: [
_SpaceshipSaucerSpriteComponent(),
],
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final shape = CircleShape()..radius = 3;
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
class _SpaceshipSaucerSpriteComponent extends SpriteComponent with HasGameRef {
_SpaceshipSaucerSpriteComponent()
: super(
anchor: Anchor.center,
// TODO(alestiago): Refactor to use sprite orignial size instead.
size: Spaceship.size,
);
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Use cached sprite.
sprite = await gameRef.loadSprite(
Assets.images.spaceship.saucer.keyName,
);
}
}
/// {@template spaceship_bridge}
/// A [BodyComponent] that provides both the collision and the rotation
/// animation for the bridge.
/// {@endtemplate}
class AndroidHead extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_bridge}
AndroidHead()
: super(
priority: RenderPriority.androidHead,
children: [_AndroidHeadSpriteAnimation()],
renderBody: false,
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final circleShape = CircleShape()..radius = 2;
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)
..createFixture(
FixtureDef(circleShape)..restitution = 0.4,
);
}
}
class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent
with HasGameRef {
@override
Future<void> onLoad() async {
await super.onLoad();
final image = await gameRef.images.load(
Assets.images.spaceship.bridge.keyName,
);
size = Vector2(8.2, 10);
position = Vector2(0, -2);
anchor = Anchor.center;
final data = SpriteAnimationData.sequenced(
amount: 72,
amountPerRow: 24,
stepTime: 0.05,
textureSize: size * 10,
);
animation = SpriteAnimation.fromFrameData(image, data);
}
}
class _SpaceshipEntrance extends LayerSensor {
_SpaceshipEntrance()
: super(
insideLayer: Layer.spaceship,
orientation: LayerEntranceOrientation.up,
insidePriority: RenderPriority.ballOnSpaceship,
) {
layer = Layer.spaceship;
}
@override
Shape get shape {
final radius = Spaceship.size.y / 2;
return PolygonShape()
..setAsEdge(
Vector2(
radius * cos(20 * pi / 180),
radius * sin(20 * pi / 180),
)..rotate(90 * pi / 180),
Vector2(
radius * cos(340 * pi / 180),
radius * sin(340 * pi / 180),
)..rotate(90 * pi / 180),
);
}
}
class _SpaceshipHole extends LayerSensor {
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
: super(
insideLayer: Layer.spaceship,
outsideLayer: outsideLayer,
orientation: LayerEntranceOrientation.down,
insidePriority: RenderPriority.ballOnSpaceship,
outsidePriority: outsidePriority,
) {
layer = Layer.spaceship;
}
@override
Shape get shape {
return ArcShape(
center: Vector2(0, -3.2),
arcRadius: 5,
angle: 1,
rotation: -2,
);
}
}
/// {@template spaceship_wall_shape}
/// The [ChainShape] that defines the shape of the [SpaceshipWall].
/// {@endtemplate}
class _SpaceshipWallShape extends ChainShape {
/// {@macro spaceship_wall_shape}
_SpaceshipWallShape() {
final minorRadius = (Spaceship.size.y - 2) / 2;
final majorRadius = (Spaceship.size.x - 2) / 2;
createChain(
[
// TODO(alestiago): Try converting this logic to radian.
for (var angle = 20; angle <= 340; angle++)
Vector2(
minorRadius * cos(angle * pi / 180),
majorRadius * sin(angle * pi / 180),
),
],
);
}
}
/// {@template spaceship_wall}
/// A [BodyComponent] that provides the collision for the wall
/// surrounding the spaceship.
///
/// It has a small opening to allow the [Ball] to get inside the spaceship
/// saucer.
///
/// It also contains the [SpriteComponent] for the lower wall
/// {@endtemplate}
class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
/// {@macro spaceship_wall}
SpaceshipWall()
: super(
priority: RenderPriority.spaceshipSaucerWall,
renderBody: false,
) {
layer = Layer.spaceship;
}
@override
Body createBody() {
final shape = _SpaceshipWallShape();
final fixtureDef = FixtureDef(shape);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
angle: -1.7,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -6,7 +6,7 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_rail} /// {@template spaceship_rail}
/// A [Blueprint] for the rail exiting the [Spaceship]. /// A [Blueprint] for the rail exiting the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRail extends Blueprint { class SpaceshipRail extends Blueprint {
/// {@macro spaceship_rail} /// {@macro spaceship_rail}
@ -116,7 +116,7 @@ class _SpaceshipRailSpriteComponent extends SpriteComponent with HasGameRef {
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.rail.main.keyName, Assets.images.android.rail.main.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -139,7 +139,7 @@ class _SpaceshipRailExitSpriteComponent extends SpriteComponent
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.rail.exit.keyName, Assets.images.android.rail.exit.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;

@ -8,7 +8,7 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// {@template spaceship_ramp} /// {@template spaceship_ramp}
/// A [Blueprint] which creates the ramp leading into the [Spaceship]. /// A [Blueprint] which creates the ramp leading into the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRamp extends Blueprint { class SpaceshipRamp extends Blueprint {
/// {@macro spaceship_ramp} /// {@macro spaceship_ramp}
@ -73,17 +73,17 @@ extension on SpaceshipRampArrowSpriteState {
String get path { String get path {
switch (this) { switch (this) {
case SpaceshipRampArrowSpriteState.inactive: case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.spaceship.ramp.arrow.inactive.keyName; return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1: case SpaceshipRampArrowSpriteState.active1:
return Assets.images.spaceship.ramp.arrow.active1.keyName; return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2: case SpaceshipRampArrowSpriteState.active2:
return Assets.images.spaceship.ramp.arrow.active2.keyName; return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3: case SpaceshipRampArrowSpriteState.active3:
return Assets.images.spaceship.ramp.arrow.active3.keyName; return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4: case SpaceshipRampArrowSpriteState.active4:
return Assets.images.spaceship.ramp.arrow.active4.keyName; return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5: case SpaceshipRampArrowSpriteState.active5:
return Assets.images.spaceship.ramp.arrow.active5.keyName; return Assets.images.android.ramp.arrow.active5.keyName;
} }
} }
@ -161,7 +161,7 @@ class _SpaceshipRampBackgroundRailingSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.android.ramp.railingBackground.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -182,7 +182,7 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.main.keyName, Assets.images.android.ramp.main.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -234,7 +234,7 @@ class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.android.ramp.boardOpening.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;
@ -304,7 +304,7 @@ class _SpaceshipRampForegroundRailingSpriteComponent extends SpriteComponent
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images.fromCache( gameRef.images.fromCache(
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.android.ramp.railingForeground.keyName,
), ),
); );
this.sprite = sprite; this.sprite = sprite;

@ -57,16 +57,16 @@ flutter:
- assets/images/dash/bumper/a/ - assets/images/dash/bumper/a/
- assets/images/dash/bumper/b/ - assets/images/dash/bumper/b/
- assets/images/dash/bumper/main/ - assets/images/dash/bumper/main/
- assets/images/spaceship/ - assets/images/android/spaceship/
- assets/images/spaceship/rail/ - assets/images/android/rail/
- assets/images/spaceship/ramp/ - assets/images/android/ramp/
- assets/images/spaceship/ramp/arrow/ - assets/images/android/ramp/arrow/
- assets/images/android/bumper/a/
- assets/images/android/bumper/b/
- assets/images/android/bumper/cow/
- assets/images/kicker/ - assets/images/kicker/
- assets/images/plunger/ - assets/images/plunger/
- assets/images/slingshot/ - assets/images/slingshot/
- assets/images/android_bumper/a/
- assets/images/android_bumper/b/
- assets/images/android_bumper/cow/
- assets/images/sparky/ - assets/images/sparky/
- assets/images/sparky/computer/ - assets/images/sparky/computer/
- assets/images/sparky/bumper/a/ - assets/images/sparky/bumper/a/

@ -9,8 +9,8 @@ class AndroidBumperAGame extends BallGame {
: super( : super(
color: const Color(0xFF0000FF), color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.androidBumper.a.lit.keyName, Assets.images.android.bumper.a.lit.keyName,
Assets.images.androidBumper.a.dimmed.keyName, Assets.images.android.bumper.a.dimmed.keyName,
], ],
); );

@ -9,8 +9,8 @@ class AndroidBumperBGame extends BallGame {
: super( : super(
color: const Color(0xFF0000FF), color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.androidBumper.b.lit.keyName, Assets.images.android.bumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,
], ],
); );

@ -8,8 +8,8 @@ class AndroidBumperCowGame extends BallGame {
AndroidBumperCowGame() AndroidBumperCowGame()
: super( : super(
imagesFileNames: [ imagesFileNames: [
Assets.images.androidBumper.cow.lit.keyName, Assets.images.android.bumper.cow.lit.keyName,
Assets.images.androidBumper.cow.dimmed.keyName, Assets.images.android.bumper.cow.dimmed.keyName,
], ],
); );

@ -0,0 +1,38 @@
import 'dart:async';
import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AndroidSpaceshipGame extends BallGame {
AndroidSpaceshipGame()
: super(
ballPriority: RenderPriority.ballOnSpaceship,
ballLayer: Layer.spaceship,
imagesFileNames: [
Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName,
],
);
static const description = '''
Shows how the AndroidSpaceship is rendered.
- Activate the "trace" parameter to overlay the body.
- Tap anywhere on the screen to spawn a Ball into the game.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await addFromBlueprint(
AndroidSpaceship(position: Vector2.zero()),
);
await traceAllBodies();
}
}

@ -1,35 +0,0 @@
import 'dart:async';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:sandbox/common/common.dart';
class SpaceshipGame extends AssetsGame with TapDetector {
static const description = '''
Shows how a Spaceship works.
- Tap anywhere on the screen to spawn a Ball into the game.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
await addFromBlueprint(
Spaceship(position: Vector2.zero()),
);
await ready();
}
@override
void onTapUp(TapUpInfo info) {
add(
Ball(baseColor: Colors.blue)
..initialPosition = info.eventPosition.game
..layer = Layer.spaceshipEntranceRamp,
);
}
}

@ -13,8 +13,8 @@ class SpaceshipRailGame extends BallGame {
ballPriority: RenderPriority.ballOnSpaceshipRail, ballPriority: RenderPriority.ballOnSpaceshipRail,
ballLayer: Layer.spaceshipExitRail, ballLayer: Layer.spaceshipExitRail,
imagesFileNames: [ imagesFileNames: [
Assets.images.spaceship.rail.main.keyName, Assets.images.android.rail.main.keyName,
Assets.images.spaceship.rail.exit.keyName, Assets.images.android.rail.exit.keyName,
], ],
); );

@ -14,16 +14,16 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
ballPriority: RenderPriority.ballOnSpaceshipRamp, ballPriority: RenderPriority.ballOnSpaceshipRamp,
ballLayer: Layer.spaceshipEntranceRamp, ballLayer: Layer.spaceshipEntranceRamp,
imagesFileNames: [ imagesFileNames: [
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.android.ramp.railingBackground.keyName,
Assets.images.spaceship.ramp.main.keyName, Assets.images.android.ramp.main.keyName,
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.android.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.android.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.arrow.inactive.keyName, Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.spaceship.ramp.arrow.active1.keyName, Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.spaceship.ramp.arrow.active2.keyName, Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName, Assets.images.android.ramp.arrow.active5.keyName,
], ],
); );

@ -3,7 +3,7 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/android_acres/android_bumper_a_game.dart'; import 'package:sandbox/stories/android_acres/android_bumper_a_game.dart';
import 'package:sandbox/stories/android_acres/android_bumper_b_game.dart'; import 'package:sandbox/stories/android_acres/android_bumper_b_game.dart';
import 'package:sandbox/stories/android_acres/android_bumper_cow_game.dart'; import 'package:sandbox/stories/android_acres/android_bumper_cow_game.dart';
import 'package:sandbox/stories/android_acres/spaceship_game.dart'; import 'package:sandbox/stories/android_acres/android_spaceship_game.dart';
import 'package:sandbox/stories/android_acres/spaceship_rail_game.dart'; import 'package:sandbox/stories/android_acres/spaceship_rail_game.dart';
import 'package:sandbox/stories/android_acres/spaceship_ramp_game.dart'; import 'package:sandbox/stories/android_acres/spaceship_ramp_game.dart';
@ -25,9 +25,9 @@ void addAndroidAcresStories(Dashbook dashbook) {
gameBuilder: (_) => AndroidBumperCowGame(), gameBuilder: (_) => AndroidBumperCowGame(),
) )
..addGame( ..addGame(
title: 'Spaceship', title: 'Android Spaceship',
description: SpaceshipGame.description, description: AndroidSpaceshipGame.description,
gameBuilder: (_) => SpaceshipGame(), gameBuilder: (_) => AndroidSpaceshipGame(),
) )
..addGame( ..addGame(
title: 'Spaceship Rail', title: 'Spaceship Rail',

@ -13,12 +13,12 @@ import '../../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.androidBumper.a.lit.keyName, Assets.images.android.bumper.a.lit.keyName,
Assets.images.androidBumper.a.dimmed.keyName, Assets.images.android.bumper.a.dimmed.keyName,
Assets.images.androidBumper.b.lit.keyName, Assets.images.android.bumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,
Assets.images.androidBumper.cow.lit.keyName, Assets.images.android.bumper.cow.lit.keyName,
Assets.images.androidBumper.cow.dimmed.keyName, Assets.images.android.bumper.cow.dimmed.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));

@ -0,0 +1,66 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart';
void main() {
group('AndroidSpaceship', () {
group('Spaceship', () {
final assets = [
Assets.images.android.spaceship.saucer.keyName,
Assets.images.android.spaceship.animatronic.keyName,
Assets.images.android.spaceship.lightBeam.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
flameTester.test('loads correctly', (game) async {
await game.addFromBlueprint(AndroidSpaceship(position: Vector2.zero()));
await game.ready();
});
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
await game
.addFromBlueprint(AndroidSpaceship(position: Vector2.zero()));
game.camera.followVector2(Vector2.zero());
await game.ready();
await tester.pump();
},
verify: (game, tester) async {
final animationDuration = game
.descendants()
.whereType<SpriteAnimationComponent>()
.last
.animation!
.totalDuration();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/android_spaceship/start.png'),
);
game.update(animationDuration * 0.5);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/android_spaceship/middle.png'),
);
game.update(animationDuration * 0.5);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/android_spaceship/end.png'),
);
},
);
});
});
}

@ -0,0 +1,48 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.boardBackground.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group('BoardBackgroundSpriteComponent', () {
flameTester.test(
'loads correctly',
(game) async {
final boardBackground = BoardBackgroundSpriteComponent();
await game.ensureAdd(boardBackground);
expect(game.contains(boardBackground), isTrue);
},
);
flameTester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final boardBackground = BoardBackgroundSpriteComponent();
await game.ensureAdd(boardBackground);
await tester.pump();
game.camera
..followVector2(Vector2.zero())
..zoom = 3.7;
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/board-background.png'),
);
},
);
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

@ -12,8 +12,8 @@ void main() {
group('SpaceshipRail', () { group('SpaceshipRail', () {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.spaceship.rail.main.keyName, Assets.images.android.rail.main.keyName,
Assets.images.spaceship.rail.exit.keyName, Assets.images.android.rail.exit.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));

@ -11,16 +11,16 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final assets = [ final assets = [
Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.android.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.android.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.railingBackground.keyName, Assets.images.android.ramp.railingBackground.keyName,
Assets.images.spaceship.ramp.main.keyName, Assets.images.android.ramp.main.keyName,
Assets.images.spaceship.ramp.arrow.inactive.keyName, Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.spaceship.ramp.arrow.active1.keyName, Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.spaceship.ramp.arrow.active2.keyName, Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName, Assets.images.android.ramp.arrow.active5.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));

@ -1,56 +0,0 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../helpers/helpers.dart';
void main() {
group('Spaceship', () {
late Filter filterData;
late Fixture fixture;
late Body body;
late Ball ball;
late Forge2DGame game;
setUp(() {
filterData = MockFilter();
fixture = MockFixture();
when(() => fixture.filterData).thenReturn(filterData);
body = MockBody();
when(() => body.fixtures).thenReturn([fixture]);
game = MockGame();
ball = MockBall();
when(() => ball.gameRef).thenReturn(game);
when(() => ball.body).thenReturn(body);
});
group('Spaceship', () {
final tester = FlameTester(TestGame.new);
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
final position = Vector2(30, -30);
await game.addFromBlueprint(Spaceship(position: position));
game.camera.followVector2(position);
await game.ready();
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/spaceship.png'),
);
},
);
});
});
}

@ -1,95 +1,105 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// Appends a new [ContactCallbacks] to the parent. /// Appends a new [ContactCallbacks] to the parent.
/// ///
/// This is a convenience class for adding a [ContactCallbacks] to the parent. /// This is a convenience class for adding a [ContactCallbacks] to the parent.
/// In constract with just adding a [ContactCallbacks] to the parent's body /// In contrast with just assigning a [ContactCallbacks] to a userData, this
/// userData, this class respects the previous [ContactCallbacks] in the /// class respects the previous userData.
/// parent's body userData, if any. Hence, it avoids overriding any previous
/// [ContactCallbacks] in the parent.
/// ///
/// It does so by grouping the [ContactCallbacks] in a [_ContactCallbacksGroup], /// It does so by grouping the userData in a [_UserData], and resetting the
/// and resetting the parent's userData accordingly. /// parent's userData accordingly.
// TODO(alestiago): Make use of generics to infer the type of the contact. // TODO(alestiago): Make use of generics to infer the type of the contact.
// https://github.com/VGVentures/pinball/pull/234#discussion_r859182267 // https://github.com/VGVentures/pinball/pull/234#discussion_r859182267
// TODO(alestiago): Consider if there is a need to support adjusting a fixture's
// userData.
class ContactBehavior<T extends BodyComponent> extends Component class ContactBehavior<T extends BodyComponent> extends Component
with ContactCallbacks, ParentIsA<T> { with ContactCallbacks, ParentIsA<T> {
final _fixturesUserData = <Object>{};
/// Specifies which fixtures should be considered for contact.
///
/// Fixtures are identifiable by their userData.
///
/// If no fixtures are specified, the [ContactCallbacks] is applied to the
/// entire body, hence all fixtures are considered.
void applyTo(Iterable<Object> userData) => _fixturesUserData.addAll(userData);
@override @override
@mustCallSuper
Future<void> onLoad() async { Future<void> onLoad() async {
final userData = parent.body.userData; if (_fixturesUserData.isNotEmpty) {
if (userData is _ContactCallbacksGroup) { for (final fixture in _targetedFixtures) {
userData.addContactCallbacks(this); fixture.userData = _UserData.fromFixture(fixture)..add(this);
} else if (userData is ContactCallbacks) { }
final contactCallbacksGroup = _ContactCallbacksGroup()
..addContactCallbacks(userData)
..addContactCallbacks(this);
parent.body.userData = contactCallbacksGroup;
} else { } else {
parent.body.userData = this; parent.body.userData = _UserData.fromBody(parent.body)..add(this);
} }
} }
Iterable<Fixture> get _targetedFixtures =>
parent.body.fixtures.where((fixture) {
if (_fixturesUserData.contains(fixture.userData)) return true;
final userData = fixture.userData;
if (userData is _UserData) {
return _fixturesUserData.contains(userData.value);
}
return false;
});
} }
class _ContactCallbacksGroup implements ContactCallbacks { class _UserData with ContactCallbacks {
final List<ContactCallbacks> _contactCallbacks = []; _UserData._(Object? userData) : _userData = [userData];
factory _UserData._fromUserData(Object? userData) {
if (userData is _UserData) return userData;
return _UserData._(userData);
}
factory _UserData.fromFixture(Fixture fixture) =>
_UserData._fromUserData(fixture.userData);
factory _UserData.fromBody(Body body) =>
_UserData._fromUserData(body.userData);
final List<Object?> _userData;
Iterable<ContactCallbacks> get _contactCallbacks =>
_userData.whereType<ContactCallbacks>();
Object? get value => _userData.first;
void add(Object? userData) => _userData.add(userData);
@override @override
@mustCallSuper
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
onBeginContact?.call(other, contact); super.beginContact(other, contact);
for (final callback in _contactCallbacks) { for (final callback in _contactCallbacks) {
callback.beginContact(other, contact); callback.beginContact(other, contact);
} }
} }
@override @override
@mustCallSuper
void endContact(Object other, Contact contact) { void endContact(Object other, Contact contact) {
onEndContact?.call(other, contact); super.endContact(other, contact);
for (final callback in _contactCallbacks) { for (final callback in _contactCallbacks) {
callback.endContact(other, contact); callback.endContact(other, contact);
} }
} }
@override @override
@mustCallSuper
void preSolve(Object other, Contact contact, Manifold oldManifold) { void preSolve(Object other, Contact contact, Manifold oldManifold) {
onPreSolve?.call(other, contact, oldManifold); super.preSolve(other, contact, oldManifold);
for (final callback in _contactCallbacks) { for (final callback in _contactCallbacks) {
callback.preSolve(other, contact, oldManifold); callback.preSolve(other, contact, oldManifold);
} }
} }
@override @override
@mustCallSuper
void postSolve(Object other, Contact contact, ContactImpulse impulse) { void postSolve(Object other, Contact contact, ContactImpulse impulse) {
onPostSolve?.call(other, contact, impulse); super.postSolve(other, contact, impulse);
for (final callback in _contactCallbacks) { for (final callback in _contactCallbacks) {
callback.postSolve(other, contact, impulse); callback.postSolve(other, contact, impulse);
} }
} }
void addContactCallbacks(ContactCallbacks callback) {
_contactCallbacks.add(callback);
}
@override
void Function(Object other, Contact contact)? onBeginContact;
@override
void Function(Object other, Contact contact)? onEndContact;
@override
void Function(Object other, Contact contact, ContactImpulse impulse)?
onPostSolve;
@override
void Function(Object other, Contact contact, Manifold oldManifold)?
onPreSolve;
} }

@ -58,28 +58,78 @@ void main() {
late Contact contact; late Contact contact;
late Manifold manifold; late Manifold manifold;
late ContactImpulse contactImpulse; late ContactImpulse contactImpulse;
late FixtureDef fixtureDef;
setUp(() { setUp(() {
other = Object(); other = Object();
contact = _MockContact(); contact = _MockContact();
manifold = _MockManifold(); manifold = _MockManifold();
contactImpulse = _MockContactImpulse(); contactImpulse = _MockContactImpulse();
fixtureDef = FixtureDef(CircleShape());
}); });
flameTester.test( flameTester.test(
'should add a new ContactCallbacks to the parent', "should add a new ContactCallbacks to the parent's body userData "
'when not applied to fixtures',
(game) async { (game) async {
final parent = _TestBodyComponent(); final parent = _TestBodyComponent();
final contactBehavior = ContactBehavior(); final contactBehavior = ContactBehavior();
await parent.add(contactBehavior); await parent.add(contactBehavior);
await game.ensureAdd(parent); await game.ensureAdd(parent);
expect(parent.body.userData, contactBehavior); expect(parent.body.userData, isA<ContactCallbacks>());
}, },
); );
flameTester.test( flameTester.test(
"should respect the previous ContactCallbacks in the parent's userData", 'should add a new ContactCallbacks to the targeted fixture ',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final fixture1 =
parent.body.createFixture(fixtureDef..userData = 'foo');
final fixture2 = parent.body.createFixture(fixtureDef..userData = null);
final contactBehavior = ContactBehavior()
..applyTo(
[fixture1.userData!],
);
await parent.ensureAdd(contactBehavior);
expect(parent.body.userData, isNull);
expect(fixture1.userData, isA<ContactCallbacks>());
expect(fixture2.userData, isNull);
},
);
flameTester.test(
'should add a new ContactCallbacks to the targeted fixtures ',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final fixture1 =
parent.body.createFixture(fixtureDef..userData = 'foo');
final fixture2 =
parent.body.createFixture(fixtureDef..userData = 'boo');
final contactBehavior = ContactBehavior()
..applyTo([
fixture1.userData!,
fixture2.userData!,
]);
await parent.ensureAdd(contactBehavior);
expect(parent.body.userData, isNull);
expect(fixture1.userData, isA<ContactCallbacks>());
expect(fixture2.userData, isA<ContactCallbacks>());
},
);
flameTester.test(
"should respect the previous ContactCallbacks in the parent's userData "
'when not applied to fixtures',
(game) async { (game) async {
final parent = _TestBodyComponent(); final parent = _TestBodyComponent();
await game.ensureAdd(parent); await game.ensureAdd(parent);
@ -113,7 +163,8 @@ void main() {
}, },
); );
flameTester.test('can group multiple ContactBehaviors and keep listening', flameTester.test(
'can group multiple ContactBehaviors and keep listening',
(game) async { (game) async {
final parent = _TestBodyComponent(); final parent = _TestBodyComponent();
await game.ensureAdd(parent); await game.ensureAdd(parent);
@ -148,6 +199,58 @@ void main() {
expect(contactBehavior1.postSolveContactCallsCount, equals(1)); expect(contactBehavior1.postSolveContactCallsCount, equals(1));
expect(contactBehavior2.postSolveContactCallsCount, equals(1)); expect(contactBehavior2.postSolveContactCallsCount, equals(1));
expect(contactBehavior3.postSolveContactCallsCount, equals(1)); expect(contactBehavior3.postSolveContactCallsCount, equals(1));
}); },
);
flameTester.test(
'can group multiple ContactBehaviors and keep listening '
'when applied to a fixture',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final fixture = parent.body.createFixture(fixtureDef..userData = 'foo');
final contactBehavior1 = _TestContactBehavior()
..applyTo(
[fixture.userData!],
);
final contactBehavior2 = _TestContactBehavior()
..applyTo(
[fixture.userData!],
);
final contactBehavior3 = _TestContactBehavior()
..applyTo(
[fixture.userData!],
);
await parent.ensureAddAll([
contactBehavior1,
contactBehavior2,
contactBehavior3,
]);
final contactCallbacks = fixture.userData! as ContactCallbacks;
contactCallbacks.beginContact(other, contact);
expect(contactBehavior1.beginContactCallsCount, equals(1));
expect(contactBehavior2.beginContactCallsCount, equals(1));
expect(contactBehavior3.beginContactCallsCount, equals(1));
contactCallbacks.endContact(other, contact);
expect(contactBehavior1.endContactCallsCount, equals(1));
expect(contactBehavior2.endContactCallsCount, equals(1));
expect(contactBehavior3.endContactCallsCount, equals(1));
contactCallbacks.preSolve(other, contact, manifold);
expect(contactBehavior1.preSolveContactCallsCount, equals(1));
expect(contactBehavior2.preSolveContactCallsCount, equals(1));
expect(contactBehavior3.preSolveContactCallsCount, equals(1));
contactCallbacks.postSolve(other, contact, contactImpulse);
expect(contactBehavior1.postSolveContactCallsCount, equals(1));
expect(contactBehavior2.postSolveContactCallsCount, equals(1));
expect(contactBehavior3.postSolveContactCallsCount, equals(1));
},
);
}); });
} }

@ -0,0 +1,16 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// 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';
}

@ -1,3 +1,8 @@
library pinball_ui; library pinball_ui;
export 'package:url_launcher/url_launcher.dart';
export 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
export 'src/dialog/dialog.dart'; export 'src/dialog/dialog.dart';
export 'src/external_links/external_links.dart';
export 'src/theme/theme.dart';

@ -0,0 +1,12 @@
import 'package:flutter/foundation.dart';
import 'package:url_launcher/url_launcher.dart';
/// Opens the given [url] in a new tab of the host browser
Future<void> openLink(String url, {VoidCallback? onError}) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else if (onError != null) {
onError();
}
}

@ -1,17 +1,11 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
abstract class AppColors { abstract class PinballColors {
static const Color white = Color(0xFFFFFFFF); static const Color white = Color(0xFFFFFFFF);
static const Color darkBlue = Color(0xFF0C32A4); static const Color darkBlue = Color(0xFF0C32A4);
static const Color yellow = Color(0xFFFFEE02); static const Color yellow = Color(0xFFFFEE02);
static const Color orange = Color(0xFFE5AB05); static const Color orange = Color(0xFFE5AB05);
static const Color blue = Color(0xFF4B94F6); static const Color blue = Color(0xFF4B94F6);
static const Color transparent = Color(0x00000000); static const Color transparent = Color(0x00000000);
} }

@ -1,17 +1,18 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:pinball/theme/theme.dart'; import 'package:pinball_ui/gen/fonts.gen.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_ui/pinball_ui.dart';
const _fontPackage = 'pinball_components'; const _fontPackage = 'pinball_components';
const _primaryFontFamily = FontFamily.pixeloidSans; const _primaryFontFamily = FontFamily.pixeloidSans;
abstract class AppTextStyle { abstract class PinballTextStyle {
static const headline1 = TextStyle( static const headline1 = TextStyle(
fontSize: 28, fontSize: 28,
package: _fontPackage, package: _fontPackage,
fontFamily: _primaryFontFamily, fontFamily: _primaryFontFamily,
color: PinballColors.white,
); );
static const headline2 = TextStyle( static const headline2 = TextStyle(
@ -21,36 +22,22 @@ abstract class AppTextStyle {
); );
static const headline3 = TextStyle( static const headline3 = TextStyle(
color: AppColors.white, color: PinballColors.white,
fontSize: 20, fontSize: 20,
package: _fontPackage, package: _fontPackage,
fontFamily: _primaryFontFamily, fontFamily: _primaryFontFamily,
fontWeight: FontWeight.bold,
); );
static const headline4 = TextStyle( static const headline4 = TextStyle(
color: AppColors.white, color: PinballColors.white,
fontSize: 16, fontSize: 16,
package: _fontPackage, package: _fontPackage,
fontFamily: _primaryFontFamily, fontFamily: _primaryFontFamily,
); );
static const title = TextStyle(
color: AppColors.darkBlue,
fontSize: 20,
package: _fontPackage,
fontFamily: _primaryFontFamily,
);
static const subtitle3 = TextStyle(
color: AppColors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
package: _fontPackage,
fontFamily: _primaryFontFamily,
);
static const subtitle2 = TextStyle( static const subtitle2 = TextStyle(
color: AppColors.white, color: PinballColors.white,
fontSize: 16, fontSize: 16,
package: _fontPackage, package: _fontPackage,
fontFamily: _primaryFontFamily, fontFamily: _primaryFontFamily,
@ -60,5 +47,6 @@ abstract class AppTextStyle {
fontSize: 10, fontSize: 10,
fontFamily: _primaryFontFamily, fontFamily: _primaryFontFamily,
package: _fontPackage, package: _fontPackage,
color: PinballColors.yellow,
); );
} }

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Pinball theme
class PinballTheme {
/// Standard [ThemeData] for Pinball UI
static ThemeData get standard {
return ThemeData(
textTheme: _textTheme,
);
}
static TextTheme get _textTheme {
return const TextTheme(
headline1: PinballTextStyle.headline1,
headline2: PinballTextStyle.headline2,
headline3: PinballTextStyle.headline3,
headline4: PinballTextStyle.headline4,
subtitle1: PinballTextStyle.subtitle1,
subtitle2: PinballTextStyle.subtitle2,
);
}
}

@ -0,0 +1,3 @@
export 'pinball_colors.dart';
export 'pinball_text_style.dart';
export 'pinball_theme.dart';

@ -9,19 +9,29 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
url_launcher: ^6.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mocktail: ^0.3.0
test: ^1.19.2 test: ^1.19.2
very_good_analysis: ^2.4.0 very_good_analysis: ^2.4.0
flutter: flutter:
uses-material-design: true uses-material-design: true
generate: true generate: true
assets: assets:
- assets/images/dialog/ - assets/images/dialog/
fonts:
- family: PixeloidSans
fonts:
- asset: fonts/PixeloidSans-nR3g1.ttf
- asset: fonts/PixeloidSansBold-RpeJo.ttf
weight: 700
- family: PixeloidMono
fonts:
- asset: fonts/PixeloidMono-1G8ae.ttf
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -0,0 +1,81 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_ui/pinball_ui.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {}
void main() {
late UrlLauncherPlatform urlLauncher;
setUp(() {
urlLauncher = MockUrlLauncher();
UrlLauncherPlatform.instance = urlLauncher;
});
group('openLink', () {
test('launches the link', () async {
when(
() => urlLauncher.canLaunch(any()),
).thenAnswer(
(_) async => true,
);
when(
() => urlLauncher.launch(
any(),
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer(
(_) async => true,
);
await openLink('uri');
verify(
() => urlLauncher.launch(
any(),
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
);
});
test('executes the onError callback when it cannot launch', () async {
var wasCalled = false;
when(
() => urlLauncher.canLaunch(any()),
).thenAnswer(
(_) async => false,
);
when(
() => urlLauncher.launch(
any(),
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer(
(_) async => true,
);
await openLink(
'url',
onError: () {
wasCalled = true;
},
);
await expectLater(wasCalled, isTrue);
});
});
}

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('PinballColors', () {
test('white is 0xFFFFFFFF', () {
expect(PinballColors.white, const Color(0xFFFFFFFF));
});
test('darkBlue is 0xFF0C32A4', () {
expect(PinballColors.darkBlue, const Color(0xFF0C32A4));
});
test('yellow is 0xFFFFEE02', () {
expect(PinballColors.yellow, const Color(0xFFFFEE02));
});
test('orange is 0xFFE5AB05', () {
expect(PinballColors.orange, const Color(0xFFE5AB05));
});
test('blue is 0xFF4B94F6', () {
expect(PinballColors.blue, const Color(0xFF4B94F6));
});
test('transparent is 0x00000000', () {
expect(PinballColors.transparent, const Color(0x00000000));
});
});
}

@ -0,0 +1,41 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('PinballTextStyle', () {
test('headline1 has fontSize 28 and white color', () {
const style = PinballTextStyle.headline1;
expect(style.fontSize, 28);
expect(style.color, PinballColors.white);
});
test('headline2 has fontSize 24', () {
const style = PinballTextStyle.headline2;
expect(style.fontSize, 24);
});
test('headline3 has fontSize 20 and white color', () {
const style = PinballTextStyle.headline3;
expect(style.fontSize, 20);
expect(style.color, PinballColors.white);
});
test('headline4 has fontSize 16 and white color', () {
const style = PinballTextStyle.headline4;
expect(style.fontSize, 16);
expect(style.color, PinballColors.white);
});
test('subtitle1 has fontSize 10 and yellow color', () {
const style = PinballTextStyle.subtitle1;
expect(style.fontSize, 10);
expect(style.color, PinballColors.yellow);
});
test('subtitle2 has fontSize 16 and white color', () {
const style = PinballTextStyle.subtitle2;
expect(style.fontSize, 16);
expect(style.color, PinballColors.white);
});
});
}

@ -0,0 +1,98 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('PinballTheme', () {
group('standard', () {
test('headline1 matches PinballTextStyle#headline1', () {
expect(
PinballTheme.standard.textTheme.headline1!.fontSize,
PinballTextStyle.headline1.fontSize,
);
expect(
PinballTheme.standard.textTheme.headline1!.color,
PinballTextStyle.headline1.color,
);
expect(
PinballTheme.standard.textTheme.headline1!.fontFamily,
PinballTextStyle.headline1.fontFamily,
);
});
test('headline2 matches PinballTextStyle#headline2', () {
expect(
PinballTheme.standard.textTheme.headline2!.fontSize,
PinballTextStyle.headline2.fontSize,
);
expect(
PinballTheme.standard.textTheme.headline2!.fontFamily,
PinballTextStyle.headline2.fontFamily,
);
expect(
PinballTheme.standard.textTheme.headline2!.fontWeight,
PinballTextStyle.headline2.fontWeight,
);
});
test('headline3 matches PinballTextStyle#headline3', () {
expect(
PinballTheme.standard.textTheme.headline3!.fontSize,
PinballTextStyle.headline3.fontSize,
);
expect(
PinballTheme.standard.textTheme.headline3!.color,
PinballTextStyle.headline3.color,
);
expect(
PinballTheme.standard.textTheme.headline3!.fontFamily,
PinballTextStyle.headline3.fontFamily,
);
});
test('headline4 matches PinballTextStyle#headline4', () {
expect(
PinballTheme.standard.textTheme.headline4!.fontSize,
PinballTextStyle.headline4.fontSize,
);
expect(
PinballTheme.standard.textTheme.headline4!.color,
PinballTextStyle.headline4.color,
);
expect(
PinballTheme.standard.textTheme.headline4!.fontFamily,
PinballTextStyle.headline4.fontFamily,
);
});
test('subtitle1 matches PinballTextStyle#subtitle1', () {
expect(
PinballTheme.standard.textTheme.subtitle1!.fontSize,
PinballTextStyle.subtitle1.fontSize,
);
expect(
PinballTheme.standard.textTheme.subtitle1!.color,
PinballTextStyle.subtitle1.color,
);
expect(
PinballTheme.standard.textTheme.subtitle1!.fontFamily,
PinballTextStyle.subtitle1.fontFamily,
);
});
test('subtitle2 matches PinballTextStyle#subtitle2', () {
expect(
PinballTheme.standard.textTheme.subtitle2!.fontSize,
PinballTextStyle.subtitle2.fontSize,
);
expect(
PinballTheme.standard.textTheme.subtitle2!.color,
PinballTextStyle.subtitle2.color,
);
expect(
PinballTheme.standard.textTheme.subtitle2!.fontFamily,
PinballTextStyle.subtitle2.fontFamily,
);
});
});
});
}

@ -36,6 +36,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.20.1" version: "0.20.1"
authentication_repository:
dependency: "direct main"
description:
path: "packages/authentication_repository"
relative: true
source: path
version: "1.0.0+1"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -169,13 +176,34 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.1.2" version: "6.1.2"
firebase_core: firebase_auth:
dependency: "direct main"
description:
name: firebase_auth
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.16"
firebase_auth_platform_interface:
dependency: transitive dependency: transitive
description:
name: firebase_auth_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "6.2.4"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.13"
firebase_core:
dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.13.1" version: "1.15.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -189,7 +217,7 @@ packages:
name: firebase_core_web name: firebase_core_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.6.1" version: "1.6.2"
flame: flame:
dependency: "direct main" dependency: "direct main"
description: description:
@ -672,6 +700,62 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
url_launcher:
dependency: transitive
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.0"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.16"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.15"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
@ -744,4 +828,4 @@ packages:
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.16.0 <3.0.0" dart: ">=2.16.0 <3.0.0"
flutter: ">=2.8.0" flutter: ">=2.10.0"

@ -7,9 +7,13 @@ environment:
sdk: ">=2.16.0 <3.0.0" sdk: ">=2.16.0 <3.0.0"
dependencies: dependencies:
authentication_repository:
path: packages/authentication_repository
bloc: ^8.0.2 bloc: ^8.0.2
cloud_firestore: ^3.1.10 cloud_firestore: ^3.1.10
equatable: ^2.0.3 equatable: ^2.0.3
firebase_auth: ^3.3.16
firebase_core: ^1.15.0
flame: ^1.1.1 flame: ^1.1.1
flame_bloc: ^1.2.0 flame_bloc: ^1.2.0
flame_forge2d: flame_forge2d:

@ -5,6 +5,7 @@
// license that can be found in the LICENSE file or at // license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
import 'package:authentication_repository/authentication_repository.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -16,19 +17,21 @@ import '../../helpers/mocks.dart';
void main() { void main() {
group('App', () { group('App', () {
late AuthenticationRepository authenticationRepository;
late LeaderboardRepository leaderboardRepository; late LeaderboardRepository leaderboardRepository;
late PinballAudio pinballAudio; late PinballAudio pinballAudio;
setUp(() { setUp(() {
authenticationRepository = MockAuthenticationRepository();
leaderboardRepository = MockLeaderboardRepository(); leaderboardRepository = MockLeaderboardRepository();
pinballAudio = MockPinballAudio(); pinballAudio = MockPinballAudio();
when(pinballAudio.load).thenAnswer((_) => Future.value()); when(pinballAudio.load).thenAnswer((_) => Future.value());
}); });
testWidgets('renders PinballGamePage', (tester) async { testWidgets('renders PinballGamePage', (tester) async {
await tester.pumpWidget( await tester.pumpWidget(
App( App(
authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
pinballAudio: pinballAudio, pinballAudio: pinballAudio,
), ),

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/footer/footer.dart';
import 'package:pinball_ui/pinball_ui.dart';
import '../helpers/helpers.dart';
void main() {
group('Footer', () {
late UrlLauncherPlatform urlLauncher;
setUp(() async {
urlLauncher = MockUrlLauncher();
UrlLauncherPlatform.instance = urlLauncher;
});
testWidgets('renders "Made with..." and "Google I/O"', (tester) async {
await tester.pumpApp(const Footer());
expect(find.text('Google I/O'), findsOneWidget);
expect(
find.byWidgetPredicate(
(widget) =>
widget is RichText &&
widget.text.toPlainText() == 'Made with Flutter & Firebase',
),
findsOneWidget,
);
});
testWidgets(
'tapping on "Flutter" opens the flutter website',
(tester) async {
when(() => urlLauncher.canLaunch(any())).thenAnswer((_) async => true);
when(
() => urlLauncher.launch(
any(),
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
await tester.pumpApp(const Footer());
final flutterTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && tapTextSpan(widget, 'Flutter'),
);
await tester.tap(flutterTextFinder);
await tester.pumpAndSettle();
verify(
() => urlLauncher.launch(
'https://flutter.dev',
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
);
},
);
testWidgets(
'tapping on "Firebase" opens the firebase website',
(tester) async {
when(() => urlLauncher.canLaunch(any())).thenAnswer((_) async => true);
when(
() => urlLauncher.launch(
any(),
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
await tester.pumpApp(const Footer());
final firebaseTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && tapTextSpan(widget, 'Firebase'),
);
await tester.tap(firebaseTextFinder);
await tester.pumpAndSettle();
verify(
() => urlLauncher.launch(
'https://firebase.google.com',
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
);
},
);
});
}

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

Loading…
Cancel
Save