Merge branch 'main' into refactor/input-letter-logic

pull/335/head
Allison Ryan 3 years ago committed by GitHub
commit 6f41ead7ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,11 +1,33 @@
{ {
"firestore": {
"rules": "firestore.rules"
},
"hosting": { "hosting": {
"public": "build/web", "public": "build/web",
"site": "ashehwkdkdjruejdnensjsjdne", "site": "ashehwkdkdjruejdnensjsjdne",
"ignore": [ "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"firebase.json", "headers": [
"**/.*", {
"**/node_modules/**" "source": "**/*.@(jpg|jpeg|gif|png)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=3600"
}
]
},
{
"source": "**",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache, no-store, must-revalidate"
}
]
}
] ]
},
"storage": {
"rules": "storage.rules"
} }
} }

@ -0,0 +1,29 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /leaderboard/{userId} {
function prohibited(initials) {
let prohibitedInitials = get(/databases/$(database)/documents/prohibitedInitials/list).data.prohibitedInitials;
return initials in prohibitedInitials;
}
function inCharLimit(initials) {
return initials.size() < 4;
}
function isAuthedUser(auth) {
return request.auth.uid != null && auth.token.firebase.sign_in_provider == "anonymous"
}
// Leaderboard can be read if it doesn't contain any prohibited initials
allow read: if !prohibited(resource.data.playerInitials);
// A leaderboard entry can be created if the user is authenticated,
// it's 3 characters long, and not a prohibited combination.
allow create: if isAuthedUser(request.auth) &&
inCharLimit(request.resource.data.playerInitials) &&
!prohibited(request.resource.data.playerInitials);
}
}
}

@ -8,6 +8,7 @@ import 'package:leaderboard_repository/leaderboard_repository.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/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -16,15 +17,15 @@ class App extends StatelessWidget {
Key? key, Key? key,
required AuthenticationRepository authenticationRepository, required AuthenticationRepository authenticationRepository,
required LeaderboardRepository leaderboardRepository, required LeaderboardRepository leaderboardRepository,
required PinballAudio pinballAudio, required PinballPlayer pinballPlayer,
}) : _authenticationRepository = authenticationRepository, }) : _authenticationRepository = authenticationRepository,
_leaderboardRepository = leaderboardRepository, _leaderboardRepository = leaderboardRepository,
_pinballAudio = pinballAudio, _pinballPlayer = pinballPlayer,
super(key: key); super(key: key);
final AuthenticationRepository _authenticationRepository; final AuthenticationRepository _authenticationRepository;
final LeaderboardRepository _leaderboardRepository; final LeaderboardRepository _leaderboardRepository;
final PinballAudio _pinballAudio; final PinballPlayer _pinballPlayer;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -32,10 +33,13 @@ class App extends StatelessWidget {
providers: [ providers: [
RepositoryProvider.value(value: _authenticationRepository), RepositoryProvider.value(value: _authenticationRepository),
RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _leaderboardRepository),
RepositoryProvider.value(value: _pinballAudio), RepositoryProvider.value(value: _pinballPlayer),
],
child: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => CharacterThemeCubit()),
BlocProvider(create: (_) => StartGameBloc()),
], ],
child: BlocProvider(
create: (context) => CharacterThemeCubit(),
child: MaterialApp( child: MaterialApp(
title: 'I/O Pinball', title: 'I/O Pinball',
theme: PinballTheme.standard, theme: PinballTheme.standard,

@ -3,12 +3,13 @@
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:pinball/game/pinball_game.dart'; import 'package:pinball/game/pinball_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> { class BumperNoisyBehavior extends ContactBehavior with HasGameRef<PinballGame> {
@override @override
void beginContact(Object other, Contact contact) { void beginContact(Object other, Contact contact) {
super.beginContact(other, contact); super.beginContact(other, contact);
gameRef.audio.bumper(); gameRef.player.play(PinballAudio.bumper);
} }
} }

@ -40,7 +40,8 @@ class ScoringBehavior extends Component with HasGameRef<PinballGame> {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
gameRef.read<GameBloc>().add(Scored(points: _points.value)); gameRef.read<GameBloc>().add(Scored(points: _points.value));
await gameRef.firstChild<ZCanvasComponent>()!.add( final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
await canvas.add(
ScoreComponent( ScoreComponent(
points: _points, points: _points,
position: _position, position: _position,

@ -14,6 +14,16 @@ class GameBloc extends Bloc<GameEvent, GameState> {
on<MultiplierIncreased>(_onIncreasedMultiplier); on<MultiplierIncreased>(_onIncreasedMultiplier);
on<BonusActivated>(_onBonusActivated); on<BonusActivated>(_onBonusActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated); on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
on<GameOver>(_onGameOver);
on<GameStarted>(_onGameStarted);
}
void _onGameStarted(GameStarted _, Emitter emit) {
emit(state.copyWith(status: GameStatus.playing));
}
void _onGameOver(GameOver _, Emitter emit) {
emit(state.copyWith(status: GameStatus.gameOver));
} }
void _onRoundLost(RoundLost event, Emitter emit) { void _onRoundLost(RoundLost event, Emitter emit) {
@ -26,12 +36,13 @@ class GameBloc extends Bloc<GameEvent, GameState> {
roundScore: 0, roundScore: 0,
multiplier: 1, multiplier: 1,
rounds: roundsLeft, rounds: roundsLeft,
status: roundsLeft == 0 ? GameStatus.gameOver : state.status,
), ),
); );
} }
void _onScored(Scored event, Emitter emit) { void _onScored(Scored event, Emitter emit) {
if (!state.isGameOver) { if (state.status.isPlaying) {
emit( emit(
state.copyWith(roundScore: state.roundScore + event.points), state.copyWith(roundScore: state.roundScore + event.points),
); );
@ -39,7 +50,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
} }
void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) { void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) {
if (!state.isGameOver) { if (state.status.isPlaying) {
emit( emit(
state.copyWith( state.copyWith(
multiplier: math.min(state.multiplier + 1, 6), multiplier: math.min(state.multiplier + 1, 6),

@ -59,3 +59,17 @@ class MultiplierIncreased extends GameEvent {
@override @override
List<Object?> get props => []; List<Object?> get props => [];
} }
class GameStarted extends GameEvent {
const GameStarted();
@override
List<Object?> get props => [];
}
class GameOver extends GameEvent {
const GameOver();
@override
List<Object?> get props => [];
}

@ -20,6 +20,17 @@ enum GameBonus {
androidSpaceship, androidSpaceship,
} }
enum GameStatus {
waiting,
playing,
gameOver,
}
extension GameStatusX on GameStatus {
bool get isPlaying => this == GameStatus.playing;
bool get isGameOver => this == GameStatus.gameOver;
}
/// {@template game_state} /// {@template game_state}
/// Represents the state of the pinball game. /// Represents the state of the pinball game.
/// {@endtemplate} /// {@endtemplate}
@ -31,13 +42,15 @@ class GameState extends Equatable {
required this.multiplier, required this.multiplier,
required this.rounds, required this.rounds,
required this.bonusHistory, required this.bonusHistory,
required this.status,
}) : assert(totalScore >= 0, "TotalScore can't be negative"), }) : assert(totalScore >= 0, "TotalScore can't be negative"),
assert(roundScore >= 0, "Round score can't be negative"), assert(roundScore >= 0, "Round score can't be negative"),
assert(multiplier > 0, 'Multiplier must be greater than zero'), assert(multiplier > 0, 'Multiplier must be greater than zero'),
assert(rounds >= 0, "Number of rounds can't be negative"); assert(rounds >= 0, "Number of rounds can't be negative");
const GameState.initial() const GameState.initial()
: totalScore = 0, : status = GameStatus.waiting,
totalScore = 0,
roundScore = 0, roundScore = 0,
multiplier = 1, multiplier = 1,
rounds = 3, rounds = 3,
@ -65,8 +78,7 @@ class GameState extends Equatable {
/// PinballGame. /// PinballGame.
final List<GameBonus> bonusHistory; final List<GameBonus> bonusHistory;
/// Determines when the game is over. final GameStatus status;
bool get isGameOver => rounds == 0;
/// The score displayed at the game. /// The score displayed at the game.
int get displayScore => roundScore + totalScore; int get displayScore => roundScore + totalScore;
@ -78,6 +90,7 @@ class GameState extends Equatable {
int? balls, int? balls,
int? rounds, int? rounds,
List<GameBonus>? bonusHistory, List<GameBonus>? bonusHistory,
GameStatus? status,
}) { }) {
assert( assert(
totalScore == null || totalScore >= this.totalScore, totalScore == null || totalScore >= this.totalScore,
@ -90,6 +103,7 @@ class GameState extends Equatable {
multiplier: multiplier ?? this.multiplier, multiplier: multiplier ?? this.multiplier,
rounds: rounds ?? this.rounds, rounds: rounds ?? this.rounds,
bonusHistory: bonusHistory ?? this.bonusHistory, bonusHistory: bonusHistory ?? this.bonusHistory,
status: status ?? this.status,
); );
} }
@ -100,5 +114,6 @@ class GameState extends Equatable {
multiplier, multiplier,
rounds, rounds,
bonusHistory, bonusHistory,
status,
]; ];
} }

@ -15,7 +15,16 @@ class AndroidAcres extends Component {
AndroidAcres() AndroidAcres()
: super( : super(
children: [ children: [
SpaceshipRamp(), SpaceshipRamp(
children: [
RampShotBehavior(
points: Points.fiveThousand,
),
RampBonusBehavior(
points: Points.oneMillion,
),
],
),
SpaceshipRail(), SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)), AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic( AndroidAnimatronic(

@ -1 +1,3 @@
export 'android_spaceship_bonus_behavior.dart'; export 'android_spaceship_bonus_behavior.dart';
export 'ramp_bonus_behavior.dart';
export 'ramp_shot_behavior.dart';

@ -0,0 +1,62 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_bonus_behavior}
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
/// {@endtemplate}
class RampBonusBehavior extends Component
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
/// {@macro ramp_bonus_behavior}
RampBonusBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampBonusBehavior].
///
/// This can be used for testing [RampBonusBehavior] in isolation.
@visibleForTesting
RampBonusBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (achievedOneMillionPoints) {
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -60),
duration: 2,
),
);
}
});
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
}
}

@ -0,0 +1,63 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/cupertino.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_shot_behavior}
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
/// {@endtemplate}
class RampShotBehavior extends Component
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
/// {@macro ramp_shot_behavior}
RampShotBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampShotBehavior].
///
/// This can be used for testing [RampShotBehavior] in isolation.
@visibleForTesting
RampShotBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (!achievedOneMillionPoints) {
gameRef.read<GameBloc>().add(const MultiplierIncreased());
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -45),
),
);
}
});
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
}
}

@ -8,7 +8,7 @@ export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart'; export 'dino_desert/dino_desert.dart';
export 'drain.dart'; export 'drain.dart';
export 'flutter_forest/flutter_forest.dart'; export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart'; export 'game_bloc_status_listener.dart';
export 'google_word/google_word.dart'; export 'google_word/google_word.dart';
export 'launcher.dart'; export 'launcher.dart';
export 'multiballs/multiballs.dart'; export 'multiballs/multiballs.dart';

@ -1,7 +1,6 @@
// 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:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -17,7 +16,7 @@ class ControlledBall extends Ball with Controls<BallController> {
/// A [Ball] that launches from the [Plunger]. /// A [Ball] that launches from the [Plunger].
ControlledBall.launch({ ControlledBall.launch({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
}) : super(baseColor: characterTheme.ballColor) { }) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this); controller = BallController(this);
layer = Layer.launcher; layer = Layer.launcher;
zIndex = ZIndexes.ballOnLaunchRamp; zIndex = ZIndexes.ballOnLaunchRamp;
@ -28,13 +27,13 @@ class ControlledBall extends Ball with Controls<BallController> {
/// {@endtemplate} /// {@endtemplate}
ControlledBall.bonus({ ControlledBall.bonus({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
}) : super(baseColor: characterTheme.ballColor) { }) : super(assetPath: characterTheme.ball.keyName) {
controller = BallController(this); controller = BallController(this);
zIndex = ZIndexes.ballOnBoard; zIndex = ZIndexes.ballOnBoard;
} }
/// [Ball] used in [DebugPinballGame]. /// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { ControlledBall.debug() : super() {
controller = BallController(this); controller = BallController(this);
zIndex = ZIndexes.ballOnBoard; zIndex = ZIndexes.ballOnBoard;
} }

