fix: merge conflicts fixed

pull/406/head
RuiAlonso 3 years ago
commit 9a0e8f063d

@ -0,0 +1,5 @@
{
"projects": {
"default": "pinball-dev"
}
}

@ -11,6 +11,6 @@ jobs:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with: with:
flutter_channel: stable flutter_channel: stable
flutter_version: 2.10.0 flutter_version: 2.10.5
coverage_excludes: "lib/gen/*.dart" coverage_excludes: "lib/gen/*.dart"
test_optimization: false test_optimization: false

@ -5,7 +5,11 @@
"hosting": { "hosting": {
"public": "build/web", "public": "build/web",
"site": "ashehwkdkdjruejdnensjsjdne", "site": "ashehwkdkdjruejdnensjsjdne",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"], "ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"headers": [ "headers": [
{ {
"source": "**/*.@(jpg|jpeg|gif|png)", "source": "**/*.@(jpg|jpeg|gif|png)",

@ -17,7 +17,7 @@ service cloud.firestore {
} }
// Leaderboard can be read if it doesn't contain any prohibited initials // Leaderboard can be read if it doesn't contain any prohibited initials
allow read: if !prohibited(resource.data.playerInitials); allow read: if isAuthedUser(request.auth);
// A leaderboard entry can be created if the user is authenticated, // A leaderboard entry can be created if the user is authenticated,
// it's 3 characters long, and not a prohibited combination. // it's 3 characters long, and not a prohibited combination.

@ -0,0 +1 @@
node_modules/

@ -0,0 +1,28 @@
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const db = admin.firestore();
exports.timedLeaderboardCleanup = functions.firestore
.document("leaderboard/{leaderboardEntry}")
.onCreate(async (_, __) => {
functions.logger.info(
"Document created, getting all leaderboard documents"
);
const snapshot = await db
.collection("leaderboard")
.orderBy("score", "desc")
.offset(10)
.get();
functions.logger.info(
`Preparing to delete ${snapshot.docs.length} documents.`
);
try {
await Promise.all(snapshot.docs.map((doc) => doc.ref.delete()));
functions.logger.info("Success");
} catch (error) {
functions.logger.error(`Failed to delete documents ${error}`);
}
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,23 @@
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "16"
},
"main": "index.js",
"dependencies": {
"firebase-admin": "^10.0.2",
"firebase-functions": "^3.18.0"
},
"devDependencies": {
"firebase-functions-test": "^0.2.0"
},
"private": true
}

@ -39,6 +39,7 @@ class App extends StatelessWidget {
providers: [ providers: [
BlocProvider(create: (_) => CharacterThemeCubit()), BlocProvider(create: (_) => CharacterThemeCubit()),
BlocProvider(create: (_) => StartGameBloc()), BlocProvider(create: (_) => StartGameBloc()),
BlocProvider(create: (_) => GameBloc()),
], ],
child: MaterialApp( child: MaterialApp(
title: 'I/O Pinball', title: 'I/O Pinball',

@ -1,76 +0,0 @@
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,
),
),
],
),
);
}
}

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

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

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

@ -0,0 +1,38 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Behavior that handles playing a bonus sound effect
class BonusNoiseBehavior extends Component {
@override
Future<void> onLoad() async {
await add(
FlameBlocListener<GameBloc, GameState>(
listenWhen: (previous, current) {
return previous.bonusHistory.length != current.bonusHistory.length;
},
onNewState: (state) {
final bonus = state.bonusHistory.last;
final audioPlayer = readProvider<PinballPlayer>();
switch (bonus) {
case GameBonus.googleWord:
audioPlayer.play(PinballAudio.google);
break;
case GameBonus.sparkyTurboCharge:
audioPlayer.play(PinballAudio.sparky);
break;
case GameBonus.dinoChomp:
break;
case GameBonus.androidSpaceship:
break;
case GameBonus.dashNest:
break;
}
},
),
);
}
}

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

@ -32,7 +32,6 @@ final _subtitleTextPaint = TextPaint(
/// {@template initials_input_display} /// {@template initials_input_display}
/// Display that handles the user input on the game over view. /// Display that handles the user input on the game over view.
/// {@endtemplate} /// {@endtemplate}
// TODO(allisonryan0002): add mobile input buttons.
class InitialsInputDisplay extends Component with HasGameRef { class InitialsInputDisplay extends Component with HasGameRef {
/// {@macro initials_input_display} /// {@macro initials_input_display}
InitialsInputDisplay({ InitialsInputDisplay({

@ -9,7 +9,6 @@ import 'package:pinball_flame/pinball_flame.dart';
/// ///
/// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s. /// The [BottomGroup] consists of [Flipper]s, [Baseboard]s and [Kicker]s.
/// {@endtemplate} /// {@endtemplate}
// TODO(allisonryan0002): Consider renaming.
class BottomGroup extends Component with ZIndex { class BottomGroup extends Component with ZIndex {
/// {@macro bottom_group} /// {@macro bottom_group}
BottomGroup() BottomGroup()

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart'; export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart'; export 'backbox/backbox.dart';
export 'bottom_group.dart'; export 'bottom_group.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
export 'controlled_plunger.dart'; export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart'; export 'dino_desert/dino_desert.dart';
@ -12,4 +11,4 @@ export 'google_word/google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'multiballs/multiballs.dart'; export 'multiballs/multiballs.dart';
export 'multipliers/multipliers.dart'; export 'multipliers/multipliers.dart';
export 'sparky_scorch.dart'; export 'sparky_scorch/sparky_scorch.dart';

@ -1,66 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template controlled_ball}
/// A [Ball] with a [BallController] attached.
///
/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is
/// not over, a new [Ball] will be spawned.
/// {@endtemplate}
class ControlledBall extends Ball with Controls<BallController> {
/// A [Ball] that launches from the [Plunger].
ControlledBall.launch({
required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this);
layer = Layer.launcher;
zIndex = ZIndexes.ballOnLaunchRamp;
}
/// {@macro controlled_ball}
ControlledBall.bonus({
required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this);
zIndex = ZIndexes.ballOnBoard;
}
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super() {
controller = BallController(this);
zIndex = ZIndexes.ballOnBoard;
}
}
/// {@template ball_controller}
/// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate}
class BallController extends ComponentController<Ball>
with FlameBlocReader<GameBloc, GameState> {
/// {@macro ball_controller}
BallController(Ball ball) : super(ball);
/// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge
/// sequence runs, then boosts the ball out of the computer.
Future<void> turboCharge() async {
bloc.add(const SparkyTurboChargeActivated());
component.stop();
// TODO(alestiago): Refactor this hard coded duration once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1564
await Future<void>.delayed(
const Duration(milliseconds: 2583),
);
component.resume();
await component.add(
BallTurboChargingBehavior(impulse: Vector2(40, 110)),
);
}
}

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

@ -1,9 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// Bonus obtained at the [FlutterForest]. /// Bonus obtained at the [FlutterForest].
/// ///
@ -25,9 +25,6 @@ class FlutterForestBonusBehavior extends Component
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single; final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
for (final bumper in bumpers) { for (final bumper in bumpers) {
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
bumper.bloc.stream.listen((state) { bumper.bloc.stream.listen((state) {
final activatedAllBumpers = bumpers.every( final activatedAllBumpers = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active, (bumper) => bumper.bloc.state == DashNestBumperState.active,
@ -41,10 +38,14 @@ class FlutterForestBonusBehavior extends Component
if (signpost.bloc.isFullyProgressed()) { if (signpost.bloc.isFullyProgressed()) {
bloc.add(const BonusActivated(GameBonus.dashNest)); bloc.add(const BonusActivated(GameBonus.dashNest));
final characterTheme =
readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme;
canvas.add( canvas.add(
ControlledBall.bonus( Ball(assetPath: characterTheme.ball.keyName)
characterTheme: readProvider<CharacterTheme>(), ..initialPosition = Vector2(29.2, -24.5)
)..initialPosition = Vector2(29.2, -24.5), ..zIndex = ZIndexes.ballOnBoard,
); );
animatronic.playing = true; animatronic.playing = true;
signpost.bloc.onProgressed(); signpost.bloc.onProgressed();

@ -1,9 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// Listens to the [GameBloc] and updates the game accordingly. /// Listens to the [GameBloc] and updates the game accordingly.
class GameBlocStatusListener extends Component class GameBlocStatusListener extends Component
@ -26,7 +26,9 @@ class GameBlocStatusListener extends Component
readProvider<PinballPlayer>().play(PinballAudio.gameOverVoiceOver); readProvider<PinballPlayer>().play(PinballAudio.gameOverVoiceOver);
gameRef.descendants().whereType<Backbox>().first.requestInitials( gameRef.descendants().whereType<Backbox>().first.requestInitials(
score: state.displayScore, score: state.displayScore,
character: readProvider<CharacterTheme>(), character: readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme,
); );
break; break;
} }

@ -1,7 +1,6 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -14,15 +13,11 @@ class GoogleWordBonusBehavior extends Component
final googleLetters = parent.children.whereType<GoogleLetter>(); final googleLetters = parent.children.whereType<GoogleLetter>();
for (final letter in googleLetters) { for (final letter in googleLetters) {
// TODO(alestiago): Refactor subscription management once the following is
// merged:
// https://github.com/flame-engine/flame/pull/1538
letter.bloc.stream.listen((_) { letter.bloc.stream.listen((_) {
final achievedBonus = googleLetters final achievedBonus = googleLetters
.every((letter) => letter.bloc.state == GoogleLetterState.lit); .every((letter) => letter.bloc.state == GoogleLetterState.lit);
if (achievedBonus) { if (achievedBonus) {
readProvider<PinballPlayer>().play(PinballAudio.google);
bloc.add(const BonusActivated(GameBonus.googleWord)); bloc.add(const BonusActivated(GameBonus.googleWord));
for (final letter in googleLetters) { for (final letter in googleLetters) {
letter.bloc.onReset(); letter.bloc.onReset();

@ -0,0 +1 @@
export 'sparky_computer_bonus_behavior.dart';

@ -0,0 +1,24 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.sparkyTurboCharge] when a [Ball] enters the
/// [SparkyComputer].
class SparkyComputerBonusBehavior extends Component
with ParentIsA<SparkyScorch>, FlameBlocReader<GameBloc, GameState> {
@override
void onMount() {
super.onMount();
final sparkyComputer = parent.firstChild<SparkyComputer>()!;
final animatronic = parent.firstChild<SparkyAnimatronic>()!;
sparkyComputer.bloc.stream.listen((state) async {
final listenWhen = state == SparkyComputerState.withBall;
if (!listenWhen) return;
bloc.add(const BonusActivated(GameBonus.sparkyTurboCharge));
animatronic.playing = true;
});
}
}

@ -1,9 +1,9 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/components/components.dart'; import 'package:pinball/game/components/sparky_scorch/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_scorch} /// {@template sparky_scorch}
@ -33,51 +33,20 @@ class SparkyScorch extends Component {
BumperNoiseBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13.2, -49.9),
SparkyAnimatronic()..position = Vector2(-14, -58.2), SparkyAnimatronic()..position = Vector2(-14, -58.2),
SparkyComputer(), SparkyComputer(
], children: [
); ScoringContactBehavior(points: Points.twoHundredThousand)
} ..applyTo(['turbo_charge_sensor']),
],
/// {@template sparky_computer_sensor} ),
/// Small sensor body used to detect when a ball has entered the SparkyComputerBonusBehavior(),
/// [SparkyComputer].
/// {@endtemplate}
class SparkyComputerSensor extends BodyComponent
with InitialPosition, ContactCallbacks {
/// {@macro sparky_computer_sensor}
SparkyComputerSensor()
: super(
renderBody: false,
children: [
ScoringContactBehavior(points: Points.twentyThousand),
], ],
); );
@override /// Creates [SparkyScorch] without any children.
Body createBody() { ///
final shape = PolygonShape() /// This can be used for testing [SparkyScorch]'s behaviors in isolation.
..setAsBox( @visibleForTesting
1, SparkyScorch.test();
0.1,
Vector2.zero(),
-0.18,
);
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.turboCharge();
gameRef.firstChild<SparkyAnimatronic>()?.playing = true;
}
} }

@ -11,15 +11,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends PinballForge2DGame class PinballGame extends PinballForge2DGame
with HasKeyboardHandlerComponents, MultiTouchTapDetector, HasTappables { with HasKeyboardHandlerComponents, MultiTouchTapDetector, HasTappables {
PinballGame({ PinballGame({
required CharacterTheme characterTheme, required CharacterThemeCubit characterThemeBloc,
required this.leaderboardRepository, required this.leaderboardRepository,
required GameBloc gameBloc, required GameBloc gameBloc,
required AppLocalizations l10n, required AppLocalizations l10n,
@ -27,7 +27,7 @@ class PinballGame extends PinballForge2DGame
}) : focusNode = FocusNode(), }) : focusNode = FocusNode(),
_gameBloc = gameBloc, _gameBloc = gameBloc,
_player = player, _player = player,
_characterTheme = characterTheme, _characterThemeBloc = characterThemeBloc,
_l10n = l10n, _l10n = l10n,
super( super(
gravity: Vector2(0, 30), gravity: Vector2(0, 30),
@ -43,7 +43,7 @@ class PinballGame extends PinballForge2DGame
final FocusNode focusNode; final FocusNode focusNode;
final CharacterTheme _characterTheme; final CharacterThemeCubit _characterThemeBloc;
final PinballPlayer _player; final PinballPlayer _player;
@ -56,19 +56,27 @@ class PinballGame extends PinballForge2DGame
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await add( await add(
FlameBlocProvider<GameBloc, GameState>.value( FlameMultiBlocProvider(
value: _gameBloc, providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: _gameBloc,
),
FlameBlocProvider<CharacterThemeCubit, CharacterThemeState>.value(
value: _characterThemeBloc,
),
],
children: [ children: [
MultiFlameProvider( MultiFlameProvider(
providers: [ providers: [
FlameProvider<PinballPlayer>.value(_player), FlameProvider<PinballPlayer>.value(_player),
FlameProvider<CharacterTheme>.value(_characterTheme),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository), FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<AppLocalizations>.value(_l10n), FlameProvider<AppLocalizations>.value(_l10n),
], ],
children: [ children: [
BonusNoiseBehavior(),
GameBlocStatusListener(), GameBlocStatusListener(),
BallSpawningBehavior(), BallSpawningBehavior(),
BallThemingBehavior(),
CameraFocusingBehavior(), CameraFocusingBehavior(),
CanvasComponent( CanvasComponent(
onSpritePainted: (paint) { onSpritePainted: (paint) {
@ -160,13 +168,13 @@ class PinballGame extends PinballForge2DGame
class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({ DebugPinballGame({
required CharacterTheme characterTheme, required CharacterThemeCubit characterThemeBloc,
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballPlayer player, required PinballPlayer player,
required GameBloc gameBloc, required GameBloc gameBloc,
}) : super( }) : super(
characterTheme: characterTheme, characterThemeBloc: characterThemeBloc,
player: player, player: player,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
l10n: l10n, l10n: l10n,
@ -190,8 +198,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
if (info.raw.kind == PointerDeviceKind.mouse) { if (info.raw.kind == PointerDeviceKind.mouse) {
final canvas = descendants().whereType<ZCanvasComponent>().single; final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug() final ball = Ball()..initialPosition = info.eventPosition.game;
..initialPosition = info.eventPosition.game;
canvas.add(ball); canvas.add(ball);
} }
} }
@ -218,7 +225,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
void _turboChargeBall(Vector2 line) { void _turboChargeBall(Vector2 line) {
final canvas = descendants().whereType<ZCanvasComponent>().single; final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()..initialPosition = lineStart!; final ball = Ball()..initialPosition = lineStart!;
final impulse = line * -1 * 10; final impulse = line * -1 * 10;
ball.add(BallTurboChargingBehavior(impulse: impulse)); ball.add(BallTurboChargingBehavior(impulse: impulse));
canvas.add(ball); canvas.add(ball);
@ -246,7 +253,6 @@ class PreviewLine extends PositionComponent with HasGameRef<DebugPinballGame> {
} }
} }
// TODO(wolfenrain): investigate this CI failure.
class _DebugInformation extends Component with HasGameRef<DebugPinballGame> { class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
@override @override
PositionType get positionType => PositionType.widget; PositionType get positionType => PositionType.widget;
@ -264,7 +270,7 @@ class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
void render(Canvas canvas) { void render(Canvas canvas) {
final debugText = [ final debugText = [
'FPS: ${gameRef.fps().toStringAsFixed(1)}', 'FPS: ${gameRef.fps().toStringAsFixed(1)}',
'BALLS: ${gameRef.descendants().whereType<ControlledBall>().length}', 'BALLS: ${gameRef.descendants().whereType<Ball>().length}',
].join(' | '); ].join(' | ');
final height = _debugTextPaint.measureTextHeight(debugText); final height = _debugTextPaint.measureTextHeight(debugText);

@ -7,7 +7,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/more_information/more_information.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
@ -21,59 +23,44 @@ class PinballGamePage extends StatelessWidget {
final bool isDebugMode; final bool isDebugMode;
static Route route({ static Route route({bool isDebugMode = kDebugMode}) {
bool isDebugMode = kDebugMode,
}) {
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (context) { builder: (_) => PinballGamePage(isDebugMode: isDebugMode),
return PinballGamePage(
isDebugMode: isDebugMode,
);
},
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final characterTheme = final characterThemeBloc = context.read<CharacterThemeCubit>();
context.read<CharacterThemeCubit>().state.characterTheme;
final player = context.read<PinballPlayer>(); final player = context.read<PinballPlayer>();
final leaderboardRepository = context.read<LeaderboardRepository>(); final leaderboardRepository = context.read<LeaderboardRepository>();
final gameBloc = context.read<GameBloc>();
final game = isDebugMode
? DebugPinballGame(
characterThemeBloc: characterThemeBloc,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
gameBloc: gameBloc,
)
: PinballGame(
characterThemeBloc: characterThemeBloc,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
gameBloc: gameBloc,
);
final loadables = [
...game.preLoadAssets(),
...player.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
];
return BlocProvider( return BlocProvider(
create: (_) => GameBloc(), create: (_) => AssetsManagerCubit(loadables)..load(),
child: Builder( child: PinballGameView(game: game),
builder: (context) {
final gameBloc = context.read<GameBloc>();
final game = isDebugMode
? DebugPinballGame(
characterTheme: characterTheme,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
gameBloc: gameBloc,
)
: PinballGame(
characterTheme: characterTheme,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
gameBloc: gameBloc,
);
final loadables = [
...game.preLoadAssets(),
...player.load(),
...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(),
];
return BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
child: PinballGameView(game: game),
);
},
),
); );
} }
} }
@ -142,6 +129,7 @@ class PinballGameLoadedView extends StatelessWidget {
), ),
), ),
const _PositionedGameHud(), const _PositionedGameHud(),
const _PositionedInfoIcon(),
], ],
), ),
); );
@ -159,6 +147,7 @@ class _PositionedGameHud extends StatelessWidget {
final isGameOver = context.select( final isGameOver = context.select(
(GameBloc bloc) => bloc.state.status.isGameOver, (GameBloc bloc) => bloc.state.status.isGameOver,
); );
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
@ -174,3 +163,27 @@ class _PositionedGameHud extends StatelessWidget {
); );
} }
} }
class _PositionedInfoIcon extends StatelessWidget {
const _PositionedInfoIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Positioned(
top: 0,
left: 0,
child: BlocBuilder<GameBloc, GameState>(
builder: (context, state) {
return Visibility(
visible: state.status.isGameOver,
child: IconButton(
iconSize: 50,
icon: Assets.images.linkBox.infoIcon.image(),
onPressed: () => showMoreInformationDialog(context),
),
);
},
),
);
}
}

@ -104,22 +104,6 @@
"@toSubmit": { "@toSubmit": {
"description": "Ending text displayed on initials input screen informational text span" "description": "Ending text displayed on initials input screen informational text span"
}, },
"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"
},
"linkBoxTitle": "Resources", "linkBoxTitle": "Resources",
"@linkBoxTitle": { "@linkBoxTitle": {
"description": "Text shown on the link box title section." "description": "Text shown on the link box title section."

@ -0,0 +1 @@
export 'more_information_dialog.dart';

@ -0,0 +1,218 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Inflates [MoreInformationDialog] using [showDialog].
Future<void> showMoreInformationDialog(BuildContext context) {
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
return showDialog<void>(
context: context,
barrierColor: PinballColors.transparent,
barrierDismissible: true,
builder: (_) {
return Center(
child: SizedBox(
height: gameWidgetWidth * 0.87,
width: gameWidgetWidth,
child: const MoreInformationDialog(),
),
);
},
);
}
/// {@template more_information_dialog}
/// Dialog used to show informational links
/// {@endtemplate}
class MoreInformationDialog extends StatelessWidget {
/// {@macro more_information_dialog}
const MoreInformationDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: PinballColors.transparent,
child: _LinkBoxDecoration(
child: Column(
children: const [
SizedBox(height: 16),
_LinkBoxHeader(),
Expanded(
child: _LinkBoxBody(),
),
],
),
),
);
}
}
class _LinkBoxHeader extends StatelessWidget {
const _LinkBoxHeader({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final indent = MediaQuery.of(context).size.width / 5;
return Column(
children: [
Text(
l10n.linkBoxTitle,
style: Theme.of(context).textTheme.headline3!.copyWith(
color: PinballColors.blue,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Divider(
color: PinballColors.white,
endIndent: indent,
indent: indent,
thickness: 2,
),
],
);
}
}
class _LinkBoxDecoration extends StatelessWidget {
const _LinkBoxDecoration({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const CrtBackground().copyWith(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.all(
color: PinballColors.white,
width: 5,
),
),
child: child,
);
}
}
class _LinkBoxBody extends StatelessWidget {
const _LinkBoxBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const _MadeWithFlutterAndFirebase(),
_TextLink(
text: l10n.linkBoxOpenSourceCode,
link: _MoreInformationUrl.openSourceCode,
),
_TextLink(
text: l10n.linkBoxGoogleIOText,
link: _MoreInformationUrl.googleIOEvent,
),
_TextLink(
text: l10n.linkBoxFlutterGames,
link: _MoreInformationUrl.flutterGamesWebsite,
),
_TextLink(
text: l10n.linkBoxHowItsMade,
link: _MoreInformationUrl.howItsMadeArticle,
),
_TextLink(
text: l10n.linkBoxTermsOfService,
link: _MoreInformationUrl.termsOfService,
),
_TextLink(
text: l10n.linkBoxPrivacyPolicy,
link: _MoreInformationUrl.privacyPolicy,
),
],
);
}
}
class _TextLink extends StatelessWidget {
const _TextLink({
Key? key,
required this.text,
required this.link,
}) : super(key: key);
final String text;
final String link;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: () => openLink(link),
child: Text(
text,
style: theme.textTheme.headline5!.copyWith(
color: PinballColors.white,
),
overflow: TextOverflow.ellipsis,
),
);
}
}
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.linkBoxMadeWithText,
style: theme.textTheme.headline5!.copyWith(color: PinballColors.white),
children: <TextSpan>[
TextSpan(
text: l10n.linkBoxFlutterLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink(_MoreInformationUrl.flutterWebsite),
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ' & '),
TextSpan(
text: l10n.linkBoxFirebaseLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink(_MoreInformationUrl.firebaseWebsite),
style: theme.textTheme.headline5!.copyWith(
decoration: TextDecoration.underline,
),
),
],
),
);
}
}
abstract class _MoreInformationUrl {
static const flutterWebsite = 'https://flutter.dev';
static const firebaseWebsite = 'https://firebase.google.com';
static const openSourceCode = 'https://github.com/VGVentures/pinball';
static const googleIOEvent = 'https://events.google.com/io/';
static const flutterGamesWebsite = 'http://flutter.dev/games';
static const howItsMadeArticle =
'https://medium.com/flutter/i-o-pinball-powered-by-flutter-and-firebase-d22423f3f5d';
static const termsOfService = 'https://policies.google.com/terms';
static const privacyPolicy = 'https://policies.google.com/privacy';
}

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

@ -1,5 +1,4 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
// TODO(allisonryan0002): Document this section when the API is stable.
part of 'character_theme_cubit.dart'; part of 'character_theme_cubit.dart';

@ -44,31 +44,10 @@ class LeaderboardRepository {
final tenthPositionScore = leaderboard[9].score; final tenthPositionScore = leaderboard[9].score;
if (entry.score > tenthPositionScore) { if (entry.score > tenthPositionScore) {
await _saveScore(entry); await _saveScore(entry);
await _deleteScoresUnder(tenthPositionScore);
} }
} }
} }
/// Determines if the given [initials] are allowed.
Future<bool> areInitialsAllowed({required String initials}) async {
// Initials can only be three uppercase A-Z letters
final initialsRegex = RegExp(r'^[A-Z]{3}$');
if (!initialsRegex.hasMatch(initials)) {
return false;
}
try {
final document = await _firebaseFirestore
.collection('prohibitedInitials')
.doc('list')
.get();
final prohibitedInitials =
document.get('prohibitedInitials') as List<String>;
return !prohibitedInitials.contains(initials);
} on Exception catch (error, stackTrace) {
throw FetchProhibitedInitialsException(error, stackTrace);
}
}
Future<List<LeaderboardEntryData>> _fetchLeaderboardSortedByScore() async { Future<List<LeaderboardEntryData>> _fetchLeaderboardSortedByScore() async {
try { try {
final querySnapshot = await _firebaseFirestore final querySnapshot = await _firebaseFirestore
@ -91,23 +70,6 @@ class LeaderboardRepository {
throw AddLeaderboardEntryException(error, stackTrace); throw AddLeaderboardEntryException(error, stackTrace);
} }
} }
Future<void> _deleteScoresUnder(int score) async {
try {
final querySnapshot = await _firebaseFirestore
.collection(_leaderboardCollectionName)
.where(_scoreFieldName, isLessThanOrEqualTo: score)
.get();
final documents = querySnapshot.docs;
for (final document in documents) {
await document.reference.delete();
}
} on LeaderboardDeserializationException {
rethrow;
} on Exception catch (error, stackTrace) {
throw DeleteLeaderboardException(error, stackTrace);
}
}
} }
extension on List<QueryDocumentSnapshot> { extension on List<QueryDocumentSnapshot> {

@ -40,16 +40,6 @@ class FetchLeaderboardException extends LeaderboardException {
: super(error, stackTrace); : super(error, stackTrace);
} }
/// {@template delete_leaderboard_exception}
/// Exception thrown when failure occurs while deleting the leaderboard under
/// the tenth position.
/// {@endtemplate}
class DeleteLeaderboardException extends LeaderboardException {
/// {@macro fetch_top_10_leaderboard_exception}
const DeleteLeaderboardException(Object error, StackTrace stackTrace)
: super(error, stackTrace);
}
/// {@template add_leaderboard_entry_exception} /// {@template add_leaderboard_entry_exception}
/// Exception thrown when failure occurs while adding entry to leaderboard. /// Exception thrown when failure occurs while adding entry to leaderboard.
/// {@endtemplate} /// {@endtemplate}
@ -58,12 +48,3 @@ class AddLeaderboardEntryException extends LeaderboardException {
const AddLeaderboardEntryException(Object error, StackTrace stackTrace) const AddLeaderboardEntryException(Object error, StackTrace stackTrace)
: super(error, stackTrace); : super(error, stackTrace);
} }
/// {@template fetch_prohibited_initials_exception}
/// Exception thrown when failure occurs while fetching prohibited initials.
/// {@endtemplate}
class FetchProhibitedInitialsException extends LeaderboardException {
/// {@macro fetch_prohibited_initials_exception}
const FetchProhibitedInitialsException(Object error, StackTrace stackTrace)
: super(error, stackTrace);
}

@ -21,9 +21,6 @@ class _MockQueryDocumentSnapshot extends Mock
class _MockDocumentReference extends Mock class _MockDocumentReference extends Mock
implements DocumentReference<Map<String, dynamic>> {} implements DocumentReference<Map<String, dynamic>> {}
class _MockDocumentSnapshot extends Mock
implements DocumentSnapshot<Map<String, dynamic>> {}
void main() { void main() {
group('LeaderboardRepository', () { group('LeaderboardRepository', () {
late FirebaseFirestore firestore; late FirebaseFirestore firestore;
@ -245,73 +242,10 @@ void main() {
); );
}); });
test(
'throws DeleteLeaderboardException '
'when deleting scores outside the top 10 fails', () async {
final deleteQuery = _MockQuery();
final deleteQuerySnapshot = _MockQuerySnapshot();
final newScore = LeaderboardEntryData(
playerInitials: 'ABC',
score: 15000,
character: CharacterType.android,
);
final leaderboardScores = [
10000,
9500,
9000,
8500,
8000,
7500,
7000,
6500,
6000,
5500,
5000,
];
final deleteDocumentSnapshots = [5500, 5000].map((score) {
final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash',
'playerInitials': 'AAA',
'score': score
});
when(() => queryDocumentSnapshot.id).thenReturn('id$score');
when(() => queryDocumentSnapshot.reference)
.thenReturn(documentReference);
return queryDocumentSnapshot;
}).toList();
when(deleteQuery.get).thenAnswer((_) async => deleteQuerySnapshot);
when(() => deleteQuerySnapshot.docs)
.thenReturn(deleteDocumentSnapshots);
final queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash',
'playerInitials': 'AAA',
'score': score
});
when(() => queryDocumentSnapshot.id).thenReturn('id$score');
when(() => queryDocumentSnapshot.reference)
.thenReturn(documentReference);
return queryDocumentSnapshot;
}).toList();
when(
() => collectionReference.where('score', isLessThanOrEqualTo: 5500),
).thenAnswer((_) => deleteQuery);
when(() => documentReference.delete()).thenThrow(Exception('oops'));
when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots);
expect(
() => leaderboardRepository.addLeaderboardEntry(newScore),
throwsA(isA<DeleteLeaderboardException>()),
);
});
test( test(
'saves the new score when there are more than 10 scores in the ' 'saves the new score when there are more than 10 scores in the '
'leaderboard and the new score is higher than the lowest top 10, and ' 'leaderboard and the new score is higher than the lowest top 10',
'deletes the scores that are not in the top 10 anymore', () async { () async {
final deleteQuery = _MockQuery();
final deleteQuerySnapshot = _MockQuerySnapshot();
final newScore = LeaderboardEntryData( final newScore = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 15000, score: 15000,
@ -330,21 +264,6 @@ void main() {
5500, 5500,
5000, 5000,
]; ];
final deleteDocumentSnapshots = [5500, 5000].map((score) {
final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash',
'playerInitials': 'AAA',
'score': score
});
when(() => queryDocumentSnapshot.id).thenReturn('id$score');
when(() => queryDocumentSnapshot.reference)
.thenReturn(documentReference);
return queryDocumentSnapshot;
}).toList();
when(deleteQuery.get).thenAnswer((_) async => deleteQuerySnapshot);
when(() => deleteQuerySnapshot.docs)
.thenReturn(deleteDocumentSnapshots);
final queryDocumentSnapshots = leaderboardScores.map((score) { final queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = _MockQueryDocumentSnapshot(); final queryDocumentSnapshot = _MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{ when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
@ -357,105 +276,11 @@ void main() {
.thenReturn(documentReference); .thenReturn(documentReference);
return queryDocumentSnapshot; return queryDocumentSnapshot;
}).toList(); }).toList();
when(
() => collectionReference.where('score', isLessThanOrEqualTo: 5500),
).thenAnswer((_) => deleteQuery);
when(() => documentReference.delete())
.thenAnswer((_) async => Future.value());
when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots);
await leaderboardRepository.addLeaderboardEntry(newScore); await leaderboardRepository.addLeaderboardEntry(newScore);
verify(() => collectionReference.add(newScore.toJson())).called(1); verify(() => collectionReference.add(newScore.toJson())).called(1);
verify(() => documentReference.delete()).called(2);
});
});
group('areInitialsAllowed', () {
late LeaderboardRepository leaderboardRepository;
late CollectionReference<Map<String, dynamic>> collectionReference;
late DocumentReference<Map<String, dynamic>> documentReference;
late DocumentSnapshot<Map<String, dynamic>> documentSnapshot;
setUp(() async {
collectionReference = _MockCollectionReference();
documentReference = _MockDocumentReference();
documentSnapshot = _MockDocumentSnapshot();
leaderboardRepository = LeaderboardRepository(firestore);
when(() => firestore.collection('prohibitedInitials'))
.thenReturn(collectionReference);
when(() => collectionReference.doc('list'))
.thenReturn(documentReference);
when(() => documentReference.get())
.thenAnswer((_) async => documentSnapshot);
when<dynamic>(() => documentSnapshot.get('prohibitedInitials'))
.thenReturn(['BAD']);
});
test('returns true if initials are three letters and allowed', () async {
final isUsernameAllowedResponse =
await leaderboardRepository.areInitialsAllowed(
initials: 'ABC',
);
expect(
isUsernameAllowedResponse,
isTrue,
);
}); });
test(
'returns false if initials are shorter than 3 characters',
() async {
final areInitialsAllowedResponse =
await leaderboardRepository.areInitialsAllowed(initials: 'AB');
expect(areInitialsAllowedResponse, isFalse);
},
);
test(
'returns false if initials are longer than 3 characters',
() async {
final areInitialsAllowedResponse =
await leaderboardRepository.areInitialsAllowed(initials: 'ABCD');
expect(areInitialsAllowedResponse, isFalse);
},
);
test(
'returns false if initials contain a lowercase letter',
() async {
final areInitialsAllowedResponse =
await leaderboardRepository.areInitialsAllowed(initials: 'AbC');
expect(areInitialsAllowedResponse, isFalse);
},
);
test(
'returns false if initials contain a special character',
() async {
final areInitialsAllowedResponse =
await leaderboardRepository.areInitialsAllowed(initials: 'A@C');
expect(areInitialsAllowedResponse, isFalse);
},
);
test('returns false if initials are forbidden', () async {
final areInitialsAllowedResponse =
await leaderboardRepository.areInitialsAllowed(initials: 'BAD');
expect(areInitialsAllowedResponse, isFalse);
});
test(
'throws FetchProhibitedInitialsException when Exception occurs '
'when trying to retrieve information from firestore',
() async {
when(() => firestore.collection('prohibitedInitials'))
.thenThrow(Exception('oops'));
expect(
() => leaderboardRepository.areInitialsAllowed(initials: 'ABC'),
throwsA(isA<FetchProhibitedInitialsException>()),
);
},
);
}); });
}); });
} }

