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
with:
flutter_channel: stable
flutter_version: 2.10.0
flutter_version: 2.10.5
coverage_excludes: "lib/gen/*.dart"
test_optimization: false

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

@ -17,7 +17,7 @@ service cloud.firestore {
}
// 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,
// 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: [
BlocProvider(create: (_) => CharacterThemeCubit()),
BlocProvider(create: (_) => StartGameBloc()),
BlocProvider(create: (_) => GameBloc()),
],
child: MaterialApp(
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_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// Spawns a new [Ball] into the game when all balls are lost and still
/// [GameStatus.playing].
@ -23,12 +23,16 @@ class BallSpawningBehavior extends Component
void onNewState(GameState state) {
final plunger = gameRef.descendants().whereType<Plunger>().single;
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
final characterTheme = readProvider<CharacterTheme>();
final ball = ControlledBall.launch(characterTheme: characterTheme)
final characterTheme = readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme;
final ball = Ball(assetPath: characterTheme.ball.keyName)
..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
)
..layer = Layer.launcher
..zIndex = ZIndexes.ballOnLaunchRamp;
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_theming_behavior.dart';
export 'bonus_noise_behavior.dart';
export 'bumper_noise_behavior.dart';
export 'camera_focusing_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() {
super.onMount();
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) {
final listenWhen = state == AndroidSpaceshipState.withBonus;
if (!listenWhen) return;

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

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

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart';
export 'bottom_group.dart';
export 'controlled_ball.dart';
export 'controlled_flipper.dart';
export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart';
@ -12,4 +11,4 @@ export 'google_word/google_word.dart';
export 'launcher.dart';
export 'multiballs/multiballs.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() {
super.onMount();
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) {
final listenWhen = state.status == ChromeDinoStatus.chomping;
if (!listenWhen) return;

@ -1,9 +1,9 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// Bonus obtained at the [FlutterForest].
///
@ -25,9 +25,6 @@ class FlutterForestBonusBehavior extends Component
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
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) {
final activatedAllBumpers = bumpers.every(
(bumper) => bumper.bloc.state == DashNestBumperState.active,
@ -41,10 +38,14 @@ class FlutterForestBonusBehavior extends Component
if (signpost.bloc.isFullyProgressed()) {
bloc.add(const BonusActivated(GameBonus.dashNest));
final characterTheme =
readBloc<CharacterThemeCubit, CharacterThemeState>()
.state
.characterTheme;
canvas.add(
ControlledBall.bonus(
characterTheme: readProvider<CharacterTheme>(),
)..initialPosition = Vector2(29.2, -24.5),
Ball(assetPath: characterTheme.ball.keyName)
..initialPosition = Vector2(29.2, -24.5)
..zIndex = ZIndexes.ballOnBoard,
);
animatronic.playing = true;
signpost.bloc.onProgressed();

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

@ -1,7 +1,6 @@
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_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
@ -14,15 +13,11 @@ class GoogleWordBonusBehavior extends Component
final googleLetters = parent.children.whereType<GoogleLetter>();
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((_) {
final achievedBonus = googleLetters
.every((letter) => letter.bloc.state == GoogleLetterState.lit);
if (achievedBonus) {
readProvider<PinballPlayer>().play(PinballAudio.google);
bloc.add(const BonusActivated(GameBonus.googleWord));
for (final letter in googleLetters) {
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
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/components/components.dart';
import 'package:pinball/game/components/sparky_scorch/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template sparky_scorch}
@ -33,51 +33,20 @@ class SparkyScorch extends Component {
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13.2, -49.9),
SparkyAnimatronic()..position = Vector2(-14, -58.2),
SparkyComputer(),
],
);
}
/// {@template sparky_computer_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SparkyComputer].
/// {@endtemplate}
class SparkyComputerSensor extends BodyComponent
with InitialPosition, ContactCallbacks {
/// {@macro sparky_computer_sensor}
SparkyComputerSensor()
: super(
renderBody: false,
SparkyComputer(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
ScoringContactBehavior(points: Points.twoHundredThousand)
..applyTo(['turbo_charge_sensor']),
],
),
SparkyComputerBonusBehavior(),
],
);
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
1,
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;
}
/// Creates [SparkyScorch] without any children.
///
/// This can be used for testing [SparkyScorch]'s behaviors in isolation.
@visibleForTesting
SparkyScorch.test();
}

@ -11,15 +11,15 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.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_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends PinballForge2DGame
with HasKeyboardHandlerComponents, MultiTouchTapDetector, HasTappables {
PinballGame({
required CharacterTheme characterTheme,
required CharacterThemeCubit characterThemeBloc,
required this.leaderboardRepository,
required GameBloc gameBloc,
required AppLocalizations l10n,
@ -27,7 +27,7 @@ class PinballGame extends PinballForge2DGame
}) : focusNode = FocusNode(),
_gameBloc = gameBloc,
_player = player,
_characterTheme = characterTheme,
_characterThemeBloc = characterThemeBloc,
_l10n = l10n,
super(
gravity: Vector2(0, 30),
@ -43,7 +43,7 @@ class PinballGame extends PinballForge2DGame
final FocusNode focusNode;
final CharacterTheme _characterTheme;
final CharacterThemeCubit _characterThemeBloc;
final PinballPlayer _player;
@ -56,19 +56,27 @@ class PinballGame extends PinballForge2DGame
@override
Future<void> onLoad() async {
await add(
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: _gameBloc,
),
FlameBlocProvider<CharacterThemeCubit, CharacterThemeState>.value(
value: _characterThemeBloc,
),
],
children: [
MultiFlameProvider(
providers: [
FlameProvider<PinballPlayer>.value(_player),
FlameProvider<CharacterTheme>.value(_characterTheme),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<AppLocalizations>.value(_l10n),
],
children: [
BonusNoiseBehavior(),
GameBlocStatusListener(),
BallSpawningBehavior(),
BallThemingBehavior(),
CameraFocusingBehavior(),
CanvasComponent(
onSpritePainted: (paint) {
@ -160,13 +168,13 @@ class PinballGame extends PinballForge2DGame
class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({
required CharacterTheme characterTheme,
required CharacterThemeCubit characterThemeBloc,
required LeaderboardRepository leaderboardRepository,
required AppLocalizations l10n,
required PinballPlayer player,
required GameBloc gameBloc,
}) : super(
characterTheme: characterTheme,
characterThemeBloc: characterThemeBloc,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: l10n,
@ -190,8 +198,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
if (info.raw.kind == PointerDeviceKind.mouse) {
final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()
..initialPosition = info.eventPosition.game;
final ball = Ball()..initialPosition = info.eventPosition.game;
canvas.add(ball);
}
}
@ -218,7 +225,7 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
void _turboChargeBall(Vector2 line) {
final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()..initialPosition = lineStart!;
final ball = Ball()..initialPosition = lineStart!;
final impulse = line * -1 * 10;
ball.add(BallTurboChargingBehavior(impulse: impulse));
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> {
@override
PositionType get positionType => PositionType.widget;
@ -264,7 +270,7 @@ class _DebugInformation extends Component with HasGameRef<DebugPinballGame> {
void render(Canvas canvas) {
final debugText = [
'FPS: ${gameRef.fps().toStringAsFixed(1)}',
'BALLS: ${gameRef.descendants().whereType<ControlledBall>().length}',
'BALLS: ${gameRef.descendants().whereType<Ball>().length}',
].join(' | ');
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:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.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/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
@ -21,40 +23,28 @@ class PinballGamePage extends StatelessWidget {
final bool isDebugMode;
static Route route({
bool isDebugMode = kDebugMode,
}) {
static Route route({bool isDebugMode = kDebugMode}) {
return MaterialPageRoute<void>(
builder: (context) {
return PinballGamePage(
isDebugMode: isDebugMode,
);
},
builder: (_) => PinballGamePage(isDebugMode: isDebugMode),
);
}
@override
Widget build(BuildContext context) {
final characterTheme =
context.read<CharacterThemeCubit>().state.characterTheme;
final characterThemeBloc = context.read<CharacterThemeCubit>();
final player = context.read<PinballPlayer>();
final leaderboardRepository = context.read<LeaderboardRepository>();
return BlocProvider(
create: (_) => GameBloc(),
child: Builder(
builder: (context) {
final gameBloc = context.read<GameBloc>();
final game = isDebugMode
? DebugPinballGame(
characterTheme: characterTheme,
characterThemeBloc: characterThemeBloc,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
gameBloc: gameBloc,
)
: PinballGame(
characterTheme: characterTheme,
characterThemeBloc: characterThemeBloc,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
@ -72,9 +62,6 @@ class PinballGamePage extends StatelessWidget {
create: (_) => AssetsManagerCubit(loadables)..load(),
child: PinballGameView(game: game),
);
},
),
);
}
}
@ -142,6 +129,7 @@ class PinballGameLoadedView extends StatelessWidget {
),
),
const _PositionedGameHud(),
const _PositionedInfoIcon(),
],
),
);
@ -159,6 +147,7 @@ class _PositionedGameHud extends StatelessWidget {
final isGameOver = context.select(
(GameBloc bloc) => bloc.state.status.isGameOver,
);
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width;
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": {
"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": {
"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
// TODO(allisonryan0002): Document this section when the API is stable.
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';

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

@ -44,31 +44,10 @@ class LeaderboardRepository {
final tenthPositionScore = leaderboard[9].score;
if (entry.score > tenthPositionScore) {
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 {
try {
final querySnapshot = await _firebaseFirestore
@ -91,23 +70,6 @@ class LeaderboardRepository {
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> {

@ -40,16 +40,6 @@ class FetchLeaderboardException extends LeaderboardException {
: 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}
/// Exception thrown when failure occurs while adding entry to leaderboard.
/// {@endtemplate}
@ -58,12 +48,3 @@ class AddLeaderboardEntryException extends LeaderboardException {
const AddLeaderboardEntryException(Object error, StackTrace 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
implements DocumentReference<Map<String, dynamic>> {}
class _MockDocumentSnapshot extends Mock
implements DocumentSnapshot<Map<String, dynamic>> {}
void main() {
group('LeaderboardRepository', () {
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(
'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 '
'deletes the scores that are not in the top 10 anymore', () async {
final deleteQuery = _MockQuery();
final deleteQuerySnapshot = _MockQuerySnapshot();
'leaderboard and the new score is higher than the lowest top 10',
() async {
final newScore = LeaderboardEntryData(
playerInitials: 'ABC',
score: 15000,
@ -330,21 +264,6 @@ void main() {
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>{
@ -357,105 +276,11 @@ void main() {
.thenReturn(documentReference);
return queryDocumentSnapshot;
}).toList();
when(
() => collectionReference.where('score', isLessThanOrEqualTo: 5500),
).thenAnswer((_) => deleteQuery);
when(() => documentReference.delete())
.thenAnswer((_) async => Future.value());
when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots);
await leaderboardRepository.addLeaderboardEntry(newScore);
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 {
const $AssetsSfxGen();
String get afterLaunch => 'assets/sfx/after_launch.mp3';
String get bumperA => 'assets/sfx/bumper_a.mp3';
String get bumperB => 'assets/sfx/bumper_b.mp3';
String get gameOverVoiceOver => 'assets/sfx/game_over_voice_over.mp3';
String get google => 'assets/sfx/google.mp3';
String get ioPinballVoiceOver => 'assets/sfx/io_pinball_voice_over.mp3';
String get launcher => 'assets/sfx/launcher.mp3';
String get sparky => 'assets/sfx/sparky.mp3';
}
class Assets {

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

@ -141,6 +141,10 @@ void main() {
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/google.mp3'),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/sparky.mp3'),
).called(1);
verify(
() => preCacheSingleAudio.onCall(
'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 {
await Future.wait(player.load());
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', () {
test('plays the correct file', () async {
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.
///
/// 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
AndroidBumper.test({
required this.bloc,
@ -105,8 +103,6 @@ class AndroidBumper extends BodyComponent with InitialPosition, ZIndex {
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
final AndroidBumperCubit bloc;

@ -38,16 +38,12 @@ class AndroidSpaceship extends Component {
/// Creates an [AndroidSpaceship] without any children.
///
/// 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
AndroidSpaceship.test({
required this.bloc,
Iterable<Component>? children,
}) : super(children: children);
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
final AndroidSpaceshipCubit bloc;
@override
@ -129,7 +125,6 @@ class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent
}
}
// TODO(allisonryan0002): add pulsing behavior.
class _LightBeamSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex {
_LightBeamSpriteComponent()

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.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;
export 'behaviors/behaviors.dart';
export 'cubit/ball_cubit.dart';
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces around.
/// {@endtemplate}
class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// {@macro ball}
Ball({
String? assetPath,
}) : super(
Ball({String? assetPath}) : this._(bloc: BallCubit(), assetPath: assetPath);
Ball._({required this.bloc, String? assetPath})
: super(
renderBody: false,
children: [
_BallSpriteComponent(assetPath: assetPath),
FlameBlocProvider<BallCubit, BallState>.value(
value: bloc,
children: [BallSpriteComponent(assetPath: assetPath)],
),
BallScalingBehavior(),
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;
}
@ -36,11 +37,22 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
///
/// This can be used for testing [Ball]'s behaviors in isolation.
@visibleForTesting
Ball.test()
: super(
children: [_BallSpriteComponent()],
Ball.test({
BallCubit? bloc,
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].
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
/// weight and those emitted from collisions.
// TODO(allisonryan0002): prevent motion from contact with other balls.
void stop() {
body
..gravityScale = Vector2.zero()
@ -76,21 +87,32 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
}
}
class _BallSpriteComponent extends SpriteComponent with HasGameRef {
_BallSpriteComponent({
this.assetPath,
}) : super(
anchor: Anchor.center,
);
/// {@template ball_sprite_component}
/// Visual representation of the [Ball].
/// {@endtemplate}
@visibleForTesting
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
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images
.fromCache(assetPath ?? theme.Assets.images.dash.ball.keyName),
.fromCache(_assetPath ?? theme.Assets.images.dash.ball.keyName),
);
this.sprite = sprite;
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.firstChild<SpriteComponent>()!.scale.setValues(
final ballSprite = parent.descendants().whereType<SpriteComponent>();
if (ballSprite.isNotEmpty) {
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}
/// Contains various board properties and dimensions for global use.
/// {@endtemplate}
// TODO(allisonryan0002): consider alternatives for global dimensions.
class BoardDimensions {
/// Width and height of the board.
static final size = Vector2(101.6, 143.8);

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

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

@ -38,15 +38,11 @@ class ChromeDino extends BodyComponent
/// Creates a [ChromeDino] without any children.
///
/// 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
ChromeDino.test({
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
final ChromeDinoCubit bloc;
@ -61,13 +57,13 @@ class ChromeDino extends BodyComponent
List<FixtureDef> _createFixtureDefs() {
const mouthAngle = -(halfSweepingAngle + 0.28);
final size = Vector2(5.5, 6);
final size = Vector2(6, 6);
final topEdge = PolygonShape()
..setAsBox(
size.x / 2,
0.1,
initialPosition + Vector2(-4.2, -1.4),
initialPosition + Vector2(-4, -1.4),
mouthAngle,
);
final topEdgeFixtureDef = FixtureDef(topEdge, density: 100);
@ -76,7 +72,7 @@ class ChromeDino extends BodyComponent
..setAsBox(
0.1,
size.y / 2,
initialPosition + Vector2(-1.3, 0.5),
initialPosition + Vector2(-1, 0.5),
-halfSweepingAngle,
);
final backEdgeFixtureDef = FixtureDef(backEdge, density: 100);
@ -85,7 +81,7 @@ class ChromeDino extends BodyComponent
..setAsBox(
size.x / 2,
0.1,
initialPosition + Vector2(-3.5, 4.7),
initialPosition + Vector2(-3.3, 4.7),
mouthAngle,
);
final bottomEdgeFixtureDef = FixtureDef(
@ -110,7 +106,7 @@ class ChromeDino extends BodyComponent
..setAsBox(
0.2,
0.2,
initialPosition + Vector2(-3.5, 1.5),
initialPosition + Vector2(-3, 1.5),
0,
);
final insideSensorFixtureDef = FixtureDef(

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

@ -90,8 +90,6 @@ class DashNestBumper extends BodyComponent with InitialPosition {
/// Creates an [DashNestBumper] without any children.
///
/// 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
DashNestBumper.test({required this.bloc})
: _majorRadius = 3,
@ -100,8 +98,6 @@ class DashNestBumper extends BodyComponent with InitialPosition {
final double _majorRadius;
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
final DashNestBumperCubit bloc;

@ -8,14 +8,6 @@ import 'package:flutter/material.dart';
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}
/// A [BodyComponent] which creates a fire trail effect using the given
/// parameters

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

@ -68,15 +68,11 @@ class GoogleLetter extends BodyComponent with InitialPosition {
/// Creates a [GoogleLetter] without any children.
///
/// 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
GoogleLetter.test({
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
final GoogleLetterCubit bloc;

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

@ -51,16 +51,12 @@ class Kicker extends BodyComponent with InitialPosition {
/// Creates a [Kicker] without any children.
///
/// 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
Kicker.test({
required this.bloc,
required BoardSide 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
final KickerCubit bloc;
@ -129,7 +125,6 @@ class Kicker extends BodyComponent with InitialPosition {
FixtureDef(bouncyEdge, userData: 'bouncy_edge'),
];
// TODO(alestiago): Evaluate if there is value on centering the fixtures.
final centroid = geometry.centroid(
[
upperCircle.position + Vector2(0, -upperCircle.radius),
@ -177,8 +172,6 @@ class _KickerSpriteGroupComponent extends SpriteGroupComponent<KickerState>
@override
Future<void> onLoad() async {
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
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 {
void moveBy(Vector2 offset) {
if (this is CircleShape) {

@ -32,9 +32,6 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
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() {
final fixturesDef = <FixtureDef>[];

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

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

@ -75,15 +75,11 @@ class Multiball extends Component {
/// Creates an [Multiball] without any children.
///
/// 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
Multiball.test({
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
final MultiballCubit bloc;

@ -81,8 +81,6 @@ class Multiplier extends Component {
/// Creates a [Multiplier] without any children.
///
/// 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
Multiplier.test({
required MultiplierValue value,
@ -91,8 +89,6 @@ class Multiplier extends Component {
_position = Vector2.zero(),
_angle = 0;
// TODO(ruimiguel): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
final MultiplierCubit bloc;
final MultiplierValue _value;

@ -178,26 +178,16 @@ class _PlungerSpriteAnimationGroupComponent
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Used cached images.
final spriteSheet = await gameRef.images.load(
Assets.images.plunger.plunger.keyName,
);
const amountPerRow = 20;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
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(
spriteSheet,
SpriteAnimationData.sequenced(
@ -209,7 +199,6 @@ class _PlungerSpriteAnimationGroupComponent
loop: false,
),
);
animations = {
_PlungerAnimationState.release: pullAnimation.reversed(),
_PlungerAnimationState.pull: pullAnimation,

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

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

@ -36,15 +36,11 @@ class Signpost extends BodyComponent with InitialPosition {
/// Creates a [Signpost] without any children.
///
/// 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
Signpost.test({
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
final SignpostCubit bloc;

@ -38,15 +38,11 @@ class SkillShot extends BodyComponent with ZIndex {
/// Creates a [SkillShot] without any children.
///
/// 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
SkillShot.test({
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
final SkillShotCubit bloc;

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

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

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

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

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

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

@ -44,8 +44,6 @@ void main() {
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
flameTester.test('closes bloc when removed', (game) async {
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
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockAndroidSpaceshipCubit();

@ -1,5 +1,6 @@
// ignore_for_file: cascade_invocations
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';
@ -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', () {
flameTester.test('a BallScalingBehavior', (game) async {
final ball = Ball();

@ -62,8 +62,8 @@ void main() {
await game.ensureAddAll([ball1, ball2]);
game.update(1);
final sprite1 = ball1.firstChild<SpriteComponent>()!;
final sprite2 = ball2.firstChild<SpriteComponent>()!;
final sprite1 = ball1.descendants().whereType<SpriteComponent>().single;
final sprite2 = ball2.descendants().whereType<SpriteComponent>().single;
expect(
sprite1.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);
expect(ball.firstChild<SpriteComponent>()!.getOpacity(), isZero);
expect(
ball.descendants().whereType<SpriteComponent>().single.getOpacity(),
isZero,
);
verify(() => bloc.onChomp(ball)).called(1);
},

@ -66,7 +66,14 @@ void main() {
.timer
.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)));
},
);

@ -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
flameTester.test('closes bloc when removed', (game) async {
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);
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockDashNestBumperCubit();

@ -18,8 +18,6 @@ void main() {
final flameTester = FlameTester(() => TestGame(assets));
group('Flipper', () {
// TODO(alestiago): Consider testing always both left and right Flipper.
flameTester.testGameWidget(
'renders correctly',
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>()));
});
// TODO(alestiago): Consider refactoring once the following is merged:
// https://github.com/flame-engine/flame/pull/1538
// ignore: public_member_api_docs
flameTester.test('closes bloc when removed', (game) async {
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
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockKickerCubit();

@ -29,8 +29,6 @@ void main() {
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
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSkillShotCubit();

@ -44,8 +44,6 @@ void main() {
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
flameTester.test('closes bloc when removed', (game) async {
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