@ -37,7 +37,7 @@ class FlipperController extends ComponentController<Flipper>
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (state?.isGameOver ?? false) return true; if (state?.status.isGameOver ?? false) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -38,7 +38,7 @@ class PlungerController extends ComponentController<Plunger>
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (state?.isGameOver ?? false) return true; if (state?.status.isGameOver ?? false) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -36,12 +36,14 @@ class DinoDesert extends Component {
} }
class _BarrierBehindDino extends BodyComponent { class _BarrierBehindDino extends BodyComponent {
_BarrierBehindDino() : super(renderBody: false);
@override @override
Body createBody() { Body createBody() {
final shape = EdgeShape() final shape = EdgeShape()
..set( ..set(
Vector2(25, -14.2), Vector2(25.3, -14.2),
Vector2(25, -7.7), Vector2(25.3, -7.7),
); );
return world.createBody(BodyDef())..createFixtureFromShape(shape); return world.createBody(BodyDef())..createFixtureFromShape(shape);

@ -17,7 +17,7 @@ class FlutterForestBonusBehavior extends Component
final bumpers = parent.children.whereType<DashNestBumper>(); final bumpers = parent.children.whereType<DashNestBumper>();
final signpost = parent.firstChild<Signpost>()!; final signpost = parent.firstChild<Signpost>()!;
final animatronic = parent.firstChild<DashAnimatronic>()!; final animatronic = parent.firstChild<DashAnimatronic>()!;
final canvas = gameRef.firstChild<ZCanvasComponent>()!; 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 // TODO(alestiago): Refactor subscription management once the following is

@ -0,0 +1,33 @@
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';
/// Listens to the [GameBloc] and updates the game accordingly.
class GameBlocStatusListener extends Component
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.status != newState.status;
}
@override
void onNewState(GameState state) {
switch (state.status) {
case GameStatus.waiting:
break;
case GameStatus.playing:
gameRef.player.play(PinballAudio.backgroundMusic);
gameRef.firstChild<CameraController>()?.focusOnGame();
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
case GameStatus.gameOver:
gameRef.descendants().whereType<Backbox>().first.initialsInput(
score: state.displayScore,
characterIconPath: gameRef.characterTheme.leaderboardIcon.keyName,
);
gameRef.firstChild<CameraController>()!.focusOnGameOverBackbox();
break;
}
}
}

@ -1,45 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template game_flow_controller}
/// A [Component] that controls the game over and game restart logic
/// {@endtemplate}
class GameFlowController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState> {
/// {@macro game_flow_controller}
GameFlowController(PinballGame component) : super(component);
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.isGameOver != newState.isGameOver;
}
@override
void onNewState(GameState state) {
if (state.isGameOver) {
_initialsInput();
} else {
start();
}
}
/// Puts the game in the initials input state.
void _initialsInput() {
// TODO(erickzanardo): implement score submission and "navigate" to the
// next page
component.descendants().whereType<Backbox>().first.initialsInput(
score: state?.displayScore ?? 0,
characterIconPath: component.characterTheme.leaderboardIcon.keyName,
);
component.firstChild<CameraController>()!.focusOnGameOverBackbox();
}
/// Puts the game in the playing state.
void start() {
component.audio.backgroundMusic();
component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay);
}
}

@ -1,5 +1,6 @@
import 'package:flame/components.dart'; import 'package:flame/components.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';
@ -20,7 +21,7 @@ class GoogleWordBonusBehavior extends Component
.every((letter) => letter.bloc.state == GoogleLetterState.lit); .every((letter) => letter.bloc.state == GoogleLetterState.lit);
if (achievedBonus) { if (achievedBonus) {
gameRef.audio.googleBonus(); gameRef.player.play(PinballAudio.google);
gameRef gameRef
.read<GameBloc>() .read<GameBloc>()
.add(const BonusActivated(GameBonus.googleWord)); .add(const BonusActivated(GameBonus.googleWord));

@ -13,7 +13,6 @@ extension PinballGameAssetsX on PinballGame {
return [ return [
images.load(components.Assets.images.boardBackground.keyName), images.load(components.Assets.images.boardBackground.keyName),
images.load(components.Assets.images.ball.ball.keyName),
images.load(components.Assets.images.ball.flameEffect.keyName), images.load(components.Assets.images.ball.flameEffect.keyName),
images.load(components.Assets.images.signpost.inactive.keyName), images.load(components.Assets.images.signpost.inactive.keyName),
images.load(components.Assets.images.signpost.active1.keyName), images.load(components.Assets.images.signpost.active1.keyName),
@ -132,10 +131,18 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.flapper.backSupport.keyName), images.load(components.Assets.images.flapper.backSupport.keyName),
images.load(components.Assets.images.flapper.frontSupport.keyName), images.load(components.Assets.images.flapper.frontSupport.keyName),
images.load(components.Assets.images.flapper.flap.keyName), images.load(components.Assets.images.flapper.flap.keyName),
images.load(components.Assets.images.skillShot.decal.keyName),
images.load(components.Assets.images.skillShot.pin.keyName),
images.load(components.Assets.images.skillShot.lit.keyName),
images.load(components.Assets.images.skillShot.dimmed.keyName),
images.load(dashTheme.leaderboardIcon.keyName), images.load(dashTheme.leaderboardIcon.keyName),
images.load(sparkyTheme.leaderboardIcon.keyName), images.load(sparkyTheme.leaderboardIcon.keyName),
images.load(androidTheme.leaderboardIcon.keyName), images.load(androidTheme.leaderboardIcon.keyName),
images.load(dinoTheme.leaderboardIcon.keyName), images.load(dinoTheme.leaderboardIcon.keyName),
images.load(androidTheme.ball.keyName),
images.load(dashTheme.ball.keyName),
images.load(dinoTheme.ball.keyName),
images.load(sparkyTheme.ball.keyName),
]; ];
} }
} }

@ -7,6 +7,7 @@ import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/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_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
@ -22,8 +23,8 @@ class PinballGame extends PinballForge2DGame
MultiTouchTapDetector { MultiTouchTapDetector {
PinballGame({ PinballGame({
required this.characterTheme, required this.characterTheme,
required this.audio,
required this.l10n, required this.l10n,
required this.player,
}) : super(gravity: Vector2(0, 30)) { }) : super(gravity: Vector2(0, 30)) {
images.prefix = ''; images.prefix = '';
controller = _GameBallsController(this); controller = _GameBallsController(this);
@ -37,15 +38,12 @@ class PinballGame extends PinballForge2DGame
final CharacterTheme characterTheme; final CharacterTheme characterTheme;
final PinballAudio audio; final PinballPlayer player;
final AppLocalizations l10n; final AppLocalizations l10n;
late final GameFlowController gameFlowController;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await add(gameFlowController = GameFlowController(this));
await add(CameraController(this)); await add(CameraController(this));
final machine = [ final machine = [
@ -57,6 +55,11 @@ class PinballGame extends PinballForge2DGame
GoogleWord(position: Vector2(-4.25, 1.8)), GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(), Multipliers(),
Multiballs(), Multiballs(),
SkillShot(
children: [
ScoringContactBehavior(points: Points.oneMillion),
],
),
]; ];
final characterAreas = [ final characterAreas = [
AndroidAcres(), AndroidAcres(),
@ -65,7 +68,16 @@ class PinballGame extends PinballForge2DGame
SparkyScorch(), SparkyScorch(),
]; ];
await add( await addAll(
[
GameBlocStatusListener(),
CanvasComponent(
onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [
ZCanvasComponent( ZCanvasComponent(
children: [ children: [
...machine, ...machine,
@ -76,6 +88,9 @@ class PinballGame extends PinballForge2DGame
Launcher(), Launcher(),
], ],
), ),
],
),
],
); );
await super.onLoad(); await super.onLoad();
@ -136,9 +151,7 @@ class _GameBallsController extends ComponentController<PinballGame>
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty; final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
final notGameOver = !newState.isGameOver; return noBallsLeft && newState.status.isPlaying;
return noBallsLeft && notGameOver;
} }
@override @override
@ -163,7 +176,7 @@ class _GameBallsController extends ComponentController<PinballGame>
plunger.body.position.x, plunger.body.position.x,
plunger.body.position.y - Ball.size.y, plunger.body.position.y - Ball.size.y,
); );
component.firstChild<ZCanvasComponent>()?.add(ball); component.descendants().whereType<ZCanvasComponent>().single.add(ball);
}); });
} }
} }
@ -171,11 +184,11 @@ class _GameBallsController extends ComponentController<PinballGame>
class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
DebugPinballGame({ DebugPinballGame({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
required PinballAudio audio,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballPlayer player,
}) : super( }) : super(
characterTheme: characterTheme, characterTheme: characterTheme,
audio: audio, player: player,
l10n: l10n, l10n: l10n,
) { ) {
controller = _GameBallsController(this); controller = _GameBallsController(this);
@ -197,9 +210,10 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
super.onTapUp(pointerId, info); super.onTapUp(pointerId, info);
if (info.raw.kind == PointerDeviceKind.mouse) { if (info.raw.kind == PointerDeviceKind.mouse) {
final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug() final ball = ControlledBall.debug()
..initialPosition = info.eventPosition.game; ..initialPosition = info.eventPosition.game;
firstChild<ZCanvasComponent>()?.add(ball); canvas.add(ball);
} }
} }
@ -224,10 +238,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector {
} }
void _turboChargeBall(Vector2 line) { void _turboChargeBall(Vector2 line) {
final canvas = descendants().whereType<ZCanvasComponent>().single;
final ball = ControlledBall.debug()..initialPosition = lineStart!; final ball = ControlledBall.debug()..initialPosition = lineStart!;
final impulse = line * -1 * 10; final impulse = line * -1 * 10;
ball.add(BallTurboChargingBehavior(impulse: impulse)); ball.add(BallTurboChargingBehavior(impulse: impulse));
firstChild<ZCanvasComponent>()?.add(ball); canvas.add(ball);
} }
} }

@ -36,31 +36,30 @@ class PinballGamePage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final characterTheme = final characterTheme =
context.read<CharacterThemeCubit>().state.characterTheme; context.read<CharacterThemeCubit>().state.characterTheme;
final audio = context.read<PinballAudio>(); final player = context.read<PinballPlayer>();
final pinballAudio = context.read<PinballAudio>(); final pinballAudio = context.read<PinballPlayer>();
final game = isDebugMode final game = isDebugMode
? DebugPinballGame( ? DebugPinballGame(
characterTheme: characterTheme, characterTheme: characterTheme,
audio: audio, player: player,
l10n: context.l10n, l10n: context.l10n,
) )
: PinballGame( : PinballGame(
characterTheme: characterTheme, characterTheme: characterTheme,
audio: audio, player: player,
l10n: context.l10n, l10n: context.l10n,
); );
final loadables = [ final loadables = [
...game.preLoadAssets(), ...game.preLoadAssets(),
pinballAudio.load(), ...pinballAudio.load(),
...BonusAnimation.loadAssets(), ...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(), ...SelectedCharacter.loadAssets(),
]; ];
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (_) => StartGameBloc(game: game)),
BlocProvider(create: (_) => GameBloc()), BlocProvider(create: (_) => GameBloc()),
BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()), BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()),
], ],
@ -105,11 +104,16 @@ class PinballGameLoadedView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isPlaying = context.select(
(StartGameBloc bloc) => bloc.state.status == StartGameStatus.play,
);
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);
final clampedMargin = leftMargin > 0 ? leftMargin : 0.0;
return Stack( return StartGameListener(
child: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: GameWidget<PinballGame>( child: GameWidget<PinballGame>(
@ -117,24 +121,26 @@ class PinballGameLoadedView extends StatelessWidget {
initialActiveOverlays: const [PinballGame.playButtonOverlay], initialActiveOverlays: const [PinballGame.playButtonOverlay],
overlayBuilderMap: { overlayBuilderMap: {
PinballGame.playButtonOverlay: (context, game) { PinballGame.playButtonOverlay: (context, game) {
return Positioned( return const Positioned(
bottom: 20, bottom: 20,
right: 0, right: 0,
left: 0, left: 0,
child: PlayButtonOverlay(game: game), child: PlayButtonOverlay(),
); );
}, },
}, },
), ),
), ),
// TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc
// status
Positioned( Positioned(
top: 16, top: 0,
left: leftMargin, left: clampedMargin,
child: Visibility(
visible: isPlaying,
child: const GameHud(), child: const GameHud(),
), ),
),
], ],
),
); );
} }
} }