@ -14,13 +14,13 @@ class $AssetsMusicGen {
class $AssetsSfxGen { class $AssetsSfxGen {
const $AssetsSfxGen(); const $AssetsSfxGen();
String get afterLaunch => 'assets/sfx/after_launch.mp3';
String get bumperA => 'assets/sfx/bumper_a.mp3'; String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3'; String get bumperB => 'assets/sfx/bumper_b.mp3';
String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3'; String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3';
String get google => 'assets/sfx/google.mp3'; String get google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3'; String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';
String get launcher => 'assets/sfx/launcher.mp3'; String get launcher => 'assets/sfx/launcher.mp3';
String get sparky => 'assets/sfx/sparky.mp3';
} }
class Assets { class Assets {

@ -25,6 +25,9 @@ enum PinballAudio {
/// Launcher /// Launcher
launcher, launcher,
/// Sparky
sparky,
} }
/// Defines the contract of the creation of an [AudioPool]. /// Defines the contract of the creation of an [AudioPool].
@ -161,6 +164,11 @@ class PinballPlayer {
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,
path: Assets.sfx.google, path: Assets.sfx.google,
), ),
PinballAudio.sparky: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.sparky,
),
PinballAudio.launcher: _SimplePlayAudio( PinballAudio.launcher: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio, preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio, playSingleAudio: _playSingleAudio,

@ -141,6 +141,10 @@ void main() {
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/google.mp3'), .onCall('packages/pinball_audio/assets/sfx/google.mp3'),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/sparky.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio.onCall( () => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3', 'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
@ -211,7 +215,7 @@ void main() {
}); });
}); });
group('googleBonus', () { group('google', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(player.load());
player.play(PinballAudio.google); player.play(PinballAudio.google);
@ -223,6 +227,18 @@ void main() {
}); });
}); });
group('sparky', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.sparky);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.sparky}'),
).called(1);
});
});
group('launcher', () { group('launcher', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await Future.wait(player.load()); await Future.wait(player.load());

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@ -93,8 +93,6 @@ class AndroidBumper extends BodyComponent with InitialPosition, ZIndex {
/// Creates an [AndroidBumper] without any children. /// Creates an [AndroidBumper] without any children.
/// ///
/// This can be used for testing [AndroidBumper]'s behaviors in isolation. /// This can be used for testing [AndroidBumper]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
AndroidBumper.test({ AndroidBumper.test({
required this.bloc, required this.bloc,
@ -105,8 +103,6 @@ class AndroidBumper extends BodyComponent with InitialPosition, ZIndex {
final double _minorRadius; final double _minorRadius;
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final AndroidBumperCubit bloc; final AndroidBumperCubit bloc;

@ -38,16 +38,12 @@ class AndroidSpaceship extends Component {
/// Creates an [AndroidSpaceship] without any children. /// Creates an [AndroidSpaceship] without any children.
/// ///
/// This can be used for testing [AndroidSpaceship]'s behaviors in isolation. /// This can be used for testing [AndroidSpaceship]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
AndroidSpaceship.test({ AndroidSpaceship.test({
required this.bloc, required this.bloc,
Iterable<Component>? children, Iterable<Component>? children,
}) : super(children: children); }) : super(children: children);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
final AndroidSpaceshipCubit bloc; final AndroidSpaceshipCubit bloc;
@override @override
@ -129,7 +125,6 @@ class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent
} }
} }
// TODO(allisonryan0002): add pulsing behavior.
class _LightBeamSpriteComponent extends SpriteComponent class _LightBeamSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex { with HasGameRef, ZIndex {
_LightBeamSpriteComponent() _LightBeamSpriteComponent()

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -8,27 +9,27 @@ import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme; import 'package:pinball_theme/pinball_theme.dart' as theme;
export 'behaviors/behaviors.dart'; export 'behaviors/behaviors.dart';
export 'cubit/ball_cubit.dart';
/// {@template ball} /// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around. /// A solid, [BodyType.dynamic] sphere that rolls and bounces around.
/// {@endtemplate} /// {@endtemplate}
class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// {@macro ball} /// {@macro ball}
Ball({ Ball({String? assetPath}) : this._(bloc: BallCubit(), assetPath: assetPath);
String? assetPath,
}) : super( Ball._({required this.bloc, String? assetPath})
: super(
renderBody: false, renderBody: false,
children: [ children: [
_BallSpriteComponent(assetPath: assetPath), FlameBlocProvider<BallCubit, BallState>.value(
value: bloc,
children: [BallSpriteComponent(assetPath: assetPath)],
),
BallScalingBehavior(), BallScalingBehavior(),
BallGravitatingBehavior(), BallGravitatingBehavior(),
], ],
) { ) {
// TODO(ruimiguel): while developing Ball can be launched by clicking mouse,
// and default layer is Layer.all. But on final game Ball will be always be
// be launched from Plunger and LauncherRamp will modify it to Layer.board.
// We need to see what happens if Ball appears from other place like nest
// bumper, it will need to explicit change layer to Layer.board then.
layer = Layer.board; layer = Layer.board;
} }
@ -36,11 +37,22 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// ///
/// This can be used for testing [Ball]'s behaviors in isolation. /// This can be used for testing [Ball]'s behaviors in isolation.
@visibleForTesting @visibleForTesting
Ball.test() Ball.test({
: super( BallCubit? bloc,
children: [_BallSpriteComponent()], String? assetPath,
}) : bloc = bloc ?? BallCubit(),
super(
children: [
FlameBlocProvider<BallCubit, BallState>.value(
value: bloc ?? BallCubit(),
children: [BallSpriteComponent(assetPath: assetPath)],
)
],
); );
/// Bloc to update the ball sprite when a new character is selected.
final BallCubit bloc;
/// The size of the [Ball]. /// The size of the [Ball].
static final Vector2 size = Vector2.all(4.13); static final Vector2 size = Vector2.all(4.13);
@ -60,7 +72,6 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// ///
/// The [Ball] will no longer be affected by any forces, including it's /// The [Ball] will no longer be affected by any forces, including it's
/// weight and those emitted from collisions. /// weight and those emitted from collisions.
// TODO(allisonryan0002): prevent motion from contact with other balls.
void stop() { void stop() {
body body
..gravityScale = Vector2.zero() ..gravityScale = Vector2.zero()
@ -76,21 +87,32 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
} }
} }
class _BallSpriteComponent extends SpriteComponent with HasGameRef { /// {@template ball_sprite_component}
_BallSpriteComponent({ /// Visual representation of the [Ball].
this.assetPath, /// {@endtemplate}
}) : super( @visibleForTesting
anchor: Anchor.center, class BallSpriteComponent extends SpriteComponent
); with HasGameRef, FlameBlocListenable<BallCubit, BallState> {
/// {@macro ball_sprite_component}
BallSpriteComponent({required String? assetPath})
: _assetPath = assetPath,
super(anchor: Anchor.center);
final String? _assetPath;
final String? assetPath; @override
void onNewState(BallState state) {
sprite = Sprite(
gameRef.images.fromCache(state.characterTheme.ball.keyName),
);
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprite = Sprite( final sprite = Sprite(
gameRef.images gameRef.images
.fromCache(assetPath ?? theme.Assets.images.dash.ball.keyName), .fromCache(_assetPath ?? theme.Assets.images.dash.ball.keyName),
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 12.5; size = sprite.originalSize / 12.5;

@ -16,9 +16,12 @@ class BallScalingBehavior extends Component with ParentIsA<Ball> {
parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor; parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor;
parent.firstChild<SpriteComponent>()!.scale.setValues( final ballSprite = parent.descendants().whereType<SpriteComponent>();
scaleFactor, if (ballSprite.isNotEmpty) {
scaleFactor, ballSprite.single.scale.setValues(
); scaleFactor,
scaleFactor,
);
}
} }
} }

@ -0,0 +1,15 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball_theme/pinball_theme.dart';
part 'ball_state.dart';
class BallCubit extends Cubit<BallState> {
BallCubit() : super(const BallState.initial());
void onThemeChanged(CharacterTheme characterTheme) {
emit(BallState(characterTheme: characterTheme));
}
}

@ -0,0 +1,14 @@
// ignore_for_file: public_member_api_docs
part of 'ball_cubit.dart';
class BallState extends Equatable {
const BallState({required this.characterTheme});
const BallState.initial() : this(characterTheme: const DashTheme());
final CharacterTheme characterTheme;
@override
List<Object> get props => [characterTheme];
}

@ -5,7 +5,6 @@ import 'package:flame/extensions.dart';
/// {@template board_dimensions} /// {@template board_dimensions}
/// Contains various board properties and dimensions for global use. /// Contains various board properties and dimensions for global use.
/// {@endtemplate} /// {@endtemplate}
// TODO(allisonryan0002): consider alternatives for global dimensions.
class BoardDimensions { class BoardDimensions {
/// Width and height of the board. /// Width and height of the board.
static final size = Vector2(101.6, 143.8); static final size = Vector2(101.6, 143.8);

@ -14,7 +14,7 @@ class ChromeDinoChompingBehavior extends ContactBehavior<ChromeDino> {
super.beginContact(other, contact); super.beginContact(other, contact);
if (other is! Ball) return; if (other is! Ball) return;
other.firstChild<SpriteComponent>()!.setOpacity(0); other.descendants().whereType<SpriteComponent>().single.setOpacity(0);
parent.bloc.onChomp(other); parent.bloc.onChomp(other);
} }
} }

@ -30,7 +30,7 @@ class ChromeDinoSpittingBehavior extends Component
void _spit() { void _spit() {
parent.bloc.state.ball! parent.bloc.state.ball!
..firstChild<SpriteComponent>()!.setOpacity(1) ..descendants().whereType<SpriteComponent>().single.setOpacity(1)
..body.linearVelocity = Vector2(-50, 0); ..body.linearVelocity = Vector2(-50, 0);
parent.bloc.onSpit(); parent.bloc.onSpit();
} }

@ -38,15 +38,11 @@ class ChromeDino extends BodyComponent
/// Creates a [ChromeDino] without any children. /// Creates a [ChromeDino] without any children.
/// ///
/// This can be used for testing [ChromeDino]'s behaviors in isolation. /// This can be used for testing [ChromeDino]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
ChromeDino.test({ ChromeDino.test({
required this.bloc, required this.bloc,
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final ChromeDinoCubit bloc; final ChromeDinoCubit bloc;
@ -61,13 +57,13 @@ class ChromeDino extends BodyComponent
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
const mouthAngle = -(halfSweepingAngle + 0.28); const mouthAngle = -(halfSweepingAngle + 0.28);
final size = Vector2(5.5, 6); final size = Vector2(6, 6);
final topEdge = PolygonShape() final topEdge = PolygonShape()
..setAsBox( ..setAsBox(
size.x / 2, size.x / 2,
0.1, 0.1,
initialPosition + Vector2(-4.2, -1.4), initialPosition + Vector2(-4, -1.4),
mouthAngle, mouthAngle,
); );
final topEdgeFixtureDef = FixtureDef(topEdge, density: 100); final topEdgeFixtureDef = FixtureDef(topEdge, density: 100);
@ -76,7 +72,7 @@ class ChromeDino extends BodyComponent
..setAsBox( ..setAsBox(
0.1, 0.1,
size.y / 2, size.y / 2,
initialPosition + Vector2(-1.3, 0.5), initialPosition + Vector2(-1, 0.5),
-halfSweepingAngle, -halfSweepingAngle,
); );
final backEdgeFixtureDef = FixtureDef(backEdge, density: 100); final backEdgeFixtureDef = FixtureDef(backEdge, density: 100);
@ -85,7 +81,7 @@ class ChromeDino extends BodyComponent
..setAsBox( ..setAsBox(
size.x / 2, size.x / 2,
0.1, 0.1,
initialPosition + Vector2(-3.5, 4.7), initialPosition + Vector2(-3.3, 4.7),
mouthAngle, mouthAngle,
); );
final bottomEdgeFixtureDef = FixtureDef( final bottomEdgeFixtureDef = FixtureDef(
@ -110,7 +106,7 @@ class ChromeDino extends BodyComponent
..setAsBox( ..setAsBox(
0.2, 0.2,
0.2, 0.2,
initialPosition + Vector2(-3.5, 1.5), initialPosition + Vector2(-3, 1.5),
0, 0,
); );
final insideSensorFixtureDef = FixtureDef( final insideSensorFixtureDef = FixtureDef(

@ -36,5 +36,5 @@ export 'spaceship_rail.dart';
export 'spaceship_ramp/spaceship_ramp.dart'; export 'spaceship_ramp/spaceship_ramp.dart';
export 'sparky_animatronic.dart'; export 'sparky_animatronic.dart';
export 'sparky_bumper/sparky_bumper.dart'; export 'sparky_bumper/sparky_bumper.dart';
export 'sparky_computer.dart'; export 'sparky_computer/sparky_computer.dart';
export 'z_indexes.dart'; export 'z_indexes.dart';

@ -90,8 +90,6 @@ class DashNestBumper extends BodyComponent with InitialPosition {
/// Creates an [DashNestBumper] without any children. /// Creates an [DashNestBumper] without any children.
/// ///
/// This can be used for testing [DashNestBumper]'s behaviors in isolation. /// This can be used for testing [DashNestBumper]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
DashNestBumper.test({required this.bloc}) DashNestBumper.test({required this.bloc})
: _majorRadius = 3, : _majorRadius = 3,
@ -100,8 +98,6 @@ class DashNestBumper extends BodyComponent with InitialPosition {
final double _majorRadius; final double _majorRadius;
final double _minorRadius; final double _minorRadius;
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final DashNestBumperCubit bloc; final DashNestBumperCubit bloc;

@ -8,14 +8,6 @@ import 'package:flutter/material.dart';
const _particleRadius = 0.25; const _particleRadius = 0.25;
// TODO(erickzanardo): This component could just be a ParticleComponet,
/// unfortunately there is a Particle Component is not a PositionComponent,
/// which makes it hard to be used since we have camera transformations and on
// top of that, PositionComponent has a bug inside forge 2d games
///
/// https://github.com/flame-engine/flame/issues/1484
/// https://github.com/flame-engine/flame/issues/1484
/// {@template fire_effect} /// {@template fire_effect}
/// A [BodyComponent] which creates a fire trail effect using the given /// A [BodyComponent] which creates a fire trail effect using the given
/// parameters /// parameters

@ -100,8 +100,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
final trapezium = PolygonShape()..set(trapeziumVertices); final trapezium = PolygonShape()..set(trapeziumVertices);
final trapeziumFixtureDef = FixtureDef( final trapeziumFixtureDef = FixtureDef(
trapezium, trapezium,
density: 50, // TODO(alestiago): Use a proper density. density: 50,
friction: .1, // TODO(alestiago): Use a proper friction. friction: .1,
); );
return [ return [

@ -68,15 +68,11 @@ class GoogleLetter extends BodyComponent with InitialPosition {
/// Creates a [GoogleLetter] without any children. /// Creates a [GoogleLetter] without any children.
/// ///
/// This can be used for testing [GoogleLetter]'s behaviors in isolation. /// This can be used for testing [GoogleLetter]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
GoogleLetter.test({ GoogleLetter.test({
required this.bloc, required this.bloc,
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final GoogleLetterCubit bloc; final GoogleLetterCubit bloc;

@ -24,8 +24,6 @@ mixin InitialPosition on BodyComponent {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
// TODO(alestiago): Investiagate why body.position.setFrom(initialPosition)
// works for some components and not others.
assert( assert(
body.position == initialPosition, body.position == initialPosition,
'Body position does not match initialPosition.', 'Body position does not match initialPosition.',

@ -51,16 +51,12 @@ class Kicker extends BodyComponent with InitialPosition {
/// Creates a [Kicker] without any children. /// Creates a [Kicker] without any children.
/// ///
/// This can be used for testing [Kicker]'s behaviors in isolation. /// This can be used for testing [Kicker]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
Kicker.test({ Kicker.test({
required this.bloc, required this.bloc,
required BoardSide side, required BoardSide side,
}) : _side = side; }) : _side = side;
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final KickerCubit bloc; final KickerCubit bloc;
@ -129,7 +125,6 @@ class Kicker extends BodyComponent with InitialPosition {
FixtureDef(bouncyEdge, userData: 'bouncy_edge'), FixtureDef(bouncyEdge, userData: 'bouncy_edge'),
]; ];
// TODO(alestiago): Evaluate if there is value on centering the fixtures.
final centroid = geometry.centroid( final centroid = geometry.centroid(
[ [
upperCircle.position + Vector2(0, -upperCircle.radius), upperCircle.position + Vector2(0, -upperCircle.radius),
@ -177,8 +172,6 @@ class _KickerSpriteGroupComponent extends SpriteGroupComponent<KickerState>
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
parent.bloc.stream.listen((state) => current = state); parent.bloc.stream.listen((state) => current = state);
@ -203,8 +196,6 @@ class _KickerSpriteGroupComponent extends SpriteGroupComponent<KickerState>
} }
} }
// TODO(alestiago): Evaluate if there's value on generalising this to
// all shapes.
extension on Shape { extension on Shape {
void moveBy(Vector2 offset) { void moveBy(Vector2 offset) {
if (this is CircleShape) { if (this is CircleShape) {

@ -32,9 +32,6 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
layer = Layer.launcher; layer = Layer.launcher;
} }
// TODO(ruimiguel): final asset differs slightly from the current shape. We
// need to fix shape with correct vertices, but right now merge them to have
// final assets at game and not be blocked.
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];

@ -74,7 +74,6 @@ extension LayerMaskBits on Layer {
/// {@macro layer_mask_bits} /// {@macro layer_mask_bits}
@visibleForTesting @visibleForTesting
int get maskBits { int get maskBits {
// TODO(ruialonso): test bit groups once final design is implemented.
switch (this) { switch (this) {
case Layer.all: case Layer.all:
return 0xFFFF; return 0xFFFF;

@ -50,8 +50,6 @@ abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
Shape get shape; Shape get shape;
/// {@macro layer_entrance_orientation} /// {@macro layer_entrance_orientation}
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
// collision calculations.
final LayerEntranceOrientation orientation; final LayerEntranceOrientation orientation;
@override @override

@ -75,15 +75,11 @@ class Multiball extends Component {
/// Creates an [Multiball] without any children. /// Creates an [Multiball] without any children.
/// ///
/// This can be used for testing [Multiball]'s behaviors in isolation. /// This can be used for testing [Multiball]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
Multiball.test({ Multiball.test({
required this.bloc, required this.bloc,
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final MultiballCubit bloc; final MultiballCubit bloc;

@ -81,8 +81,6 @@ class Multiplier extends Component {
/// Creates a [Multiplier] without any children. /// Creates a [Multiplier] without any children.
/// ///
/// This can be used for testing [Multiplier]'s behaviors in isolation. /// This can be used for testing [Multiplier]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
Multiplier.test({ Multiplier.test({
required MultiplierValue value, required MultiplierValue value,
@ -91,8 +89,6 @@ class Multiplier extends Component {
_position = Vector2.zero(), _position = Vector2.zero(),
_angle = 0; _angle = 0;
// TODO(ruimiguel): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
final MultiplierCubit bloc; final MultiplierCubit bloc;
final MultiplierValue _value; final MultiplierValue _value;

@ -178,26 +178,16 @@ class _PlungerSpriteAnimationGroupComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
// TODO(alestiago): Used cached images.
final spriteSheet = await gameRef.images.load( final spriteSheet = await gameRef.images.load(
Assets.images.plunger.plunger.keyName, Assets.images.plunger.plunger.keyName,
); );
const amountPerRow = 20; const amountPerRow = 20;
const amountPerColumn = 1; const amountPerColumn = 1;
final textureSize = Vector2( final textureSize = Vector2(
spriteSheet.width / amountPerRow, spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn, spriteSheet.height / amountPerColumn,
); );
size = textureSize / 10; size = textureSize / 10;
// TODO(ruimiguel): we only need plunger pull animation, and release is just
// to reverse it, so we need to divide by 2 while we don't have only half of
// the animation (but amountPerRow and amountPerColumn needs to be correct
// in order of calculate textureSize correctly).
final pullAnimation = SpriteAnimation.fromFrameData( final pullAnimation = SpriteAnimation.fromFrameData(
spriteSheet, spriteSheet,
SpriteAnimationData.sequenced( SpriteAnimationData.sequenced(
@ -209,7 +199,6 @@ class _PlungerSpriteAnimationGroupComponent
loop: false, loop: false,
), ),
); );
animations = { animations = {
_PlungerAnimationState.release: pullAnimation.reversed(), _PlungerAnimationState.release: pullAnimation.reversed(),
_PlungerAnimationState.pull: pullAnimation, _PlungerAnimationState.pull: pullAnimation,

@ -28,7 +28,6 @@ class ArcShape extends ChainShape {
final Vector2 center; final Vector2 center;
/// The radius of the arc. /// The radius of the arc.
// TODO(alestiago): Check if modifying the parent radius makes sense.
final double arcRadius; final double arcRadius;
/// Specifies the size of the arc, in radians. /// Specifies the size of the arc, in radians.

@ -26,7 +26,6 @@ class EllipseShape extends ChainShape {
/// The top left corner of the ellipse. /// The top left corner of the ellipse.
/// ///
/// Where the initial painting begins. /// Where the initial painting begins.
// TODO(ruialonso): Change to use appropiate center.
final Vector2 center; final Vector2 center;
/// Major radius is specified by [majorRadius]. /// Major radius is specified by [majorRadius].

@ -36,15 +36,11 @@ class Signpost extends BodyComponent with InitialPosition {
/// Creates a [Signpost] without any children. /// Creates a [Signpost] without any children.
/// ///
/// This can be used for testing [Signpost]'s behaviors in isolation. /// This can be used for testing [Signpost]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
Signpost.test({ Signpost.test({
required this.bloc, required this.bloc,
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final SignpostCubit bloc; final SignpostCubit bloc;

@ -38,15 +38,11 @@ class SkillShot extends BodyComponent with ZIndex {
/// Creates a [SkillShot] without any children. /// Creates a [SkillShot] without any children.
/// ///
/// This can be used for testing [SkillShot]'s behaviors in isolation. /// This can be used for testing [SkillShot]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
SkillShot.test({ SkillShot.test({
required this.bloc, required this.bloc,
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final SkillShotCubit bloc; final SkillShotCubit bloc;

@ -27,8 +27,6 @@ class SpaceshipRamp extends Component {
required this.bloc, required this.bloc,
}) : super( }) : super(
children: [ children: [
// TODO(ruimiguel): refactor RampScoringSensor and
// _SpaceshipRampOpening to be in only one sensor if possible.
RampScoringSensor( RampScoringSensor(
children: [ children: [
RampBallAscendingContactBehavior(), RampBallAscendingContactBehavior(),
@ -68,8 +66,6 @@ class SpaceshipRamp extends Component {
required this.bloc, required this.bloc,
}) : super(); }) : super();
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final SpaceshipRampCubit bloc; final SpaceshipRampCubit bloc;

@ -93,8 +93,6 @@ class SparkyBumper extends BodyComponent with InitialPosition, ZIndex {
/// Creates an [SparkyBumper] without any children. /// Creates an [SparkyBumper] without any children.
/// ///
/// This can be used for testing [SparkyBumper]'s behaviors in isolation. /// This can be used for testing [SparkyBumper]'s behaviors in isolation.
// TODO(alestiago): Refactor injecting bloc once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
@visibleForTesting @visibleForTesting
SparkyBumper.test({ SparkyBumper.test({
required this.bloc, required this.bloc,
@ -104,8 +102,6 @@ class SparkyBumper extends BodyComponent with InitialPosition, ZIndex {
final double _majorRadius; final double _majorRadius;
final double _minorRadius; final double _minorRadius;
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
final SparkyBumperCubit bloc; final SparkyBumperCubit bloc;
@ -152,8 +148,6 @@ class _SparkyBumperSpriteGroupComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
parent.bloc.stream.listen((state) => current = state); parent.bloc.stream.listen((state) => current = state);

@ -0,0 +1 @@
export 'sparky_computer_sensor_ball_contact_behavior.dart';

@ -0,0 +1,35 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template sparky_computer_sensor_ball_contact_behavior}
/// When a [Ball] enters the [SparkyComputer] it is stopped for a period of time
/// before a [BallTurboChargingBehavior] is applied to it.
/// {@endtemplate}
class SparkyComputerSensorBallContactBehavior
extends ContactBehavior<SparkyComputer> {
@override
Future<void> beginContact(Object other, Contact contact) async {
super.beginContact(other, contact);
if (other is! Ball) return;
other.stop();
parent.bloc.onBallEntered();
await parent.add(
TimerComponent(
period: 1.5,
removeOnFinish: true,
onTick: () async {
other.resume();
await other.add(
BallTurboChargingBehavior(
impulse: Vector2(40, 110),
),
);
parent.bloc.onBallTurboCharged();
},
),
);
}
}

@ -0,0 +1,17 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
part 'sparky_computer_state.dart';
class SparkyComputerCubit extends Cubit<SparkyComputerState> {
SparkyComputerCubit() : super(SparkyComputerState.withoutBall);
void onBallEntered() {
emit(SparkyComputerState.withBall);
}
void onBallTurboCharged() {
emit(SparkyComputerState.withoutBall);
}
}

@ -0,0 +1,8 @@
// ignore_for_file: public_member_api_docs
part of 'sparky_computer_cubit.dart';
enum SparkyComputerState {
withoutBall,
withBall,
}

@ -2,31 +2,48 @@
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_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/sparky_computer/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/sparky_computer_cubit.dart';
/// {@template sparky_computer} /// {@template sparky_computer}
/// A computer owned by Sparky. /// A computer owned by Sparky.
/// {@endtemplate} /// {@endtemplate}
class SparkyComputer extends Component { class SparkyComputer extends BodyComponent {
/// {@macro sparky_computer} /// {@macro sparky_computer}
SparkyComputer() SparkyComputer({Iterable<Component>? children})
: super( : bloc = SparkyComputerCubit(),
super(
renderBody: false,
children: [ children: [
_ComputerBase(), SparkyComputerSensorBallContactBehavior()
..applyTo(['turbo_charge_sensor']),
_ComputerBaseSpriteComponent(),
_ComputerTopSpriteComponent(), _ComputerTopSpriteComponent(),
_ComputerGlowSpriteComponent(), _ComputerGlowSpriteComponent(),
...?children,
], ],
); );
}
class _ComputerBase extends BodyComponent with InitialPosition, ZIndex { /// Creates a [SparkyComputer] without any children.
_ComputerBase() ///
: super( /// This can be used for testing [SparkyComputer]'s behaviors in isolation.
renderBody: false, @visibleForTesting
children: [_ComputerBaseSpriteComponent()], SparkyComputer.test({
) { required this.bloc,
zIndex = ZIndexes.computerBase; Iterable<Component>? children,
}) : super(children: children);
// ignore: public_member_api_docs
final SparkyComputerCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
@ -45,30 +62,44 @@ class _ComputerBase extends BodyComponent with InitialPosition, ZIndex {
topEdge.vertex2, topEdge.vertex2,
Vector2(-9.4, -47.1), Vector2(-9.4, -47.1),
); );
final turboChargeSensor = PolygonShape()
..setAsBox(
1,
0.1,
Vector2(-13.2, -49.9),
-0.18,
);
return [ return [
FixtureDef(leftEdge), FixtureDef(leftEdge),
FixtureDef(topEdge), FixtureDef(topEdge),
FixtureDef(rightEdge), FixtureDef(rightEdge),
FixtureDef(
turboChargeSensor,
isSensor: true,
userData: 'turbo_charge_sensor',
),
]; ];
} }
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef(position: initialPosition); final body = world.createBody(BodyDef());
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); _createFixtureDefs().forEach(body.createFixture);
return body; return body;
} }
} }
class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef { class _ComputerBaseSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_ComputerBaseSpriteComponent() _ComputerBaseSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-12.44, -48.15), position: Vector2(-12.44, -48.15),
); ) {
zIndex = ZIndexes.computerBase;
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {

@ -1,7 +1,6 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
/// Z-Indexes for the component rendering order in the pinball game. /// Z-Indexes for the component rendering order in the pinball game.
// TODO(allisonryan0002): find alternative to section comments.
abstract class ZIndexes { abstract class ZIndexes {
static const _base = 0; static const _base = 0;
static const _above = 1; static const _above = 1;
@ -21,8 +20,6 @@ abstract class ZIndexes {
// Background // Background
// TODO(allisonryan0002): fix this magic zindex. Could bump all priorities so
// there are no negatives.
static const boardBackground = 5 * _below + _base; static const boardBackground = 5 * _below + _base;
static const decal = _above + boardBackground; static const decal = _above + boardBackground;

@ -9,9 +9,10 @@ environment:
dependencies: dependencies:
bloc: ^8.0.3 bloc: ^8.0.3
flame: ^1.1.1 flame: ^1.1.1
flame_bloc: ^1.4.0
flame_forge2d: flame_forge2d:
git: git:
url: https://github.com/flame-engine/flame/ url: https://github.com/flame-engine/flame
path: packages/flame_forge2d/ path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:

@ -106,13 +106,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
flame_bloc:
dependency: transitive
description:
name: flame_bloc
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
flame_forge2d: flame_forge2d:
dependency: "direct main" dependency: "direct main"
description: description:
path: "packages/flame_forge2d" path: "packages/flame_forge2d"
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f resolved-ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
url: "https://github.com/flame-engine/flame/" url: "https://github.com/flame-engine/flame"
source: git source: git
version: "0.11.0" version: "0.11.0"
flutter: flutter:
@ -120,6 +127,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_bloc:
dependency: transitive
description:
name: flutter_bloc
url: "https://pub.dartlang.org"
source: hosted
version: "8.0.1"
flutter_colorpicker: flutter_colorpicker:
dependency: transitive dependency: transitive
description: description:
@ -214,6 +228,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.7.0"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
ordered_set: ordered_set:
dependency: transitive dependency: transitive
description: description:
@ -298,13 +319,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.4" version: "4.2.4"
share_repository: provider:
dependency: transitive dependency: transitive
description: description:
path: "../../share_repository" name: provider
relative: true url: "https://pub.dartlang.org"
source: path source: hosted
version: "1.0.0+1" version: "6.0.2"
shared_preferences: shared_preferences:
dependency: transitive dependency: transitive
description: description:

@ -11,7 +11,7 @@ dependencies:
flame: ^1.1.1 flame: ^1.1.1
flame_forge2d: flame_forge2d:
git: git:
url: https://github.com/flame-engine/flame/ url: https://github.com/flame-engine/flame
path: packages/flame_forge2d/ path: packages/flame_forge2d/
ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f ref: a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f
flutter: flutter:

@ -44,8 +44,6 @@ void main() {
expect(game.contains(androidBumper), isTrue); expect(game.contains(androidBumper), isTrue);
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockAndroidBumperCubit(); final bloc = _MockAndroidBumperCubit();

@ -70,8 +70,6 @@ void main() {
}, },
); );
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockAndroidSpaceshipCubit(); final bloc = _MockAndroidSpaceshipCubit();

@ -1,5 +1,6 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -39,6 +40,41 @@ void main() {
}, },
); );
flameTester.test(
'has only one SpriteComponent',
(game) async {
final ball = Ball();
await game.ready();
await game.ensureAdd(ball);
expect(
ball.descendants().whereType<SpriteComponent>().length,
equals(1),
);
},
);
flameTester.test(
'BallSpriteComponent changes sprite onNewState',
(game) async {
final ball = Ball();
await game.ready();
await game.ensureAdd(ball);
final ballSprite =
ball.descendants().whereType<BallSpriteComponent>().single;
final originalSprite = ballSprite.sprite;
ballSprite.onNewState(
const BallState(characterTheme: theme.DinoTheme()),
);
await game.ready();
final newSprite = ballSprite.sprite;
expect(newSprite != originalSprite, isTrue);
},
);
group('adds', () { group('adds', () {
flameTester.test('a BallScalingBehavior', (game) async { flameTester.test('a BallScalingBehavior', (game) async {
final ball = Ball(); final ball = Ball();

@ -62,8 +62,8 @@ void main() {
await game.ensureAddAll([ball1, ball2]); await game.ensureAddAll([ball1, ball2]);
game.update(1); game.update(1);
final sprite1 = ball1.firstChild<SpriteComponent>()!; final sprite1 = ball1.descendants().whereType<SpriteComponent>().single;
final sprite2 = ball2.firstChild<SpriteComponent>()!; final sprite2 = ball2.descendants().whereType<SpriteComponent>().single;
expect( expect(
sprite1.scale.x, sprite1.scale.x,
greaterThan(sprite2.scale.x), greaterThan(sprite2.scale.x),

@ -0,0 +1,18 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group(
'BallCubit',
() {
blocTest<BallCubit, BallState>(
'onThemeChanged emits new theme',
build: BallCubit.new,
act: (bloc) => bloc.onThemeChanged(const DinoTheme()),
expect: () => [const BallState(characterTheme: DinoTheme())],
);
},
);
}

@ -0,0 +1,22 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() {
group('BallState', () {
test('supports value equality', () {
expect(
BallState(characterTheme: DashTheme()),
equals(const BallState(characterTheme: DashTheme())),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(const BallState(characterTheme: DashTheme()), isNotNull);
});
});
});
}

@ -61,7 +61,10 @@ void main() {
behavior.beginContact(ball, contact); behavior.beginContact(ball, contact);
expect(ball.firstChild<SpriteComponent>()!.getOpacity(), isZero); expect(
ball.descendants().whereType<SpriteComponent>().single.getOpacity(),
isZero,
);
verify(() => bloc.onChomp(ball)).called(1); verify(() => bloc.onChomp(ball)).called(1);
}, },

@ -66,7 +66,14 @@ void main() {
.timer .timer
.onTick!(); .onTick!();
expect(ball.firstChild<SpriteComponent>()!.getOpacity(), equals(1)); expect(
ball
.descendants()
.whereType<SpriteComponent>()
.single
.getOpacity(),
equals(1),
);
expect(ball.body.linearVelocity, equals(Vector2(-50, 0))); expect(ball.body.linearVelocity, equals(Vector2(-50, 0)));
}, },
); );

@ -71,8 +71,6 @@ void main() {
}, },
); );
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 95 KiB

@ -46,8 +46,6 @@ void main() {
expect(game.contains(bumper), isTrue); expect(game.contains(bumper), isTrue);
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockDashNestBumperCubit(); final bloc = _MockDashNestBumperCubit();

@ -18,8 +18,6 @@ void main() {
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));
group('Flipper', () { group('Flipper', () {
// TODO(alestiago): Consider testing always both left and right Flipper.
flameTester.testGameWidget( flameTester.testGameWidget(
'renders correctly', 'renders correctly',
setUp: (game, tester) async { setUp: (game, tester) async {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

@ -102,8 +102,6 @@ void main() {
expect(() => GoogleLetter(6), throwsA(isA<RangeError>())); expect(() => GoogleLetter(6), throwsA(isA<RangeError>()));
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockGoogleLetterCubit(); final bloc = _MockGoogleLetterCubit();

@ -61,8 +61,6 @@ void main() {
}, },
); );
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockKickerCubit(); final bloc = _MockKickerCubit();

@ -29,8 +29,6 @@ void main() {
expect(game.contains(skillShot), isTrue); expect(game.contains(skillShot), isTrue);
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSkillShotCubit(); final bloc = _MockSkillShotCubit();

@ -44,8 +44,6 @@ void main() {
expect(game.contains(sparkyBumper), isTrue); expect(game.contains(sparkyBumper), isTrue);
}); });
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs // ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async { flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSparkyBumperCubit(); final bloc = _MockSparkyBumperCubit();

@ -0,0 +1,141 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
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_components/src/components/sparky_computer/behaviors/behaviors.dart';
import '../../../../helpers/helpers.dart';
class _MockSparkyComputerCubit extends Mock implements SparkyComputerCubit {}
class _MockBall extends Mock implements Ball {}
class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group(
'SparkyComputerSensorBallContactBehavior',
() {
test('can be instantiated', () {
expect(
SparkyComputerSensorBallContactBehavior(),
isA<SparkyComputerSensorBallContactBehavior>(),
);
});
group('beginContact', () {
flameTester.test(
'stops a ball',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
final ball = _MockBall();
await behavior.beginContact(ball, _MockContact());
verify(ball.stop).called(1);
},
);
flameTester.test(
'emits onBallEntered when contacts with a ball',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
await behavior.beginContact(_MockBall(), _MockContact());
verify(sparkyComputer.bloc.onBallEntered).called(1);
},
);
flameTester.test(
'adds TimerComponent when contacts with a ball',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
await behavior.beginContact(_MockBall(), _MockContact());
await game.ready();
expect(
sparkyComputer.firstChild<TimerComponent>(),
isA<TimerComponent>(),
);
},
);
flameTester.test(
'TimerComponent resumes ball and calls onBallTurboCharged onTick',
(game) async {
final behavior = SparkyComputerSensorBallContactBehavior();
final bloc = _MockSparkyComputerCubit();
whenListen(
bloc,
const Stream<SparkyComputerState>.empty(),
initialState: SparkyComputerState.withoutBall,
);
final sparkyComputer = SparkyComputer.test(
bloc: bloc,
);
await sparkyComputer.add(behavior);
await game.ensureAdd(sparkyComputer);
final ball = _MockBall();
await behavior.beginContact(ball, _MockContact());
await game.ready();
game.update(
sparkyComputer.firstChild<TimerComponent>()!.timer.limit,
);
await game.ready();
verify(ball.resume).called(1);
verify(sparkyComputer.bloc.onBallTurboCharged).called(1);
},
);
});
},
);
}

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

Loading…
Cancel
Save