@ -23,16 +23,18 @@ class _GameHudState extends State<GameHud> {
/// Ratio from sprite frame (width 500, height 144) w / h = ratio /// Ratio from sprite frame (width 500, height 144) w / h = ratio
static const _ratio = 3.47; static const _ratio = 3.47;
static const _width = 265.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); final isGameOver =
context.select((GameBloc bloc) => bloc.state.status.isGameOver);
final height = _calculateHeight(context);
return _ScoreViewDecoration( return _ScoreViewDecoration(
child: SizedBox( child: SizedBox(
height: _width / _ratio, height: height,
width: _width, width: height * _ratio,
child: BlocListener<GameBloc, GameState>( child: BlocListener<GameBloc, GameState>(
listenWhen: (previous, current) => listenWhen: (previous, current) =>
previous.bonusHistory.length != current.bonusHistory.length, previous.bonusHistory.length != current.bonusHistory.length,
@ -53,6 +55,17 @@ class _GameHudState extends State<GameHud> {
), ),
); );
} }
double _calculateHeight(BuildContext context) {
final height = MediaQuery.of(context).size.height * 0.09;
if (height > 90) {
return 90;
} else if (height < 60) {
return 60;
} else {
return height;
}
}
} }
class _ScoreViewDecoration extends StatelessWidget { class _ScoreViewDecoration extends StatelessWidget {

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/pinball_game.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
/// {@template play_button_overlay} /// {@template play_button_overlay}
@ -9,13 +9,7 @@ import 'package:pinball_ui/pinball_ui.dart';
/// {@endtemplate} /// {@endtemplate}
class PlayButtonOverlay extends StatelessWidget { class PlayButtonOverlay extends StatelessWidget {
/// {@macro play_button_overlay} /// {@macro play_button_overlay}
const PlayButtonOverlay({ const PlayButtonOverlay({Key? key}) : super(key: key);
Key? key,
required PinballGame game,
}) : _game = game,
super(key: key);
final PinballGame _game;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -23,9 +17,8 @@ class PlayButtonOverlay extends StatelessWidget {
return PinballButton( return PinballButton(
text: l10n.play, text: l10n.play,
onTap: () async { onTap: () {
_game.gameFlowController.start(); context.read<StartGameBloc>().add(const PlayTapped());
await showCharacterSelectionDialog(context);
}, },
); );
} }

@ -13,12 +13,13 @@ class ScoreView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); final isGameOver =
context.select((GameBloc bloc) => bloc.state.status.isGameOver);
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 8, vertical: 2,
), ),
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: kThemeAnimationDuration, duration: kThemeAnimationDuration,
@ -71,9 +72,11 @@ class _ScoreText extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final score = context.select((GameBloc bloc) => bloc.state.displayScore); final score = context.select((GameBloc bloc) => bloc.state.displayScore);
return Text( return FittedBox(
child: Text(
score.formatScore(), score.formatScore(),
style: Theme.of(context).textTheme.headline1, style: Theme.of(context).textTheme.headline2,
),
); );
} }
} }

@ -51,24 +51,16 @@ extension on Control {
} }
} }
Future<void> showHowToPlayDialog(BuildContext context) {
final audio = context.read<PinballAudio>();
return showDialog<void>(
context: context,
builder: (_) => HowToPlayDialog(),
).then((_) {
audio.ioPinballVoiceOver();
});
}
class HowToPlayDialog extends StatefulWidget { class HowToPlayDialog extends StatefulWidget {
HowToPlayDialog({ HowToPlayDialog({
Key? key, Key? key,
required this.onDismissCallback,
@visibleForTesting PlatformHelper? platformHelper, @visibleForTesting PlatformHelper? platformHelper,
}) : platformHelper = platformHelper ?? PlatformHelper(), }) : platformHelper = platformHelper ?? PlatformHelper(),
super(key: key); super(key: key);
final PlatformHelper platformHelper; final PlatformHelper platformHelper;
final VoidCallback onDismissCallback;
@override @override
State<HowToPlayDialog> createState() => _HowToPlayDialogState(); State<HowToPlayDialog> createState() => _HowToPlayDialogState();
@ -82,6 +74,7 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
closeTimer = Timer(const Duration(seconds: 3), () { closeTimer = Timer(const Duration(seconds: 3), () {
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
widget.onDismissCallback.call();
} }
}); });
} }
@ -96,10 +89,20 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMobile = widget.platformHelper.isMobile; final isMobile = widget.platformHelper.isMobile;
final l10n = context.l10n; final l10n = context.l10n;
return PinballDialog(
return WillPopScope(
onWillPop: () {
widget.onDismissCallback.call();
context.read<PinballPlayer>().play(PinballAudio.ioPinballVoiceOver);
return Future.value(true);
},
child: PinballDialog(
title: l10n.howToPlay, title: l10n.howToPlay,
subtitle: l10n.tipsForFlips, subtitle: l10n.tipsForFlips,
child: FittedBox(
child: isMobile ? const _MobileBody() : const _DesktopBody(), child: isMobile ? const _MobileBody() : const _DesktopBody(),
),
),
); );
} }
} }
@ -111,8 +114,7 @@ class _MobileBody extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final paddingWidth = MediaQuery.of(context).size.width * 0.15; final paddingWidth = MediaQuery.of(context).size.width * 0.15;
final paddingHeight = MediaQuery.of(context).size.height * 0.075; final paddingHeight = MediaQuery.of(context).size.height * 0.075;
return FittedBox( return Padding(
child: Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: paddingWidth, horizontal: paddingWidth,
), ),
@ -123,7 +125,6 @@ class _MobileBody extends StatelessWidget {
const _MobileFlipperControls(), const _MobileFlipperControls(),
], ],
), ),
),
); );
} }
} }
@ -191,13 +192,15 @@ class _DesktopBody extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: const [ children: const [
SizedBox(height: 16),
_DesktopLaunchControls(), _DesktopLaunchControls(),
SizedBox(height: 16), SizedBox(height: 16),
_DesktopFlipperControls(), _DesktopFlipperControls(),
], ],
),
); );
} }
} }

@ -11,7 +11,7 @@ void main() {
bootstrap((firestore, firebaseAuth) async { bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore); final leaderboardRepository = LeaderboardRepository(firestore);
final authenticationRepository = AuthenticationRepository(firebaseAuth); final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudio = PinballAudio(); final pinballPlayer = PinballPlayer();
unawaited( unawaited(
Firebase.initializeApp().then( Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(), (_) => authenticationRepository.authenticateAnonymously(),
@ -20,7 +20,7 @@ void main() {
return App( return App(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
pinballAudio: pinballAudio, pinballPlayer: pinballPlayer,
); );
}); });
} }

@ -11,7 +11,7 @@ void main() {
bootstrap((firestore, firebaseAuth) async { bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore); final leaderboardRepository = LeaderboardRepository(firestore);
final authenticationRepository = AuthenticationRepository(firebaseAuth); final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudio = PinballAudio(); final pinballPlayer = PinballPlayer();
unawaited( unawaited(
Firebase.initializeApp().then( Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(), (_) => authenticationRepository.authenticateAnonymously(),
@ -20,7 +20,7 @@ void main() {
return App( return App(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
pinballAudio: pinballAudio, pinballPlayer: pinballPlayer,
); );
}); });
} }

@ -11,7 +11,7 @@ void main() {
bootstrap((firestore, firebaseAuth) async { bootstrap((firestore, firebaseAuth) async {
final leaderboardRepository = LeaderboardRepository(firestore); final leaderboardRepository = LeaderboardRepository(firestore);
final authenticationRepository = AuthenticationRepository(firebaseAuth); final authenticationRepository = AuthenticationRepository(firebaseAuth);
final pinballAudio = PinballAudio(); final pinballPlayer = PinballPlayer();
unawaited( unawaited(
Firebase.initializeApp().then( Firebase.initializeApp().then(
(_) => authenticationRepository.authenticateAnonymously(), (_) => authenticationRepository.authenticateAnonymously(),
@ -20,7 +20,7 @@ void main() {
return App( return App(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
leaderboardRepository: leaderboardRepository, leaderboardRepository: leaderboardRepository,
pinballAudio: pinballAudio, pinballPlayer: pinballPlayer,
); );
}); });
} }

@ -1,20 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
/// Inflates [CharacterSelectionDialog] using [showDialog].
Future<void> showCharacterSelectionDialog(BuildContext context) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => const CharacterSelectionDialog(),
);
}
/// {@template character_selection_dialog} /// {@template character_selection_dialog}
/// Dialog used to select the playing character of the game. /// Dialog used to select the playing character of the game.
/// {@endtemplate character_selection_dialog} /// {@endtemplate character_selection_dialog}
@ -59,7 +50,7 @@ class _SelectCharacterButton extends StatelessWidget {
return PinballButton( return PinballButton(
onTap: () async { onTap: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
await showHowToPlayDialog(context); context.read<StartGameBloc>().add(const CharacterSelected());
}, },
text: l10n.select, text: l10n.select,
); );
@ -74,7 +65,8 @@ class _CharacterGrid extends StatelessWidget {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Column( Expanded(
child: Column(
children: [ children: [
_Character( _Character(
key: const Key('sparky_character_selection'), key: const Key('sparky_character_selection'),
@ -89,8 +81,10 @@ class _CharacterGrid extends StatelessWidget {
), ),
], ],
), ),
),
const SizedBox(width: 6), const SizedBox(width: 6),
Column( Expanded(
child: Column(
children: [ children: [
_Character( _Character(
key: const Key('dash_character_selection'), key: const Key('dash_character_selection'),
@ -105,6 +99,7 @@ class _CharacterGrid extends StatelessWidget {
), ),
], ],
), ),
),
], ],
); );
}, },

@ -1,6 +1,5 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:pinball/game/game.dart';
part 'start_game_event.dart'; part 'start_game_event.dart';
part 'start_game_state.dart'; part 'start_game_state.dart';
@ -10,23 +9,16 @@ part 'start_game_state.dart';
/// {@endtemplate} /// {@endtemplate}
class StartGameBloc extends Bloc<StartGameEvent, StartGameState> { class StartGameBloc extends Bloc<StartGameEvent, StartGameState> {
/// {@macro start_game_bloc} /// {@macro start_game_bloc}
StartGameBloc({ StartGameBloc() : super(const StartGameState.initial()) {
required PinballGame game,
}) : _game = game,
super(const StartGameState.initial()) {
on<PlayTapped>(_onPlayTapped); on<PlayTapped>(_onPlayTapped);
on<CharacterSelected>(_onCharacterSelected); on<CharacterSelected>(_onCharacterSelected);
on<HowToPlayFinished>(_onHowToPlayFinished); on<HowToPlayFinished>(_onHowToPlayFinished);
} }
final PinballGame _game;
void _onPlayTapped( void _onPlayTapped(
PlayTapped event, PlayTapped event,
Emitter<StartGameState> emit, Emitter<StartGameState> emit,
) { ) {
_game.gameFlowController.start();
emit( emit(
state.copyWith( state.copyWith(
status: StartGameStatus.selectCharacter, status: StartGameStatus.selectCharacter,

@ -1 +1,2 @@
export 'bloc/start_game_bloc.dart'; export 'bloc/start_game_bloc.dart';
export 'widgets/start_game_listener.dart';

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template start_game_listener}
/// Listener that manages the display of dialogs for [StartGameStatus].
///
/// It's responsible for starting the game after pressing play button
/// and playing a sound after the 'how to play' dialog.
/// {@endtemplate}
class StartGameListener extends StatelessWidget {
/// {@macro start_game_listener}
const StartGameListener({
Key? key,
required Widget child,
}) : _child = child,
super(key: key);
final Widget _child;
@override
Widget build(BuildContext context) {
return BlocListener<StartGameBloc, StartGameState>(
listener: (context, state) {
switch (state.status) {
case StartGameStatus.initial:
break;
case StartGameStatus.selectCharacter:
_onSelectCharacter(context);
context.read<GameBloc>().add(const GameStarted());
break;
case StartGameStatus.howToPlay:
_onHowToPlay(context);
break;
case StartGameStatus.play:
break;
}
},
child: _child,
);
}
void _onSelectCharacter(BuildContext context) {
_showPinballDialog(
context: context,
child: const CharacterSelectionDialog(),
barrierDismissible: false,
);
}
void _onHowToPlay(BuildContext context) {
_showPinballDialog(
context: context,
child: HowToPlayDialog(
onDismissCallback: () {
context.read<StartGameBloc>().add(const HowToPlayFinished());
},
),
);
}
void _showPinballDialog({
required BuildContext context,
required Widget child,
bool barrierDismissible = true,
}) {
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
showDialog<void>(
context: context,
barrierColor: PinballColors.transparent,
barrierDismissible: barrierDismissible,
builder: (_) {
return Center(
child: SizedBox(
height: gameWidgetWidth * 0.87,
width: gameWidgetWidth,
child: child,
),
);
},
);
}
}

@ -3,10 +3,25 @@ import 'dart:math';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flame_audio/audio_pool.dart'; import 'package:flame_audio/audio_pool.dart';
import 'package:flame_audio/flame_audio.dart'; import 'package:flame_audio/flame_audio.dart';
import 'package:flutter/material.dart';
import 'package:pinball_audio/gen/assets.gen.dart'; import 'package:pinball_audio/gen/assets.gen.dart';
/// Function that defines the contract of the creation /// Sounds available for play
/// of an [AudioPool] enum PinballAudio {
/// Google
google,
/// Bumper
bumper,
/// Background music
backgroundMusic,
/// IO Pinball voice over
ioPinballVoiceOver
}
/// Defines the contract of the creation of an [AudioPool].
typedef CreateAudioPool = Future<AudioPool> Function( typedef CreateAudioPool = Future<AudioPool> Function(
String sound, { String sound, {
bool? repeating, bool? repeating,
@ -31,12 +46,97 @@ typedef PreCacheSingleAudio = Future<void> Function(String);
/// an [AudioCache] instance /// an [AudioCache] instance
typedef ConfigureAudioCache = void Function(AudioCache); typedef ConfigureAudioCache = void Function(AudioCache);
/// {@template pinball_audio} abstract class _Audio {
void play();
Future<void> load();
String prefixFile(String file) {
return 'packages/pinball_audio/$file';
}
}
class _SimplePlayAudio extends _Audio {
_SimplePlayAudio({
required this.preCacheSingleAudio,
required this.playSingleAudio,
required this.path,
});
final PreCacheSingleAudio preCacheSingleAudio;
final PlaySingleAudio playSingleAudio;
final String path;
@override
Future<void> load() => preCacheSingleAudio(prefixFile(path));
@override
void play() {
playSingleAudio(prefixFile(path));
}
}
class _LoopAudio extends _Audio {
_LoopAudio({
required this.preCacheSingleAudio,
required this.loopSingleAudio,
required this.path,
});
final PreCacheSingleAudio preCacheSingleAudio;
final LoopSingleAudio loopSingleAudio;
final String path;
@override
Future<void> load() => preCacheSingleAudio(prefixFile(path));
@override
void play() {
loopSingleAudio(prefixFile(path));
}
}
class _BumperAudio extends _Audio {
_BumperAudio({
required this.createAudioPool,
required this.seed,
});
final CreateAudioPool createAudioPool;
final Random seed;
late AudioPool bumperA;
late AudioPool bumperB;
@override
Future<void> load() async {
await Future.wait(
[
createAudioPool(
prefixFile(Assets.sfx.bumperA),
maxPlayers: 4,
prefix: '',
).then((pool) => bumperA = pool),
createAudioPool(
prefixFile(Assets.sfx.bumperB),
maxPlayers: 4,
prefix: '',
).then((pool) => bumperB = pool),
],
);
}
@override
void play() {
(seed.nextBool() ? bumperA : bumperB).start(volume: 0.6);
}
}
/// {@template pinball_player}
/// Sound manager for the pinball game /// Sound manager for the pinball game
/// {@endtemplate} /// {@endtemplate}
class PinballAudio { class PinballPlayer {
/// {@macro pinball_audio} /// {@macro pinball_player}
PinballAudio({ PinballPlayer({
CreateAudioPool? createAudioPool, CreateAudioPool? createAudioPool,
PlaySingleAudio? playSingleAudio, PlaySingleAudio? playSingleAudio,
LoopSingleAudio? loopSingleAudio, LoopSingleAudio? loopSingleAudio,
@ -52,7 +152,29 @@ class PinballAudio {
((AudioCache a) { ((AudioCache a) {
a.prefix = ''; a.prefix = '';
}), }),
_seed = seed ?? Random(); _seed = seed ?? Random() {
audios = {
PinballAudio.google: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.google,
),
PinballAudio.ioPinballVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.ioPinballVoiceOver,
),
PinballAudio.bumper: _BumperAudio(
createAudioPool: _createAudioPool,
seed: _seed,
),
PinballAudio.backgroundMusic: _LoopAudio(
preCacheSingleAudio: _preCacheSingleAudio,
loopSingleAudio: _loopSingleAudio,
path: Assets.music.background,
),
};
}
final CreateAudioPool _createAudioPool; final CreateAudioPool _createAudioPool;
@ -66,54 +188,24 @@ class PinballAudio {
final Random _seed; final Random _seed;
late AudioPool _bumperAPool; /// Registered audios on the Player
@visibleForTesting
late AudioPool _bumperBPool; // ignore: library_private_types_in_public_api
late final Map<PinballAudio, _Audio> audios;
/// Loads the sounds effects into the memory /// Loads the sounds effects into the memory
Future<void> load() async { List<Future<void>> load() {
_configureAudioCache(FlameAudio.audioCache); _configureAudioCache(FlameAudio.audioCache);
_bumperAPool = await _createAudioPool( return audios.values.map((a) => a.load()).toList();
_prefixFile(Assets.sfx.bumperA),
maxPlayers: 4,
prefix: '',
);
_bumperBPool = await _createAudioPool(
_prefixFile(Assets.sfx.bumperB),
maxPlayers: 4,
prefix: '',
);
await Future.wait([
_preCacheSingleAudio(_prefixFile(Assets.sfx.google)),
_preCacheSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)),
_preCacheSingleAudio(_prefixFile(Assets.music.background)),
]);
}
/// Plays a random bumper sfx.
void bumper() {
(_seed.nextBool() ? _bumperAPool : _bumperBPool).start(volume: 0.6);
}
/// Plays the google word bonus
void googleBonus() {
_playSingleAudio(_prefixFile(Assets.sfx.google));
}
/// Plays the I/O Pinball voice over audio.
void ioPinballVoiceOver() {
_playSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver));
} }
/// Plays the background music /// Plays the received auido
void backgroundMusic() { void play(PinballAudio audio) {
_loopSingleAudio(_prefixFile(Assets.music.background)); assert(
} audios.containsKey(audio),
'Tried to play unregistered audio $audio',
String _prefixFile(String file) { );
return 'packages/pinball_audio/$file'; audios[audio]?.play();
} }
} }

@ -51,7 +51,7 @@ void main() {
late _MockLoopSingleAudio loopSingleAudio; late _MockLoopSingleAudio loopSingleAudio;
late _PreCacheSingleAudio preCacheSingleAudio; late _PreCacheSingleAudio preCacheSingleAudio;
late Random seed; late Random seed;
late PinballAudio audio; late PinballPlayer player;
setUpAll(() { setUpAll(() {
registerFallbackValue(_MockAudioCache()); registerFallbackValue(_MockAudioCache());
@ -81,7 +81,7 @@ void main() {
seed = _MockRandom(); seed = _MockRandom();
audio = PinballAudio( player = PinballPlayer(
configureAudioCache: configureAudioCache.onCall, configureAudioCache: configureAudioCache.onCall,
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
@ -92,12 +92,12 @@ void main() {
}); });
test('can be instantiated', () { test('can be instantiated', () {
expect(PinballAudio(), isNotNull); expect(PinballPlayer(), isNotNull);
}); });
group('load', () { group('load', () {
test('creates the bumpers pools', () async { test('creates the bumpers pools', () async {
await audio.load(); await Future.wait(player.load());
verify( verify(
() => createAudioPool.onCall( () => createAudioPool.onCall(
@ -117,25 +117,25 @@ void main() {
}); });
test('configures the audio cache instance', () async { test('configures the audio cache instance', () async {
await audio.load(); await Future.wait(player.load());
verify(() => configureAudioCache.onCall(FlameAudio.audioCache)) verify(() => configureAudioCache.onCall(FlameAudio.audioCache))
.called(1); .called(1);
}); });
test('sets the correct prefix', () async { test('sets the correct prefix', () async {
audio = PinballAudio( player = PinballPlayer(
createAudioPool: createAudioPool.onCall, createAudioPool: createAudioPool.onCall,
playSingleAudio: playSingleAudio.onCall, playSingleAudio: playSingleAudio.onCall,
preCacheSingleAudio: preCacheSingleAudio.onCall, preCacheSingleAudio: preCacheSingleAudio.onCall,
); );
await audio.load(); await Future.wait(player.load());
expect(FlameAudio.audioCache.prefix, equals('')); expect(FlameAudio.audioCache.prefix, equals(''));
}); });
test('pre cache the assets', () async { test('pre cache the assets', () async {
await audio.load(); await Future.wait(player.load());
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
@ -184,8 +184,8 @@ void main() {
group('when seed is true', () { group('when seed is true', () {
test('plays the bumper A sound pool', () async { test('plays the bumper A sound pool', () async {
when(seed.nextBool).thenReturn(true); when(seed.nextBool).thenReturn(true);
await audio.load(); await Future.wait(player.load());
audio.bumper(); player.play(PinballAudio.bumper);
verify(() => bumperAPool.start(volume: 0.6)).called(1); verify(() => bumperAPool.start(volume: 0.6)).called(1);
}); });
@ -194,8 +194,8 @@ void main() {
group('when seed is false', () { group('when seed is false', () {
test('plays the bumper B sound pool', () async { test('plays the bumper B sound pool', () async {
when(seed.nextBool).thenReturn(false); when(seed.nextBool).thenReturn(false);
await audio.load(); await Future.wait(player.load());
audio.bumper(); player.play(PinballAudio.bumper);
verify(() => bumperBPool.start(volume: 0.6)).called(1); verify(() => bumperBPool.start(volume: 0.6)).called(1);
}); });
@ -204,8 +204,8 @@ void main() {
group('googleBonus', () { group('googleBonus', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await audio.load(); await Future.wait(player.load());
audio.googleBonus(); player.play(PinballAudio.google);
verify( verify(
() => playSingleAudio () => playSingleAudio
@ -216,8 +216,8 @@ void main() {
group('ioPinballVoiceOver', () { group('ioPinballVoiceOver', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await audio.load(); await Future.wait(player.load());
audio.ioPinballVoiceOver(); player.play(PinballAudio.ioPinballVoiceOver);
verify( verify(
() => playSingleAudio.onCall( () => playSingleAudio.onCall(
@ -229,8 +229,8 @@ void main() {
group('backgroundMusic', () { group('backgroundMusic', () {
test('plays the correct file', () async { test('plays the correct file', () async {
await audio.load(); await Future.wait(player.load());
audio.backgroundMusic(); player.play(PinballAudio.backgroundMusic);
verify( verify(
() => loopSingleAudio () => loopSingleAudio
@ -238,5 +238,15 @@ void main() {
).called(1); ).called(1);
}); });
}); });
test(
'throws assertions error when playing an unregistered audio',
() async {
player.audios.remove(PinballAudio.google);
await Future.wait(player.load());
expect(() => player.play(PinballAudio.google), throwsAssertionError);
},
);
}); });
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 616 KiB

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 KiB

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 569 KiB

@ -35,6 +35,7 @@ class $AssetsImagesGen {
$AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen(); $AssetsImagesPlungerGen get plunger => const $AssetsImagesPlungerGen();
$AssetsImagesScoreGen get score => const $AssetsImagesScoreGen(); $AssetsImagesScoreGen get score => const $AssetsImagesScoreGen();
$AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen(); $AssetsImagesSignpostGen get signpost => const $AssetsImagesSignpostGen();
$AssetsImagesSkillShotGen get skillShot => const $AssetsImagesSkillShotGen();
$AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen(); $AssetsImagesSlingshotGen get slingshot => const $AssetsImagesSlingshotGen();
$AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen(); $AssetsImagesSparkyGen get sparky => const $AssetsImagesSparkyGen();
} }
@ -272,6 +273,26 @@ class $AssetsImagesSignpostGen {
const AssetGenImage('assets/images/signpost/inactive.png'); const AssetGenImage('assets/images/signpost/inactive.png');
} }
class $AssetsImagesSkillShotGen {
const $AssetsImagesSkillShotGen();
/// File path: assets/images/skill_shot/decal.png
AssetGenImage get decal =>
const AssetGenImage('assets/images/skill_shot/decal.png');
/// File path: assets/images/skill_shot/dimmed.png
AssetGenImage get dimmed =>
const AssetGenImage('assets/images/skill_shot/dimmed.png');
/// File path: assets/images/skill_shot/lit.png
AssetGenImage get lit =>
const AssetGenImage('assets/images/skill_shot/lit.png');
/// File path: assets/images/skill_shot/pin.png
AssetGenImage get pin =>
const AssetGenImage('assets/images/skill_shot/pin.png');
}
class $AssetsImagesSlingshotGen { class $AssetsImagesSlingshotGen {
const $AssetsImagesSlingshotGen(); const $AssetsImagesSlingshotGen();

@ -122,7 +122,7 @@ class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent
SpriteAnimationData.sequenced( SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn, amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow, amountPerRow: amountPerRow,
stepTime: 1 / 24, stepTime: 1 / 12,
textureSize: textureSize, textureSize: textureSize,
), ),
); );

@ -2,9 +2,10 @@ import 'dart:async';
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/widgets.dart'; import 'package:flutter/material.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' as theme;
export 'behaviors/behaviors.dart'; export 'behaviors/behaviors.dart';
@ -14,11 +15,11 @@ export 'behaviors/behaviors.dart';
class Ball extends BodyComponent with Layered, InitialPosition, ZIndex { class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// {@macro ball} /// {@macro ball}
Ball({ Ball({
required this.baseColor, String? assetPath,
}) : super( }) : super(
renderBody: false, renderBody: false,
children: [ children: [
_BallSpriteComponent()..tint(baseColor.withOpacity(0.5)), _BallSpriteComponent(assetPath: assetPath),
BallScalingBehavior(), BallScalingBehavior(),
BallGravitatingBehavior(), BallGravitatingBehavior(),
], ],
@ -35,7 +36,7 @@ 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({required this.baseColor}) Ball.test()
: super( : super(
children: [_BallSpriteComponent()], children: [_BallSpriteComponent()],
); );
@ -43,9 +44,6 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
/// 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);
/// The base [Color] used to tint this [Ball].
final Color baseColor;
@override @override
Body createBody() { Body createBody() {
final shape = CircleShape()..radius = size.x / 2; final shape = CircleShape()..radius = size.x / 2;
@ -79,14 +77,22 @@ class Ball extends BodyComponent with Layered, InitialPosition, ZIndex {
} }
class _BallSpriteComponent extends SpriteComponent with HasGameRef { class _BallSpriteComponent extends SpriteComponent with HasGameRef {
_BallSpriteComponent({
this.assetPath,
}) : super(
anchor: Anchor.center,
);
final String? assetPath;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprite = await gameRef.loadSprite( final sprite = Sprite(
Assets.images.ball.ball.keyName, gameRef.images
.fromCache(assetPath ?? theme.Assets.images.dash.ball.keyName),
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 12.5;
anchor = Anchor.center;
} }
} }

@ -7,7 +7,7 @@ import 'package:pinball_components/pinball_components.dart';
part 'chrome_dino_state.dart'; part 'chrome_dino_state.dart';
class ChromeDinoCubit extends Cubit<ChromeDinoState> { class ChromeDinoCubit extends Cubit<ChromeDinoState> {
ChromeDinoCubit() : super(const ChromeDinoState.inital()); ChromeDinoCubit() : super(const ChromeDinoState.initial());
void onOpenMouth() { void onOpenMouth() {
emit(state.copyWith(isMouthOpen: true)); emit(state.copyWith(isMouthOpen: true));

@ -14,7 +14,7 @@ class ChromeDinoState extends Equatable {
this.ball, this.ball,
}); });
const ChromeDinoState.inital() const ChromeDinoState.initial()
: this( : this(
status: ChromeDinoStatus.idle, status: ChromeDinoStatus.idle,
isMouthOpen: false, isMouthOpen: false,

@ -21,7 +21,7 @@ export 'joint_anchor.dart';
export 'kicker/kicker.dart'; export 'kicker/kicker.dart';
export 'launch_ramp.dart'; export 'launch_ramp.dart';
export 'layer.dart'; export 'layer.dart';
export 'layer_sensor.dart'; export 'layer_sensor/layer_sensor.dart';
export 'multiball/multiball.dart'; export 'multiball/multiball.dart';
export 'multiplier/multiplier.dart'; export 'multiplier/multiplier.dart';
export 'plunger.dart'; export 'plunger.dart';
@ -29,9 +29,10 @@ export 'rocket.dart';
export 'score_component.dart'; export 'score_component.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'signpost/signpost.dart'; export 'signpost/signpost.dart';
export 'skill_shot/skill_shot.dart';
export 'slingshot.dart'; export 'slingshot.dart';
export 'spaceship_rail.dart'; export 'spaceship_rail.dart';
export '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.dart';

@ -60,10 +60,10 @@ class DashNestBumper extends BodyComponent with InitialPosition {
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
majorRadius: 3, majorRadius: 3,
minorRadius: 2.5, minorRadius: 2.2,
activeAssetPath: Assets.images.dash.bumper.a.active.keyName, activeAssetPath: Assets.images.dash.bumper.a.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.a.inactive.keyName,
spritePosition: Vector2(0.35, -1.2), spritePosition: Vector2(0.3, -1.3),
bloc: DashNestBumperCubit(), bloc: DashNestBumperCubit(),
children: [ children: [
...?children, ...?children,
@ -75,11 +75,11 @@ class DashNestBumper extends BodyComponent with InitialPosition {
DashNestBumper.b({ DashNestBumper.b({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
majorRadius: 3, majorRadius: 3.1,
minorRadius: 2.5, minorRadius: 2.2,
activeAssetPath: Assets.images.dash.bumper.b.active.keyName, activeAssetPath: Assets.images.dash.bumper.b.active.keyName,
inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName, inactiveAssetPath: Assets.images.dash.bumper.b.inactive.keyName,
spritePosition: Vector2(0.35, -1.2), spritePosition: Vector2(0.4, -1.2),
bloc: DashNestBumperCubit(), bloc: DashNestBumperCubit(),
children: [ children: [
...?children, ...?children,

@ -1,90 +0,0 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template layer_entrance_orientation}
/// Determines if a layer entrance is oriented [up] or [down] on the board.
/// {@endtemplate}
enum LayerEntranceOrientation {
/// Facing up on the Board.
up,
/// Facing down on the Board.
down,
}
/// {@template layer_sensor}
/// [BodyComponent] located at the entrance and exit of a [Layer].
///
/// By default the base [layer] is set to [Layer.board] and the
/// [_outsideZIndex] is set to [ZIndexes.ballOnBoard].
/// {@endtemplate}
abstract class LayerSensor extends BodyComponent
with InitialPosition, Layered, ContactCallbacks {
/// {@macro layer_sensor}
LayerSensor({
required Layer insideLayer,
Layer? outsideLayer,
required int insideZIndex,
int? outsideZIndex,
required this.orientation,
}) : _insideLayer = insideLayer,
_outsideLayer = outsideLayer ?? Layer.board,
_insideZIndex = insideZIndex,
_outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard,
super(renderBody: false) {
layer = Layer.opening;
}
final Layer _insideLayer;
final Layer _outsideLayer;
final int _insideZIndex;
final int _outsideZIndex;
/// The [Shape] of the [LayerSensor].
Shape get shape;
/// {@macro layer_entrance_orientation}
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
// collision calculations.
final LayerEntranceOrientation orientation;
@override
Body createBody() {
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! Ball) return;
if (other.layer != _insideLayer) {
final isBallEnteringOpening =
(orientation == LayerEntranceOrientation.down &&
other.body.linearVelocity.y < 0) ||
(orientation == LayerEntranceOrientation.up &&
other.body.linearVelocity.y > 0);
if (isBallEnteringOpening) {
other
..layer = _insideLayer
..zIndex = _insideZIndex;
}
} else {
other
..layer = _outsideLayer
..zIndex = _outsideZIndex;
}
}
}

@ -0,0 +1,2 @@
export 'behaviors.dart';
export 'layer_filtering_behavior.dart';

@ -0,0 +1,31 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class LayerFilteringBehavior extends ContactBehavior<LayerSensor> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.layer != parent.insideLayer) {
final isBallEnteringOpening =
(parent.orientation == LayerEntranceOrientation.down &&
other.body.linearVelocity.y < 0) ||
(parent.orientation == LayerEntranceOrientation.up &&
other.body.linearVelocity.y > 0);
if (isBallEnteringOpening) {
other
..layer = parent.insideLayer
..zIndex = parent.insideZIndex;
}
} else {
other
..layer = parent.outsideLayer
..zIndex = parent.outsideZIndex;
}
}
}

@ -0,0 +1,66 @@
// ignore_for_file: avoid_renaming_method_parameters, public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/layer_sensor/behaviors/layer_filtering_behavior.dart';
/// {@template layer_entrance_orientation}
/// Determines if a layer entrance is oriented [up] or [down] on the board.
/// {@endtemplate}
enum LayerEntranceOrientation {
/// Facing up on the Board.
up,
/// Facing down on the Board.
down,
}
/// {@template layer_sensor}
/// [BodyComponent] located at the entrance and exit of a [Layer].
///
/// By default the base [layer] is set to [Layer.board] and the
/// [outsideZIndex] is set to [ZIndexes.ballOnBoard].
/// {@endtemplate}
abstract class LayerSensor extends BodyComponent with InitialPosition, Layered {
/// {@macro layer_sensor}
LayerSensor({
required this.insideLayer,
Layer? outsideLayer,
required this.insideZIndex,
int? outsideZIndex,
required this.orientation,
}) : outsideLayer = outsideLayer ?? Layer.board,
outsideZIndex = outsideZIndex ?? ZIndexes.ballOnBoard,
super(
renderBody: false,
children: [LayerFilteringBehavior()],
) {
layer = Layer.opening;
}
final Layer insideLayer;
final Layer outsideLayer;
final int insideZIndex;
final int outsideZIndex;
/// The [Shape] of the [LayerSensor].
Shape get shape;
/// {@macro layer_entrance_orientation}
// TODO(ruimiguel): Try to remove the need of [LayerEntranceOrientation] for
// collision calculations.
final LayerEntranceOrientation orientation;
@override
Body createBody() {
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(position: initialPosition);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -97,7 +97,7 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
void update(double dt) { void update(double dt) {
// Ensure that we only pull or release when the time is greater than zero. // Ensure that we only pull or release when the time is greater than zero.
if (_pullingDownTime > 0) { if (_pullingDownTime > 0) {
_pullingDownTime -= dt; _pullingDownTime -= PinballForge2DGame.clampDt(dt);
if (_pullingDownTime <= 0) { if (_pullingDownTime <= 0) {
release(); release();
} else { } else {

@ -0,0 +1,2 @@
export 'skill_shot_ball_contact_behavior.dart';
export 'skill_shot_blinking_behavior.dart';

@ -0,0 +1,16 @@
// ignore_for_file: public_member_api_docs
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';
class SkillShotBallContactBehavior extends ContactBehavior<SkillShot> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
parent.bloc.onBallContacted();
parent.firstChild<SpriteAnimationComponent>()?.playing = true;
}
}

@ -0,0 +1,44 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template skill_shot_blinking_behavior}
/// Makes a [SkillShot] blink between [SkillShotSpriteState.lit] and
/// [SkillShotSpriteState.dimmed] for a set amount of blinks.
/// {@endtemplate}
class SkillShotBlinkingBehavior extends TimerComponent
with ParentIsA<SkillShot> {
/// {@macro skill_shot_blinking_behavior}
SkillShotBlinkingBehavior() : super(period: 0.15);
final _maxBlinks = 4;
int _blinks = 0;
void _onNewState(SkillShotState state) {
if (state.isBlinking) {
timer
..reset()
..start();
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
timer.stop();
parent.bloc.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
if (_blinks != _maxBlinks * 2) {
parent.bloc.switched();
_blinks++;
} else {
_blinks = 0;
timer.stop();
parent.bloc.onBlinkingFinished();
}
}
}

@ -0,0 +1,39 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'skill_shot_state.dart';
class SkillShotCubit extends Cubit<SkillShotState> {
SkillShotCubit() : super(const SkillShotState.initial());
void onBallContacted() {
emit(
const SkillShotState(
spriteState: SkillShotSpriteState.lit,
isBlinking: true,
),
);
}
void switched() {
switch (state.spriteState) {
case SkillShotSpriteState.lit:
emit(state.copyWith(spriteState: SkillShotSpriteState.dimmed));
break;
case SkillShotSpriteState.dimmed:
emit(state.copyWith(spriteState: SkillShotSpriteState.lit));
break;
}
}
void onBlinkingFinished() {
emit(
const SkillShotState(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
),
);
}
}

@ -0,0 +1,37 @@
// ignore_for_file: public_member_api_docs
part of 'skill_shot_cubit.dart';
enum SkillShotSpriteState {
lit,
dimmed,
}
class SkillShotState extends Equatable {
const SkillShotState({
required this.spriteState,
required this.isBlinking,
});
const SkillShotState.initial()
: this(
spriteState: SkillShotSpriteState.dimmed,
isBlinking: false,
);
final SkillShotSpriteState spriteState;
final bool isBlinking;
SkillShotState copyWith({
SkillShotSpriteState? spriteState,
bool? isBlinking,
}) =>
SkillShotState(
spriteState: spriteState ?? this.spriteState,
isBlinking: isBlinking ?? this.isBlinking,
);
@override
List<Object?> get props => [spriteState, isBlinking];
}

@ -0,0 +1,169 @@
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/skill_shot/behaviors/behaviors.dart';
import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/skill_shot_cubit.dart';
/// {@template skill_shot}
/// Rollover awarding extra points.
/// {@endtemplate}
class SkillShot extends BodyComponent with ZIndex {
/// {@macro skill_shot}
SkillShot({Iterable<Component>? children})
: this._(
children: children,
bloc: SkillShotCubit(),
);
SkillShot._({
Iterable<Component>? children,
required this.bloc,
}) : super(
renderBody: false,
children: [
SkillShotBallContactBehavior(),
SkillShotBlinkingBehavior(),
_RolloverDecalSpriteComponent(),
PinSpriteAnimationComponent(),
_TextDecalSpriteGroupComponent(state: bloc.state.spriteState),
...?children,
],
) {
zIndex = ZIndexes.decal;
}
/// 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;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
0.1,
3.7,
Vector2(-31.9, 9.1),
0.11,
);
final fixtureDef = FixtureDef(shape, isSensor: true);
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}
class _RolloverDecalSpriteComponent extends SpriteComponent with HasGameRef {
_RolloverDecalSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-31.9, 9.1),
angle: 0.11,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = Sprite(
gameRef.images.fromCache(
Assets.images.skillShot.decal.keyName,
),
);
this.sprite = sprite;
size = sprite.originalSize / 20;
}
}
/// {@template pin_sprite_animation_component}
/// Animation for pin in [SkillShot] rollover.
/// {@endtemplate}
@visibleForTesting
class PinSpriteAnimationComponent extends SpriteAnimationComponent
with HasGameRef {
/// {@macro pin_sprite_animation_component}
PinSpriteAnimationComponent()
: super(
anchor: Anchor.center,
position: Vector2(-31.9, 9.1),
angle: 0,
playing: false,
);
@override
Future<void> onLoad() async {
await super.onLoad();
final spriteSheet = gameRef.images.fromCache(
Assets.images.skillShot.pin.keyName,
);
const amountPerRow = 3;
const amountPerColumn = 1;
final textureSize = Vector2(
spriteSheet.width / amountPerRow,
spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
spriteSheet,
SpriteAnimationData.sequenced(
amount: amountPerRow * amountPerColumn,
amountPerRow: amountPerRow,
stepTime: 1 / 24,
textureSize: textureSize,
loop: false,
),
)..onComplete = () {
animation?.reset();
playing = false;
};
}
}
class _TextDecalSpriteGroupComponent
extends SpriteGroupComponent<SkillShotSpriteState>
with HasGameRef, ParentIsA<SkillShot> {
_TextDecalSpriteGroupComponent({
required SkillShotSpriteState state,
}) : super(
anchor: Anchor.center,
position: Vector2(-35.55, 3.59),
current: state,
);
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) => current = state.spriteState);
final sprites = {
SkillShotSpriteState.lit: Sprite(
gameRef.images.fromCache(Assets.images.skillShot.lit.keyName),
),
SkillShotSpriteState.dimmed: Sprite(
gameRef.images.fromCache(Assets.images.skillShot.dimmed.keyName),
),
};
this.sprites = sprites;
size = sprites[current]!.originalSize / 10;
}
}

@ -0,0 +1,24 @@
// ignore_for_file: public_member_api_docs
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_ball_ascending_contact_behavior}
/// Detects an ascending [Ball] that enters into the [SpaceshipRamp].
///
/// The [Ball] can hit with sensor to recognize if a [Ball] goes into or out of
/// the [SpaceshipRamp].
/// {@endtemplate}
class RampBallAscendingContactBehavior
extends ContactBehavior<RampScoringSensor> {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
if (other.body.linearVelocity.y < 0) {
parent.parent.bloc.onAscendingBallEntered();
}
}
}

@ -0,0 +1,16 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'spaceship_ramp_state.dart';
class SpaceshipRampCubit extends Cubit<SpaceshipRampState> {
SpaceshipRampCubit() : super(const SpaceshipRampState.initial());
void onAscendingBallEntered() {
emit(
state.copyWith(hits: state.hits + 1),
);
}
}

@ -0,0 +1,24 @@
// ignore_for_file: public_member_api_docs
part of 'spaceship_ramp_cubit.dart';
class SpaceshipRampState extends Equatable {
const SpaceshipRampState({
required this.hits,
}) : assert(hits >= 0, "Hits can't be negative");
const SpaceshipRampState.initial() : this(hits: 0);
final int hits;
SpaceshipRampState copyWith({
int? hits,
}) {
return SpaceshipRampState(
hits: hits ?? this.hits,
);
}
@override
List<Object?> get props => [hits];
}

@ -5,16 +5,35 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart'; import 'package:pinball_components/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
export 'cubit/spaceship_ramp_cubit.dart';
/// {@template spaceship_ramp} /// {@template spaceship_ramp}
/// Ramp leading into the [AndroidSpaceship]. /// Ramp leading into the [AndroidSpaceship].
/// {@endtemplate} /// {@endtemplate}
class SpaceshipRamp extends Component { class SpaceshipRamp extends Component {
/// {@macro spaceship_ramp} /// {@macro spaceship_ramp}
SpaceshipRamp() SpaceshipRamp({
: super( Iterable<Component>? children,
}) : this._(
children: children,
bloc: SpaceshipRampCubit(),
);
SpaceshipRamp._({
Iterable<Component>? children,
required this.bloc,
}) : super(
children: [ children: [
// TODO(ruimiguel): refactor RampScoringSensor and
// _SpaceshipRampOpening to be in only one sensor if possible.
RampScoringSensor(
children: [
RampBallAscendingContactBehavior(),
],
)..initialPosition = Vector2(1.7, -20.4),
_SpaceshipRampOpening( _SpaceshipRampOpening(
outsidePriority: ZIndexes.ballOnBoard, outsidePriority: ZIndexes.ballOnBoard,
rotation: math.pi, rotation: math.pi,
@ -34,60 +53,30 @@ class SpaceshipRamp extends Component {
_SpaceshipRampForegroundRailing(), _SpaceshipRampForegroundRailing(),
_SpaceshipRampBase()..initialPosition = Vector2(1.7, -20), _SpaceshipRampBase()..initialPosition = Vector2(1.7, -20),
_SpaceshipRampBackgroundRailingSpriteComponent(), _SpaceshipRampBackgroundRailingSpriteComponent(),
_SpaceshipRampArrowSpriteComponent(), SpaceshipRampArrowSpriteComponent(
current: bloc.state.hits,
),
...?children,
], ],
); );
/// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. /// Creates a [SpaceshipRamp] without any children.
/// ///
/// If the current state is the last one it cycles back to the initial state. /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation.
void progress() => @visibleForTesting
firstChild<_SpaceshipRampArrowSpriteComponent>()?.progress(); SpaceshipRamp.test({
} required this.bloc,
}) : super();
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState { // TODO(alestiago): Consider refactoring once the following is merged:
String get path { // https://github.com/flame-engine/flame/pull/1538
switch (this) { // ignore: public_member_api_docs
case SpaceshipRampArrowSpriteState.inactive: final SpaceshipRampCubit bloc;
return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
return Assets.images.android.ramp.arrow.active5.keyName;
}
}
SpaceshipRampArrowSpriteState get next { @override
return SpaceshipRampArrowSpriteState void onRemove() {
.values[(index + 1) % SpaceshipRampArrowSpriteState.values.length]; bloc.close();
super.onRemove();
} }
} }
@ -194,37 +183,81 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
/// ///
/// Lights progressively whenever a [Ball] gets into [SpaceshipRamp]. /// Lights progressively whenever a [Ball] gets into [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class _SpaceshipRampArrowSpriteComponent @visibleForTesting
extends SpriteGroupComponent<SpaceshipRampArrowSpriteState> class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent<int>
with HasGameRef, ZIndex { with HasGameRef, ParentIsA<SpaceshipRamp>, ZIndex {
/// {@macro spaceship_ramp_arrow_sprite_component} /// {@macro spaceship_ramp_arrow_sprite_component}
_SpaceshipRampArrowSpriteComponent() SpaceshipRampArrowSpriteComponent({
: super( required int current,
}) : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-3.9, -56.5), position: Vector2(-3.9, -56.5),
current: current,
) { ) {
zIndex = ZIndexes.spaceshipRampArrow; zIndex = ZIndexes.spaceshipRampArrow;
} }
/// Changes arrow image to the next [Sprite].
void progress() => current = current?.next;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprites = <SpaceshipRampArrowSpriteState, Sprite>{}; parent.bloc.stream.listen((state) {
current = state.hits % SpaceshipRampArrowSpriteState.values.length;
});
final sprites = <int, Sprite>{};
this.sprites = sprites; this.sprites = sprites;
for (final spriteState in SpaceshipRampArrowSpriteState.values) { for (final spriteState in SpaceshipRampArrowSpriteState.values) {
sprites[spriteState] = Sprite( sprites[spriteState.index] = Sprite(
gameRef.images.fromCache(spriteState.path), gameRef.images.fromCache(spriteState.path),
); );
} }
current = SpaceshipRampArrowSpriteState.inactive; current = 0;
size = sprites[current]!.originalSize / 10; size = sprites[current]!.originalSize / 10;
} }
} }
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState {
String get path {
switch (this) {
case SpaceshipRampArrowSpriteState.inactive:
return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
return Assets.images.android.ramp.arrow.active5.keyName;
}
}
}
class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent class _SpaceshipRampBoardOpeningSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex { with HasGameRef, ZIndex {
_SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) { _SpaceshipRampBoardOpeningSpriteComponent() : super(anchor: Anchor.center) {
@ -373,3 +406,47 @@ class _SpaceshipRampOpening extends LayerSensor {
); );
} }
} }
/// {@template ramp_scoring_sensor}
/// Small sensor body used to detect when a ball has entered the
/// [SpaceshipRamp].
/// {@endtemplate}
class RampScoringSensor extends BodyComponent
with ParentIsA<SpaceshipRamp>, InitialPosition, Layered {
/// {@macro ramp_scoring_sensor}
RampScoringSensor({
Iterable<Component>? children,
}) : super(
children: children,
renderBody: false,
) {
layer = Layer.spaceshipEntranceRamp;
}
/// Creates a [RampScoringSensor] without any children.
///
@visibleForTesting
RampScoringSensor.test();
@override
Body createBody() {
final shape = PolygonShape()
..setAsBox(
2.6,
.5,
initialPosition,
-5 * math.pi / 180,
);
final fixtureDef = FixtureDef(
shape,
isSensor: true,
);
final bodyDef = BodyDef(
position: initialPosition,
userData: this,
);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -32,18 +32,18 @@ class _ComputerBase extends BodyComponent with InitialPosition, ZIndex {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final leftEdge = EdgeShape() final leftEdge = EdgeShape()
..set( ..set(
Vector2(-14.9, -46), Vector2(-15.1, -45.9),
Vector2(-15.3, -49.6), Vector2(-15.5, -49.5),
); );
final topEdge = EdgeShape() final topEdge = EdgeShape()
..set( ..set(
Vector2(-15.3, -49.6), leftEdge.vertex2,
Vector2(-10.7, -50.6), Vector2(-10.9, -50.5),
); );
final rightEdge = EdgeShape() final rightEdge = EdgeShape()
..set( ..set(
Vector2(-10.7, -50.6), topEdge.vertex2,
Vector2(-9, -47.2), Vector2(-9.2, -47.1),
); );
return [ return [
@ -67,7 +67,7 @@ class _ComputerBaseSpriteComponent extends SpriteComponent with HasGameRef {
_ComputerBaseSpriteComponent() _ComputerBaseSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-12.1, -48.15), position: Vector2(-12.24, -48.15),
); );
@override @override
@ -89,7 +89,7 @@ class _ComputerTopSpriteComponent extends SpriteComponent
_ComputerTopSpriteComponent() _ComputerTopSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-12.52, -49.37), position: Vector2(-12.66, -49.37),
) { ) {
zIndex = ZIndexes.computerTop; zIndex = ZIndexes.computerTop;
} }
@ -113,9 +113,9 @@ class _ComputerGlowSpriteComponent extends SpriteComponent
_ComputerGlowSpriteComponent() _ComputerGlowSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(7.4, 10), position: Vector2(4.2, 11),
) { ) {
zIndex = ZIndexes.computerGlow; zIndex = ZIndexes.computerGlow + 4;
} }
@override @override

@ -89,6 +89,7 @@ flutter:
- assets/images/score/ - assets/images/score/
- assets/images/backbox/ - assets/images/backbox/
- assets/images/flapper/ - assets/images/flapper/
- assets/images/skill_shot/
flutter_gen: flutter_gen:
line_length: 80 line_length: 80

@ -24,6 +24,14 @@ abstract class AssetsGame extends Forge2DGame {
} }
abstract class LineGame extends AssetsGame with PanDetector { abstract class LineGame extends AssetsGame with PanDetector {
LineGame({
List<String>? imagesFileNames,
}) : super(
imagesFileNames: [
if (imagesFileNames != null) ...imagesFileNames,
],
);
Vector2? _lineEnd; Vector2? _lineEnd;
@override @override

@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AndroidBumperAGame extends BallGame { class AndroidBumperAGame extends BallGame {
AndroidBumperAGame() AndroidBumperAGame()
: super( : super(
color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.android.bumper.a.lit.keyName, Assets.images.android.bumper.a.lit.keyName,
Assets.images.android.bumper.a.dimmed.keyName, Assets.images.android.bumper.a.dimmed.keyName,

@ -7,7 +7,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart';
class AndroidBumperBGame extends BallGame { class AndroidBumperBGame extends BallGame {
AndroidBumperBGame() AndroidBumperBGame()
: super( : super(
color: const Color(0xFF0000FF),
imagesFileNames: [ imagesFileNames: [
Assets.images.android.bumper.b.lit.keyName, Assets.images.android.bumper.b.lit.keyName,
Assets.images.android.bumper.b.dimmed.keyName, Assets.images.android.bumper.b.dimmed.keyName,

@ -1,14 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SpaceshipRailGame extends BallGame { class SpaceshipRailGame extends BallGame {
SpaceshipRailGame() SpaceshipRailGame()
: super( : super(
color: Colors.blue,
ballPriority: ZIndexes.ballOnSpaceshipRail, ballPriority: ZIndexes.ballOnSpaceshipRail,
ballLayer: Layer.spaceshipExitRail, ballLayer: Layer.spaceshipExitRail,
imagesFileNames: [ imagesFileNames: [

@ -9,7 +9,6 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart';
class SpaceshipRampGame extends BallGame with KeyboardEvents { class SpaceshipRampGame extends BallGame with KeyboardEvents {
SpaceshipRampGame() SpaceshipRampGame()
: super( : super(
color: Colors.blue,
ballPriority: ZIndexes.ballOnSpaceshipRamp, ballPriority: ZIndexes.ballOnSpaceshipRamp,
ballLayer: Layer.spaceshipEntranceRamp, ballLayer: Layer.spaceshipEntranceRamp,
imagesFileNames: [ imagesFileNames: [
@ -54,7 +53,7 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
) { ) {
if (event is RawKeyDownEvent && if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) { event.logicalKey == LogicalKeyboardKey.space) {
_spaceshipRamp.progress(); _spaceshipRamp.bloc.onAscendingBallEntered();
return KeyEventResult.handled; return KeyEventResult.handled;
} }

@ -1,9 +1,20 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
class BallBoosterGame extends LineGame { class BallBoosterGame extends LineGame {
BallBoosterGame()
: super(
imagesFileNames: [
theme.Assets.images.android.ball.keyName,
theme.Assets.images.dash.ball.keyName,
theme.Assets.images.dino.ball.keyName,
theme.Assets.images.sparky.ball.keyName,
Assets.images.ball.flameEffect.keyName,
],
);
static const description = ''' static const description = '''
Shows how a Ball with a boost works. Shows how a Ball with a boost works.
@ -12,7 +23,7 @@ class BallBoosterGame extends LineGame {
@override @override
void onLine(Vector2 line) { void onLine(Vector2 line) {
final ball = Ball(baseColor: Colors.transparent); final ball = Ball();
final impulse = line * -1 * 20; final impulse = line * -1 * 20;
ball.add(BallTurboChargingBehavior(impulse: impulse)); ball.add(BallTurboChargingBehavior(impulse: impulse));

@ -1,17 +1,20 @@
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
class BallGame extends AssetsGame with TapDetector, Traceable { class BallGame extends AssetsGame with TapDetector, Traceable {
BallGame({ BallGame({
this.color = Colors.blue,
this.ballPriority = 0, this.ballPriority = 0,
this.ballLayer = Layer.all, this.ballLayer = Layer.all,
this.character,
List<String>? imagesFileNames, List<String>? imagesFileNames,
}) : super( }) : super(
imagesFileNames: [ imagesFileNames: [
Assets.images.ball.ball.keyName, theme.Assets.images.android.ball.keyName,
theme.Assets.images.dash.ball.keyName,
theme.Assets.images.dino.ball.keyName,
theme.Assets.images.sparky.ball.keyName,
if (imagesFileNames != null) ...imagesFileNames, if (imagesFileNames != null) ...imagesFileNames,
], ],
); );
@ -22,14 +25,23 @@ class BallGame extends AssetsGame with TapDetector, Traceable {
- Tap anywhere on the screen to spawn a ball into the game. - Tap anywhere on the screen to spawn a ball into the game.
'''; ''';
final Color color; static final characterBallPaths = <String, String>{
'Dash': theme.Assets.images.dash.ball.keyName,
'Sparky': theme.Assets.images.sparky.ball.keyName,
'Android': theme.Assets.images.android.ball.keyName,
'Dino': theme.Assets.images.dino.ball.keyName,
};
final int ballPriority; final int ballPriority;
final Layer ballLayer; final Layer ballLayer;
final String? character;
@override @override
void onTapUp(TapUpInfo info) { void onTapUp(TapUpInfo info) {
add( add(
Ball(baseColor: color) Ball(
assetPath: characterBallPaths[character],
)
..initialPosition = info.eventPosition.game ..initialPosition = info.eventPosition.game
..layer = ballLayer ..layer = ballLayer
..priority = ballPriority, ..priority = ballPriority,

@ -1,5 +1,4 @@
import 'package:dashbook/dashbook.dart'; import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart';
import 'package:sandbox/common/common.dart'; import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/ball_booster_game.dart'; import 'package:sandbox/stories/ball/ball_booster_game.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
@ -7,10 +6,14 @@ import 'package:sandbox/stories/ball/basic_ball_game.dart';
void addBallStories(Dashbook dashbook) { void addBallStories(Dashbook dashbook) {
dashbook.storiesOf('Ball') dashbook.storiesOf('Ball')
..addGame( ..addGame(
title: 'Colored', title: 'Themed',
description: BallGame.description, description: BallGame.description,
gameBuilder: (context) => BallGame( gameBuilder: (context) => BallGame(
color: context.colorProperty('color', Colors.blue), character: context.listProperty(
'Character',
BallGame.characterBallPaths.keys.first,
BallGame.characterBallPaths.keys.toList(),
),
), ),
) )
..addGame( ..addGame(

@ -1,14 +1,10 @@
import 'dart:ui';
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:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class GoogleLetterGame extends BallGame { class GoogleLetterGame extends BallGame {
GoogleLetterGame() GoogleLetterGame()
: super( : super(
color: const Color(0xFF009900),
imagesFileNames: [ imagesFileNames: [
Assets.images.googleWord.letter1.lit.keyName, Assets.images.googleWord.letter1.lit.keyName,
Assets.images.googleWord.letter1.dimmed.keyName, Assets.images.googleWord.letter1.dimmed.keyName,

@ -1,14 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class LaunchRampGame extends BallGame { class LaunchRampGame extends BallGame {
LaunchRampGame() LaunchRampGame()
: super( : super(
color: Colors.blue,
ballPriority: ZIndexes.ballOnLaunchRamp, ballPriority: ZIndexes.ballOnLaunchRamp,
ballLayer: Layer.launcher, ballLayer: Layer.launcher,
); );

@ -6,8 +6,6 @@ import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/basic_ball_game.dart'; import 'package:sandbox/stories/ball/basic_ball_game.dart';
class PlungerGame extends BallGame with KeyboardEvents, Traceable { class PlungerGame extends BallGame with KeyboardEvents, Traceable {
PlungerGame() : super(color: const Color(0xFFFF0000));
static const description = ''' static const description = '''
Shows how Plunger is rendered. Shows how Plunger is rendered.

@ -2,31 +2,36 @@
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/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../helpers/helpers.dart'; import '../../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final assets = [
theme.Assets.images.android.ball.keyName,
theme.Assets.images.dash.ball.keyName,
theme.Assets.images.dino.ball.keyName,
theme.Assets.images.sparky.ball.keyName,
];
group('Ball', () { final flameTester = FlameTester(() => TestGame(assets));
const baseColor = Color(0xFFFFFFFF);
group('Ball', () {
test( test(
'can be instantiated', 'can be instantiated',
() { () {
expect(Ball(baseColor: baseColor), isA<Ball>()); expect(Ball(), isA<Ball>());
expect(Ball.test(baseColor: baseColor), isA<Ball>()); expect(Ball.test(), isA<Ball>());
}, },
); );
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ready(); await game.ready();
await game.ensureAdd(ball); await game.ensureAdd(ball);
@ -36,7 +41,7 @@ void main() {
group('adds', () { group('adds', () {
flameTester.test('a BallScalingBehavior', (game) async { flameTester.test('a BallScalingBehavior', (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
expect( expect(
ball.descendants().whereType<BallScalingBehavior>().length, ball.descendants().whereType<BallScalingBehavior>().length,
@ -45,7 +50,7 @@ void main() {
}); });
flameTester.test('a BallGravitatingBehavior', (game) async { flameTester.test('a BallGravitatingBehavior', (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
expect( expect(
ball.descendants().whereType<BallGravitatingBehavior>().length, ball.descendants().whereType<BallGravitatingBehavior>().length,
@ -58,7 +63,7 @@ void main() {
flameTester.test( flameTester.test(
'is dynamic', 'is dynamic',
(game) async { (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
expect(ball.body.bodyType, equals(BodyType.dynamic)); expect(ball.body.bodyType, equals(BodyType.dynamic));
@ -67,7 +72,7 @@ void main() {
group('can be moved', () { group('can be moved', () {
flameTester.test('by its weight', (game) async { flameTester.test('by its weight', (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
game.update(1); game.update(1);
@ -75,7 +80,7 @@ void main() {
}); });
flameTester.test('by applying velocity', (game) async { flameTester.test('by applying velocity', (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
ball.body.gravityScale = Vector2.zero(); ball.body.gravityScale = Vector2.zero();
@ -90,7 +95,7 @@ void main() {
flameTester.test( flameTester.test(
'exists', 'exists',
(game) async { (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
expect(ball.body.fixtures[0], isA<Fixture>()); expect(ball.body.fixtures[0], isA<Fixture>());
@ -100,7 +105,7 @@ void main() {
flameTester.test( flameTester.test(
'is dense', 'is dense',
(game) async { (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0]; final fixture = ball.body.fixtures[0];
@ -111,7 +116,7 @@ void main() {
flameTester.test( flameTester.test(
'shape is circular', 'shape is circular',
(game) async { (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0]; final fixture = ball.body.fixtures[0];
@ -123,7 +128,7 @@ void main() {
flameTester.test( flameTester.test(
'has Layer.all as default filter maskBits', 'has Layer.all as default filter maskBits',
(game) async { (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ready(); await game.ready();
await game.ensureAdd(ball); await game.ensureAdd(ball);
await game.ready(); await game.ready();
@ -137,7 +142,7 @@ void main() {
group('stop', () { group('stop', () {
group("can't be moved", () { group("can't be moved", () {
flameTester.test('by its weight', (game) async { flameTester.test('by its weight', (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
ball.stop(); ball.stop();
@ -152,7 +157,7 @@ void main() {
flameTester.test( flameTester.test(
'by its weight when previously stopped', 'by its weight when previously stopped',
(game) async { (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
ball.stop(); ball.stop();
ball.resume(); ball.resume();
@ -165,7 +170,7 @@ void main() {
flameTester.test( flameTester.test(
'by applying velocity when previously stopped', 'by applying velocity when previously stopped',
(game) async { (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
ball.stop(); ball.stop();
ball.resume(); ball.resume();

@ -1,21 +1,19 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart'; import 'package:flame/components.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';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final asset = Assets.images.ball.ball.keyName; final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset])); final flameTester = FlameTester(() => TestGame([asset]));
group('BallGravitatingBehavior', () { group('BallGravitatingBehavior', () {
const baseColor = Color(0xFFFFFFFF);
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
BallGravitatingBehavior(), BallGravitatingBehavior(),
@ -24,7 +22,7 @@ void main() {
}); });
flameTester.test('can be loaded', (game) async { flameTester.test('can be loaded', (game) async {
final ball = Ball.test(baseColor: baseColor); final ball = Ball.test();
final behavior = BallGravitatingBehavior(); final behavior = BallGravitatingBehavior();
await ball.add(behavior); await ball.add(behavior);
await game.ensureAdd(ball); await game.ensureAdd(ball);
@ -37,12 +35,10 @@ void main() {
flameTester.test( flameTester.test(
"overrides the body's horizontal gravity symmetrically", "overrides the body's horizontal gravity symmetrically",
(game) async { (game) async {
final ball1 = Ball.test(baseColor: baseColor) final ball1 = Ball.test()..initialPosition = Vector2(10, 0);
..initialPosition = Vector2(10, 0);
await ball1.add(BallGravitatingBehavior()); await ball1.add(BallGravitatingBehavior());
final ball2 = Ball.test(baseColor: baseColor) final ball2 = Ball.test()..initialPosition = Vector2(-10, 0);
..initialPosition = Vector2(-10, 0);
await ball2.add(BallGravitatingBehavior()); await ball2.add(BallGravitatingBehavior());
await game.ensureAddAll([ball1, ball2]); await game.ensureAddAll([ball1, ball2]);

@ -1,21 +1,19 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart'; import 'package:flame/components.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';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final asset = Assets.images.ball.ball.keyName; final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame([asset])); final flameTester = FlameTester(() => TestGame([asset]));
group('BallScalingBehavior', () { group('BallScalingBehavior', () {
const baseColor = Color(0xFFFFFFFF);
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
BallScalingBehavior(), BallScalingBehavior(),
@ -24,7 +22,7 @@ void main() {
}); });
flameTester.test('can be loaded', (game) async { flameTester.test('can be loaded', (game) async {
final ball = Ball.test(baseColor: baseColor); final ball = Ball.test();
final behavior = BallScalingBehavior(); final behavior = BallScalingBehavior();
await ball.add(behavior); await ball.add(behavior);
await game.ensureAdd(ball); await game.ensureAdd(ball);
@ -35,12 +33,10 @@ void main() {
}); });
flameTester.test('scales the shape radius', (game) async { flameTester.test('scales the shape radius', (game) async {
final ball1 = Ball.test(baseColor: baseColor) final ball1 = Ball.test()..initialPosition = Vector2(0, 10);
..initialPosition = Vector2(0, 10);
await ball1.add(BallScalingBehavior()); await ball1.add(BallScalingBehavior());
final ball2 = Ball.test(baseColor: baseColor) final ball2 = Ball.test()..initialPosition = Vector2(0, -10);
..initialPosition = Vector2(0, -10);
await ball2.add(BallScalingBehavior()); await ball2.add(BallScalingBehavior());
await game.ensureAddAll([ball1, ball2]); await game.ensureAddAll([ball1, ball2]);
@ -57,12 +53,10 @@ void main() {
flameTester.test( flameTester.test(
'scales the sprite', 'scales the sprite',
(game) async { (game) async {
final ball1 = Ball.test(baseColor: baseColor) final ball1 = Ball.test()..initialPosition = Vector2(0, 10);
..initialPosition = Vector2(0, 10);
await ball1.add(BallScalingBehavior()); await ball1.add(BallScalingBehavior());
final ball2 = Ball.test(baseColor: baseColor) final ball2 = Ball.test()..initialPosition = Vector2(0, -10);
..initialPosition = Vector2(0, -10);
await ball2.add(BallScalingBehavior()); await ball2.add(BallScalingBehavior());
await game.ensureAddAll([ball1, ball2]); await game.ensureAddAll([ball1, ball2]);

@ -1,12 +1,10 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
@ -16,9 +14,8 @@ void main() {
group( group(
'BallTurboChargingBehavior', 'BallTurboChargingBehavior',
() { () {
final assets = [Assets.images.ball.ball.keyName]; final asset = theme.Assets.images.dash.ball.keyName;
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame([asset]));
const baseColor = Color(0xFFFFFFFF);
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
@ -28,7 +25,7 @@ void main() {
}); });
flameTester.test('can be loaded', (game) async { flameTester.test('can be loaded', (game) async {
final ball = Ball.test(baseColor: baseColor); final ball = Ball.test();
final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); final behavior = BallTurboChargingBehavior(impulse: Vector2.zero());
await ball.add(behavior); await ball.add(behavior);
await game.ensureAdd(ball); await game.ensureAdd(ball);
@ -41,7 +38,7 @@ void main() {
flameTester.test( flameTester.test(
'impulses the ball velocity when loaded', 'impulses the ball velocity when loaded',
(game) async { (game) async {
final ball = Ball.test(baseColor: baseColor); final ball = Ball.test();
await game.ensureAdd(ball); await game.ensureAdd(ball);
final impulse = Vector2.all(1); final impulse = Vector2.all(1);
final behavior = BallTurboChargingBehavior(impulse: impulse); final behavior = BallTurboChargingBehavior(impulse: impulse);
@ -59,7 +56,7 @@ void main() {
); );
flameTester.test('adds sprite', (game) async { flameTester.test('adds sprite', (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
await ball.ensureAdd( await ball.ensureAdd(
@ -73,7 +70,7 @@ void main() {
}); });
flameTester.test('removes sprite after it finishes', (game) async { flameTester.test('removes sprite after it finishes', (game) async {
final ball = Ball(baseColor: baseColor); final ball = Ball();
await game.ensureAdd(ball); await game.ensureAdd(ball);
final behavior = BallTurboChargingBehavior(impulse: Vector2.zero()); final behavior = BallTurboChargingBehavior(impulse: Vector2.zero());

@ -4,11 +4,11 @@ import 'package:bloc_test/bloc_test.dart';
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:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart'; import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
@ -20,7 +20,10 @@ class _MockFixture extends Mock implements Fixture {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final assets = [
theme.Assets.images.dash.ball.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group( group(
'ChromeDinoChompingBehavior', 'ChromeDinoChompingBehavior',
@ -35,7 +38,7 @@ void main() {
flameTester.test( flameTester.test(
'beginContact sets ball sprite to be invisible and calls onChomp', 'beginContact sets ball sprite to be invisible and calls onChomp',
(game) async { (game) async {
final ball = Ball(baseColor: Colors.red); final ball = Ball();
final behavior = ChromeDinoChompingBehavior(); final behavior = ChromeDinoChompingBehavior();
final bloc = _MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
whenListen( whenListen(

@ -5,11 +5,11 @@ import 'dart:async';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart'; import 'package:pinball_components/src/components/chrome_dino/behaviors/behaviors.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../../../helpers/helpers.dart'; import '../../../../helpers/helpers.dart';
@ -17,7 +17,10 @@ class _MockChromeDinoCubit extends Mock implements ChromeDinoCubit {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new); final assets = [
theme.Assets.images.dash.ball.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
group( group(
'ChromeDinoSpittingBehavior', 'ChromeDinoSpittingBehavior',
@ -33,7 +36,7 @@ void main() {
flameTester.test( flameTester.test(
'sets ball sprite to visible and sets a linear velocity', 'sets ball sprite to visible and sets a linear velocity',
(game) async { (game) async {
final ball = Ball(baseColor: Colors.red); final ball = Ball();
final behavior = ChromeDinoSpittingBehavior(); final behavior = ChromeDinoSpittingBehavior();
final bloc = _MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
final streamController = StreamController<ChromeDinoState>(); final streamController = StreamController<ChromeDinoState>();
@ -71,7 +74,7 @@ void main() {
flameTester.test( flameTester.test(
'calls onSpit', 'calls onSpit',
(game) async { (game) async {
final ball = Ball(baseColor: Colors.red); final ball = Ball();
final behavior = ChromeDinoSpittingBehavior(); final behavior = ChromeDinoSpittingBehavior();
final bloc = _MockChromeDinoCubit(); final bloc = _MockChromeDinoCubit();
final streamController = StreamController<ChromeDinoState>(); final streamController = StreamController<ChromeDinoState>();

@ -36,7 +36,7 @@ void main() {
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(), initialState: const ChromeDinoState.initial(),
); );
final chromeDino = ChromeDino.test(bloc: bloc); final chromeDino = ChromeDino.test(bloc: bloc);
@ -58,7 +58,7 @@ void main() {
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(), initialState: const ChromeDinoState.initial(),
); );
final chromeDino = ChromeDino.test(bloc: bloc); final chromeDino = ChromeDino.test(bloc: bloc);
@ -91,7 +91,7 @@ void main() {
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
initialState: initialState:
const ChromeDinoState.inital().copyWith(isMouthOpen: true), const ChromeDinoState.initial().copyWith(isMouthOpen: true),
); );
final chromeDino = ChromeDino.test(bloc: bloc); final chromeDino = ChromeDino.test(bloc: bloc);
@ -120,7 +120,7 @@ void main() {
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
initialState: initialState:
const ChromeDinoState.inital().copyWith(isMouthOpen: false), const ChromeDinoState.initial().copyWith(isMouthOpen: false),
); );
final chromeDino = ChromeDino.test(bloc: bloc); final chromeDino = ChromeDino.test(bloc: bloc);
@ -148,7 +148,7 @@ void main() {
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
initialState: initialState:
const ChromeDinoState.inital().copyWith(isMouthOpen: false), const ChromeDinoState.initial().copyWith(isMouthOpen: false),
); );
final chromeDino = ChromeDino.test(bloc: bloc); final chromeDino = ChromeDino.test(bloc: bloc);

@ -79,7 +79,7 @@ void main() {
whenListen( whenListen(
bloc, bloc,
const Stream<ChromeDinoState>.empty(), const Stream<ChromeDinoState>.empty(),
initialState: const ChromeDinoState.inital(), initialState: const ChromeDinoState.initial(),
); );
when(bloc.close).thenAnswer((_) async {}); when(bloc.close).thenAnswer((_) async {});
final chromeDino = ChromeDino.test(bloc: bloc); final chromeDino = ChromeDino.test(bloc: bloc);

@ -1,5 +1,4 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -7,7 +6,7 @@ void main() {
group( group(
'ChromeDinoCubit', 'ChromeDinoCubit',
() { () {
final ball = Ball(baseColor: Colors.red); final ball = Ball();
blocTest<ChromeDinoCubit, ChromeDinoState>( blocTest<ChromeDinoCubit, ChromeDinoState>(
'onOpenMouth emits true', 'onOpenMouth emits true',
@ -58,7 +57,7 @@ void main() {
blocTest<ChromeDinoCubit, ChromeDinoState>( blocTest<ChromeDinoCubit, ChromeDinoState>(
'onChomp emits nothing when the ball is already in the mouth', 'onChomp emits nothing when the ball is already in the mouth',
build: ChromeDinoCubit.new, build: ChromeDinoCubit.new,
seed: () => const ChromeDinoState.inital().copyWith(ball: ball), seed: () => const ChromeDinoState.initial().copyWith(ball: ball),
act: (bloc) => bloc.onChomp(ball), act: (bloc) => bloc.onChomp(ball),
expect: () => <ChromeDinoState>[], expect: () => <ChromeDinoState>[],
); );

@ -1,6 +1,5 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
@ -37,7 +36,7 @@ void main() {
status: ChromeDinoStatus.idle, status: ChromeDinoStatus.idle,
isMouthOpen: false, isMouthOpen: false,
); );
expect(ChromeDinoState.inital(), equals(initialState)); expect(ChromeDinoState.initial(), equals(initialState));
}); });
}); });
@ -61,7 +60,7 @@ void main() {
'copies correctly ' 'copies correctly '
'when all arguments specified', 'when all arguments specified',
() { () {
final ball = Ball(baseColor: Colors.red); final ball = Ball();
const chromeDinoState = ChromeDinoState( const chromeDinoState = ChromeDinoState(
status: ChromeDinoStatus.chomping, status: ChromeDinoStatus.chomping,
isMouthOpen: true, isMouthOpen: true,

@ -2,9 +2,9 @@
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/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_theme/pinball_theme.dart' as theme;
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
@ -13,6 +13,7 @@ void main() {
final assets = [ final assets = [
Assets.images.flipper.left.keyName, Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName, Assets.images.flipper.right.keyName,
theme.Assets.images.dash.ball.keyName,
]; ];
final flameTester = FlameTester(() => TestGame(assets)); final flameTester = FlameTester(() => TestGame(assets));
@ -89,7 +90,7 @@ void main() {
'has greater mass than Ball', 'has greater mass than Ball',
(game) async { (game) async {
final flipper = Flipper(side: BoardSide.left); final flipper = Flipper(side: BoardSide.left);
final ball = Ball(baseColor: Colors.white); final ball = Ball();
await game.ready(); await game.ready();
await game.ensureAddAll([flipper, ball]); await game.ensureAddAll([flipper, ball]);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

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

Loading…
Cancel
Save