fix: merge with main fixed

pull/359/head
RuiAlonso 3 years ago
commit 8e41a7db93

@ -17,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) {
@ -33,7 +33,7 @@ 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( child: MultiBlocProvider(
providers: [ providers: [

@ -0,0 +1,35 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// Spawns a new [Ball] into the game when all balls are lost and still
/// [GameStatus.playing].
class BallSpawningBehavior extends Component
with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
@override
bool listenWhen(GameState? previousState, GameState newState) {
if (!newState.status.isPlaying) return false;
final startedGame = previousState?.status.isWaiting ?? true;
final lostRound =
(previousState?.rounds ?? newState.rounds + 1) > newState.rounds;
return startedGame || lostRound;
}
@override
void onNewState(GameState state) {
final plunger = gameRef.descendants().whereType<Plunger>().single;
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
final characterTheme = readProvider<CharacterTheme>();
final ball = ControlledBall.launch(characterTheme: characterTheme)
..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
canvas.add(ball);
}
}

@ -1,2 +1,4 @@
export 'bumper_noisy_behavior.dart'; export 'ball_spawning_behavior.dart';
export 'bumper_noise_behavior.dart';
export 'camera_focusing_behavior.dart';
export 'scoring_behavior.dart'; export 'scoring_behavior.dart';

@ -1,14 +1,13 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
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_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 BumperNoiseBehavior extends ContactBehavior {
@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(); readProvider<PinballPlayer>().play(PinballAudio.bumper);
} }
} }

@ -0,0 +1,83 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template focus_data}
/// Defines a [Camera] focus point.
/// {@endtemplate}
class FocusData {
/// {@template focus_data}
FocusData({
required this.zoom,
required this.position,
});
/// The amount of zoom.
final double zoom;
/// The position of the camera.
final Vector2 position;
}
/// Changes the game focus when the [GameBloc] status changes.
class CameraFocusingBehavior extends Component
with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
late final Map<String, FocusData> _foci;
@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:
_zoom(_foci['game']!);
break;
case GameStatus.gameOver:
_zoom(_foci['backbox']!);
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
_foci = {
'game': FocusData(
zoom: gameRef.size.y / 16,
position: Vector2(0, -7.8),
),
'waiting': FocusData(
zoom: gameRef.size.y / 18,
position: Vector2(0, -112),
),
'backbox': FocusData(
zoom: gameRef.size.y / 10,
position: Vector2(0, -111),
),
};
_snap(_foci['waiting']!);
}
void _snap(FocusData data) {
gameRef.camera
..speed = 100
..followVector2(data.position)
..zoom = data.zoom;
}
void _zoom(FocusData data) {
final zoom = CameraZoom(value: data.zoom);
zoom.completed.then((_) {
gameRef.camera.moveTo(data.position);
});
add(zoom);
}
}

@ -2,6 +2,7 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/effects.dart'; import 'package:flame/effects.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.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';
@ -12,7 +13,8 @@ import 'package:pinball_flame/pinball_flame.dart';
/// ///
/// The behavior removes itself after the duration. /// The behavior removes itself after the duration.
/// {@endtemplate} /// {@endtemplate}
class ScoringBehavior extends Component with HasGameRef<PinballGame> { class ScoringBehavior extends Component
with HasGameRef, FlameBlocReader<GameBloc, GameState> {
/// {@macto scoring_behavior} /// {@macto scoring_behavior}
ScoringBehavior({ ScoringBehavior({
required Points points, required Points points,
@ -39,7 +41,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)); await super.onLoad();
bloc.add(Scored(points: _points.value));
final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single; final canvas = gameRef.descendants().whereType<ZCanvasComponent>().single;
await canvas.add( await canvas.add(
ScoreComponent( ScoreComponent(
@ -54,8 +57,7 @@ class ScoringBehavior extends Component with HasGameRef<PinballGame> {
/// {@template scoring_contact_behavior} /// {@template scoring_contact_behavior}
/// Adds points to the score when the [Ball] contacts the [parent]. /// Adds points to the score when the [Ball] contacts the [parent].
/// {@endtemplate} /// {@endtemplate}
class ScoringContactBehavior extends ContactBehavior class ScoringContactBehavior extends ContactBehavior {
with HasGameRef<PinballGame> {
/// {@macro scoring_contact_behavior} /// {@macro scoring_contact_behavior}
ScoringContactBehavior({ ScoringContactBehavior({
required Points points, required Points points,

@ -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,18 @@ enum GameBonus {
androidSpaceship, androidSpaceship,
} }
enum GameStatus {
waiting,
playing,
gameOver,
}
extension GameStatusX on GameStatus {
bool get isWaiting => this == GameStatus.waiting;
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 +43,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 +79,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 +91,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 +104,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 +115,6 @@ class GameState extends Equatable {
multiplier, multiplier,
rounds, rounds,
bonusHistory, bonusHistory,
status,
]; ];
} }

@ -35,21 +35,21 @@ class AndroidAcres extends Component {
AndroidBumper.a( AndroidBumper.a(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-25, 1.3), )..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b( AndroidBumper.b(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-32.8, -9.2), )..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow( AndroidBumper.cow(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-20.5, -13.8), )..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(), AndroidSpaceshipBonusBehavior(),
], ],
); );

@ -1,11 +1,12 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.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';
/// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus. /// Adds a [GameBonus.androidSpaceship] when [AndroidSpaceship] has a bonus.
class AndroidSpaceshipBonusBehavior extends Component class AndroidSpaceshipBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<AndroidAcres> { with ParentIsA<AndroidAcres>, FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { void onMount() {
super.onMount(); super.onMount();
@ -18,9 +19,7 @@ class AndroidSpaceshipBonusBehavior extends Component
final listenWhen = state == AndroidSpaceshipState.withBonus; final listenWhen = state == AndroidSpaceshipState.withBonus;
if (!listenWhen) return; if (!listenWhen) return;
gameRef bloc.add(const BonusActivated(GameBonus.androidSpaceship));
.read<GameBloc>()
.add(const BonusActivated(GameBonus.androidSpaceship));
androidSpaceship.bloc.onBonusAwarded(); androidSpaceship.bloc.onBonusAwarded();
}); });
} }

@ -3,15 +3,13 @@ import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.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';
/// {@template ramp_bonus_behavior} /// {@template ramp_bonus_behavior}
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp]. /// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class RampBonusBehavior extends Component class RampBonusBehavior extends Component with ParentIsA<SpaceshipRamp> {
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> {
/// {@macro ramp_bonus_behavior} /// {@macro ramp_bonus_behavior}
RampBonusBehavior({ RampBonusBehavior({
required Points points, required Points points,

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
@ -11,7 +12,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp]. /// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
/// {@endtemplate} /// {@endtemplate}
class RampShotBehavior extends Component class RampShotBehavior extends Component
with ParentIsA<SpaceshipRamp>, HasGameRef<PinballGame> { with ParentIsA<SpaceshipRamp>, FlameBlocReader<GameBloc, GameState> {
/// {@macro ramp_shot_behavior} /// {@macro ramp_shot_behavior}
RampShotBehavior({ RampShotBehavior({
required Points points, required Points points,
@ -43,7 +44,7 @@ class RampShotBehavior extends Component
final achievedOneMillionPoints = state.hits % 10 == 0; final achievedOneMillionPoints = state.hits % 10 == 0;
if (!achievedOneMillionPoints) { if (!achievedOneMillionPoints) {
gameRef.read<GameBloc>().add(const MultiplierIncreased()); bloc.add(const MultiplierIncreased());
parent.add( parent.add(
ScoringBehavior( ScoringBehavior(

@ -1,38 +1,89 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/components/backbox/bloc/backbox_bloc.dart';
import 'package:pinball/game/components/backbox/displays/displays.dart'; import 'package:pinball/game/components/backbox/displays/displays.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' hide Assets;
/// {@template backbox} /// {@template backbox}
/// The [Backbox] of the pinball machine. /// The [Backbox] of the pinball machine.
/// {@endtemplate} /// {@endtemplate}
class Backbox extends PositionComponent with HasGameRef, ZIndex { class Backbox extends PositionComponent with ZIndex {
/// {@macro backbox} /// {@macro backbox}
Backbox() Backbox({
: super( required LeaderboardRepository leaderboardRepository,
position: Vector2(0, -87), }) : _bloc = BackboxBloc(leaderboardRepository: leaderboardRepository);
anchor: Anchor.bottomCenter,
children: [ /// {@macro backbox}
_BackboxSpriteComponent(), @visibleForTesting
], Backbox.test({
) { required BackboxBloc bloc,
}) : _bloc = bloc;
late final Component _display;
final BackboxBloc _bloc;
late StreamSubscription<BackboxState> _subscription;
@override
Future<void> onLoad() async {
position = Vector2(0, -87);
anchor = Anchor.bottomCenter;
zIndex = ZIndexes.backbox; zIndex = ZIndexes.backbox;
await add(_BackboxSpriteComponent());
await add(_display = Component());
_subscription = _bloc.stream.listen((state) {
_display.children.removeWhere((_) => true);
_build(state);
});
}
@override
void onRemove() {
super.onRemove();
_subscription.cancel();
}
void _build(BackboxState state) {
if (state is LoadingState) {
_display.add(LoadingDisplay());
} else if (state is InitialsFormState) {
_display.add(
InitialsInputDisplay(
score: state.score,
characterIconPath: state.character.leaderboardIcon.keyName,
onSubmit: (initials) {
_bloc.add(
PlayerInitialsSubmitted(
score: state.score,
initials: initials,
character: state.character,
),
);
},
),
);
} else if (state is InitialsSuccessState) {
_display.add(InitialsSubmissionSuccessDisplay());
} else if (state is InitialsFailureState) {
_display.add(InitialsSubmissionFailureDisplay());
}
} }
/// Puts [InitialsInputDisplay] on the [Backbox]. /// Puts [InitialsInputDisplay] on the [Backbox].
Future<void> initialsInput({ void requestInitials({
required int score, required int score,
required String characterIconPath, required CharacterTheme character,
InitialsOnSubmit? onSubmit, }) {
}) async { _bloc.add(
removeAll(children.where((child) => child is! _BackboxSpriteComponent)); PlayerInitialsRequested(
await add(
InitialsInputDisplay(
score: score, score: score,
characterIconPath: characterIconPath, character: character,
onSubmit: onSubmit,
), ),
); );
} }

@ -0,0 +1,56 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/leaderboard/models/leader_board_entry.dart';
import 'package:pinball_theme/pinball_theme.dart';
part 'backbox_event.dart';
part 'backbox_state.dart';
/// {@template backbox_bloc}
/// Bloc which manages the Backbox display.
/// {@endtemplate}
class BackboxBloc extends Bloc<BackboxEvent, BackboxState> {
/// {@macro backbox_bloc}
BackboxBloc({
required LeaderboardRepository leaderboardRepository,
}) : _leaderboardRepository = leaderboardRepository,
super(LoadingState()) {
on<PlayerInitialsRequested>(_onPlayerInitialsRequested);
on<PlayerInitialsSubmitted>(_onPlayerInitialsSubmitted);
}
final LeaderboardRepository _leaderboardRepository;
void _onPlayerInitialsRequested(
PlayerInitialsRequested event,
Emitter<BackboxState> emit,
) {
emit(
InitialsFormState(
score: event.score,
character: event.character,
),
);
}
Future<void> _onPlayerInitialsSubmitted(
PlayerInitialsSubmitted event,
Emitter<BackboxState> emit,
) async {
try {
emit(LoadingState());
await _leaderboardRepository.addLeaderboardEntry(
LeaderboardEntryData(
playerInitials: event.initials,
score: event.score,
character: event.character.toType,
),
);
emit(InitialsSuccessState());
} catch (error, stackTrace) {
addError(error, stackTrace);
emit(InitialsFailureState());
}
}
}

@ -0,0 +1,53 @@
part of 'backbox_bloc.dart';
/// {@template backbox_event}
/// Base class for backbox events.
/// {@endtemplate}
abstract class BackboxEvent extends Equatable {
/// {@macro backbox_event}
const BackboxEvent();
}
/// {@template player_initials_requested}
/// Event that triggers the user initials display.
/// {@endtemplate}
class PlayerInitialsRequested extends BackboxEvent {
/// {@macro player_initials_requested}
const PlayerInitialsRequested({
required this.score,
required this.character,
});
/// Player's score.
final int score;
/// Player's character.
final CharacterTheme character;
@override
List<Object?> get props => [score, character];
}
/// {@template player_initials_submitted}
/// Event that submits the user score and initials.
/// {@endtemplate}
class PlayerInitialsSubmitted extends BackboxEvent {
/// {@macro player_initials_submitted}
const PlayerInitialsSubmitted({
required this.score,
required this.initials,
required this.character,
});
/// Player's score.
final int score;
/// Player's initials.
final String initials;
/// Player's character.
final CharacterTheme character;
@override
List<Object?> get props => [score, initials, character];
}

@ -0,0 +1,59 @@
part of 'backbox_bloc.dart';
/// {@template backbox_state}
/// The base state for all [BackboxState].
/// {@endtemplate backbox_state}
abstract class BackboxState extends Equatable {
/// {@macro backbox_state}
const BackboxState();
}
/// Loading state for the backbox.
class LoadingState extends BackboxState {
@override
List<Object?> get props => [];
}
/// State when the leaderboard was successfully loaded.
class LeaderboardSuccessState extends BackboxState {
@override
List<Object?> get props => [];
}
/// State when the leaderboard failed to load.
class LeaderboardFailureState extends BackboxState {
@override
List<Object?> get props => [];
}
/// {@template initials_form_state}
/// State when the user is inputting their initials.
/// {@endtemplate}
class InitialsFormState extends BackboxState {
/// {@macro initials_form_state}
const InitialsFormState({
required this.score,
required this.character,
}) : super();
/// Player's score.
final int score;
/// Player's character.
final CharacterTheme character;
@override
List<Object?> get props => [score, character];
}
/// State when the leaderboard was successfully loaded.
class InitialsSuccessState extends BackboxState {
@override
List<Object?> get props => [];
}
/// State when the initials submission failed.
class InitialsFailureState extends BackboxState {
@override
List<Object?> get props => [];
}

@ -1,2 +1,5 @@
export 'initials_input_display.dart';
export 'info_display.dart'; export 'info_display.dart';
export 'initials_input_display.dart';
export 'initials_submission_failure_display.dart';
export 'initials_submission_success_display.dart';
export 'loading_display.dart';

@ -4,7 +4,7 @@ import 'dart:math';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.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_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
@ -59,7 +59,7 @@ class InitialsInputDisplay extends Component with HasGameRef {
await add( await add(
InitialsLetterPrompt( InitialsLetterPrompt(
position: Vector2( position: Vector2(
11.4 + (2.3 * i), 10.8 + (2.5 * i),
-20, -20,
), ),
hasFocus: i == 0, hasFocus: i == 0,
@ -103,8 +103,7 @@ class InitialsInputDisplay extends Component with HasGameRef {
} }
} }
class _ScoreLabelTextComponent extends TextComponent class _ScoreLabelTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_ScoreLabelTextComponent() _ScoreLabelTextComponent()
: super( : super(
anchor: Anchor.centerLeft, anchor: Anchor.centerLeft,
@ -119,7 +118,7 @@ class _ScoreLabelTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.score; text = readProvider<AppLocalizations>().score;
} }
} }
@ -133,12 +132,11 @@ class _ScoreTextComponent extends TextComponent {
); );
} }
class _NameLabelTextComponent extends TextComponent class _NameLabelTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_NameLabelTextComponent() _NameLabelTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(11.4, -24), position: Vector2(10.8, -24),
textRenderer: _bodyTextPaint.copyWith( textRenderer: _bodyTextPaint.copyWith(
(style) => style.copyWith( (style) => style.copyWith(
color: PinballColors.red, color: PinballColors.red,
@ -149,7 +147,7 @@ class _NameLabelTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.name; text = readProvider<AppLocalizations>().name;
} }
} }
@ -158,7 +156,7 @@ class _CharacterIconSpriteComponent extends SpriteComponent with HasGameRef {
: _characterIconPath = characterIconPath, : _characterIconPath = characterIconPath,
super( super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(8.4, -20), position: Vector2(7.6, -20),
); );
final String _characterIconPath; final String _characterIconPath;
@ -241,8 +239,9 @@ class InitialsLetterPrompt extends PositionComponent {
bool _cycle(bool up) { bool _cycle(bool up) {
if (_hasFocus) { if (_hasFocus) {
final newCharCode = var newCharCode = _charIndex + (up ? -1 : 1);
min(max(_charIndex + (up ? 1 : -1), 0), _alphabetLength); if (newCharCode < 0) newCharCode = _alphabetLength;
if (newCharCode > _alphabetLength) newCharCode = 0;
_input.text = String.fromCharCode(_alphabetCode + newCharCode); _input.text = String.fromCharCode(_alphabetCode + newCharCode);
_charIndex = newCharCode; _charIndex = newCharCode;
@ -299,8 +298,7 @@ class _InstructionsComponent extends PositionComponent with HasGameRef {
); );
} }
class _EnterInitialsTextComponent extends TextComponent class _EnterInitialsTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_EnterInitialsTextComponent() _EnterInitialsTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -311,11 +309,11 @@ class _EnterInitialsTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.enterInitials; text = readProvider<AppLocalizations>().enterInitials;
} }
} }
class _ArrowsTextComponent extends TextComponent with HasGameRef<PinballGame> { class _ArrowsTextComponent extends TextComponent {
_ArrowsTextComponent() _ArrowsTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -330,12 +328,11 @@ class _ArrowsTextComponent extends TextComponent with HasGameRef<PinballGame> {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.arrows; text = readProvider<AppLocalizations>().arrows;
} }
} }
class _AndPressTextComponent extends TextComponent class _AndPressTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_AndPressTextComponent() _AndPressTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -346,12 +343,11 @@ class _AndPressTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.andPress; text = readProvider<AppLocalizations>().andPress;
} }
} }
class _EnterReturnTextComponent extends TextComponent class _EnterReturnTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_EnterReturnTextComponent() _EnterReturnTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -366,12 +362,11 @@ class _EnterReturnTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.enterReturn; text = readProvider<AppLocalizations>().enterReturn;
} }
} }
class _ToSubmitTextComponent extends TextComponent class _ToSubmitTextComponent extends TextComponent {
with HasGameRef<PinballGame> {
_ToSubmitTextComponent() _ToSubmitTextComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
@ -382,6 +377,6 @@ class _ToSubmitTextComponent extends TextComponent
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
text = gameRef.l10n.toSubmit; text = readProvider<AppLocalizations>().toSubmit;
} }
} }

@ -0,0 +1,27 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_ui/pinball_ui.dart';
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 3,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template initials_submission_failure_display}
/// [Backbox] display for when a failure occurs during initials submission.
/// {@endtemplate}
class InitialsSubmissionFailureDisplay extends TextComponent {
@override
Future<void> onLoad() async {
await super.onLoad();
position = Vector2(0, -10);
anchor = Anchor.center;
text = 'Failure!';
textRenderer = _bodyTextPaint;
}
}

@ -0,0 +1,27 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_ui/pinball_ui.dart';
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 3,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template initials_submission_success_display}
/// [Backbox] display for initials successfully submitted.
/// {@endtemplate}
class InitialsSubmissionSuccessDisplay extends TextComponent {
@override
Future<void> onLoad() async {
await super.onLoad();
position = Vector2(0, -10);
anchor = Anchor.center;
text = 'Success!';
textRenderer = _bodyTextPaint;
}
}

@ -0,0 +1,49 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_ui/pinball_ui.dart';
final _bodyTextPaint = TextPaint(
style: const TextStyle(
fontSize: 3,
color: PinballColors.white,
fontFamily: PinballFonts.pixeloidSans,
),
);
/// {@template loading_display}
/// Display used to show the loading animation.
/// {@endtemplate}
class LoadingDisplay extends TextComponent {
/// {@template loading_display}
LoadingDisplay();
late final String _label;
@override
Future<void> onLoad() async {
await super.onLoad();
position = Vector2(0, -10);
anchor = Anchor.center;
text = _label = readProvider<AppLocalizations>().loading;
textRenderer = _bodyTextPaint;
await add(
TimerComponent(
period: 1,
repeat: true,
onTick: () {
final index = text.indexOf('.');
if (index != -1 && text.substring(index).length == 3) {
text = _label;
} else {
text = '$text.';
}
},
),
);
}
}

@ -39,14 +39,14 @@ class _BottomGroupSide extends Component {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
final direction = _side.direction; final direction = _side.direction;
final centerXAdjustment = _side.isLeft ? 0 : -6.66; final centerXAdjustment = _side.isLeft ? -0.45 : -6.8;
final flipper = ControlledFlipper( final flipper = ControlledFlipper(
side: _side, side: _side,
)..initialPosition = Vector2((11.8 * direction) + centerXAdjustment, 43.6); )..initialPosition = Vector2((11.6 * direction) + centerXAdjustment, 43.6);
final baseboard = Baseboard(side: _side) final baseboard = Baseboard(side: _side)
..initialPosition = Vector2( ..initialPosition = Vector2(
(25.58 * direction) + centerXAdjustment, (25.38 * direction) + centerXAdjustment,
28.71, 28.71,
); );
final kicker = Kicker( final kicker = Kicker(
@ -56,7 +56,7 @@ class _BottomGroupSide extends Component {
..applyTo(['bouncy_edge']), ..applyTo(['bouncy_edge']),
], ],
)..initialPosition = Vector2( )..initialPosition = Vector2(
(22.64 * direction) + centerXAdjustment, (22.44 * direction) + centerXAdjustment,
25.1, 25.1,
); );

@ -1,93 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Adds helpers methods to Flame's [Camera].
extension CameraX on Camera {
/// Instantly apply the point of focus to the [Camera].
void snapToFocus(FocusData data) {
followVector2(data.position);
zoom = data.zoom;
}
/// Returns a [CameraZoom] that can be added to a [FlameGame].
CameraZoom focusToCameraZoom(FocusData data) {
final zoom = CameraZoom(value: data.zoom);
zoom.completed.then((_) {
moveTo(data.position);
});
return zoom;
}
}
/// {@template focus_data}
/// Model class that defines a focus point of the camera.
/// {@endtemplate}
class FocusData {
/// {@template focus_data}
FocusData({
required this.zoom,
required this.position,
});
/// The amount of zoom.
final double zoom;
/// The position of the camera.
final Vector2 position;
}
/// {@template camera_controller}
/// A [Component] that controls its game camera focus.
/// {@endtemplate}
class CameraController extends ComponentController<FlameGame> {
/// {@macro camera_controller}
CameraController(FlameGame component) : super(component) {
final gameZoom = component.size.y / 16;
final waitingBackboxZoom = component.size.y / 18;
final gameOverBackboxZoom = component.size.y / 10;
gameFocus = FocusData(
zoom: gameZoom,
position: Vector2(0, -7.8),
);
waitingBackboxFocus = FocusData(
zoom: waitingBackboxZoom,
position: Vector2(0, -112),
);
gameOverBackboxFocus = FocusData(
zoom: gameOverBackboxZoom,
position: Vector2(0, -111),
);
// Game starts with the camera focused on the [Backbox].
component.camera
..speed = 100
..snapToFocus(waitingBackboxFocus);
}
/// Holds the data for the game focus point.
late final FocusData gameFocus;
/// Holds the data for the waiting backbox focus point.
late final FocusData waitingBackboxFocus;
/// Holds the data for the game over backbox focus point.
late final FocusData gameOverBackboxFocus;
/// Move the camera focus to the game board.
void focusOnGame() {
component.add(component.camera.focusToCameraZoom(gameFocus));
}
/// Move the camera focus to the waiting backbox.
void focusOnWaitingBackbox() {
component.add(component.camera.focusToCameraZoom(waitingBackboxFocus));
}
/// Move the camera focus to the game over backbox.
void focusOnGameOverBackbox() {
component.add(component.camera.focusToCameraZoom(gameOverBackboxFocus));
}
}

@ -1,14 +1,13 @@
export 'android_acres/android_acres.dart'; export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart'; export 'backbox/backbox.dart';
export 'bottom_group.dart'; export 'bottom_group.dart';
export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
export 'controlled_plunger.dart'; export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart'; export 'dino_desert/dino_desert.dart';
export 'drain.dart'; export 'drain/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,6 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.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';
@ -22,9 +23,7 @@ class ControlledBall extends Ball with Controls<BallController> {
zIndex = ZIndexes.ballOnLaunchRamp; zIndex = ZIndexes.ballOnLaunchRamp;
} }
/// {@template bonus_ball}
/// {@macro controlled_ball} /// {@macro controlled_ball}
/// {@endtemplate}
ControlledBall.bonus({ ControlledBall.bonus({
required CharacterTheme characterTheme, required CharacterTheme characterTheme,
}) : super(assetPath: characterTheme.ball.keyName) { }) : super(assetPath: characterTheme.ball.keyName) {
@ -43,20 +42,14 @@ class ControlledBall extends Ball with Controls<BallController> {
/// Controller attached to a [Ball] that handles its game related logic. /// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate} /// {@endtemplate}
class BallController extends ComponentController<Ball> class BallController extends ComponentController<Ball>
with HasGameRef<PinballGame> { with FlameBlocReader<GameBloc, GameState> {
/// {@macro ball_controller} /// {@macro ball_controller}
BallController(Ball ball) : super(ball); BallController(Ball ball) : super(ball);
/// Event triggered when the ball is lost.
// TODO(alestiago): Refactor using behaviors.
void lost() {
component.shouldRemove = true;
}
/// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge /// Stops the [Ball] inside of the [SparkyComputer] while the turbo charge
/// sequence runs, then boosts the ball out of the computer. /// sequence runs, then boosts the ball out of the computer.
Future<void> turboCharge() async { Future<void> turboCharge() async {
gameRef.read<GameBloc>().add(const SparkyTurboChargeActivated()); bloc.add(const SparkyTurboChargeActivated());
component.stop(); component.stop();
// TODO(alestiago): Refactor this hard coded duration once the following is // TODO(alestiago): Refactor this hard coded duration once the following is
@ -70,13 +63,4 @@ class BallController extends ComponentController<Ball>
BallTurboChargingBehavior(impulse: Vector2(40, 110)), BallTurboChargingBehavior(impulse: Vector2(40, 110)),
); );
} }
@override
void onRemove() {
super.onRemove();
final noBallsLeft = gameRef.descendants().whereType<Ball>().isEmpty;
if (noBallsLeft) {
gameRef.read<GameBloc>().add(const RoundLost());
}
}
} }

@ -21,7 +21,7 @@ class ControlledFlipper extends Flipper with Controls<FlipperController> {
/// A [ComponentController] that controls a [Flipper]s movement. /// A [ComponentController] that controls a [Flipper]s movement.
/// {@endtemplate} /// {@endtemplate}
class FlipperController extends ComponentController<Flipper> class FlipperController extends ComponentController<Flipper>
with KeyboardHandler, BlocComponent<GameBloc, GameState> { with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro flipper_controller} /// {@macro flipper_controller}
FlipperController(Flipper flipper) FlipperController(Flipper flipper)
: _keys = flipper.side.flipperKeys, : _keys = flipper.side.flipperKeys,
@ -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 (!bloc.state.status.isPlaying) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -2,6 +2,7 @@ import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -14,13 +15,36 @@ class ControlledPlunger extends Plunger with Controls<PlungerController> {
: super(compressionDistance: compressionDistance) { : super(compressionDistance: compressionDistance) {
controller = PlungerController(this); controller = PlungerController(this);
} }
@override
void release() {
super.release();
add(PlungerNoiseBehavior());
}
}
/// A behavior attached to the plunger when it launches the ball which plays the
/// related sound effects.
class PlungerNoiseBehavior extends Component {
@override
Future<void> onLoad() async {
await super.onLoad();
readProvider<PinballPlayer>().play(PinballAudio.launcher);
}
@override
void update(double dt) {
super.update(dt);
removeFromParent();
}
} }
/// {@template plunger_controller} /// {@template plunger_controller}
/// A [ComponentController] that controls a [Plunger]s movement. /// A [ComponentController] that controls a [Plunger]s movement.
/// {@endtemplate} /// {@endtemplate}
class PlungerController extends ComponentController<Plunger> class PlungerController extends ComponentController<Plunger>
with KeyboardHandler, BlocComponent<GameBloc, GameState> { with KeyboardHandler, FlameBlocReader<GameBloc, GameState> {
/// {@macro plunger_controller} /// {@macro plunger_controller}
PlungerController(Plunger plunger) : super(plunger); PlungerController(Plunger plunger) : super(plunger);
@ -38,7 +62,7 @@ class PlungerController extends ComponentController<Plunger>
RawKeyEvent event, RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed, Set<LogicalKeyboardKey> keysPressed,
) { ) {
if (state?.isGameOver ?? false) return true; if (bloc.state.status.isGameOver) return true;
if (!_keys.contains(event.logicalKey)) return true; if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {

@ -1,11 +1,12 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.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';
/// Adds a [GameBonus.dinoChomp] when a [Ball] is chomped by the [ChromeDino]. /// Adds a [GameBonus.dinoChomp] when a [Ball] is chomped by the [ChromeDino].
class ChromeDinoBonusBehavior extends Component class ChromeDinoBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<DinoDesert> { with ParentIsA<DinoDesert>, FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { void onMount() {
super.onMount(); super.onMount();
@ -18,7 +19,7 @@ class ChromeDinoBonusBehavior extends Component
final listenWhen = state.status == ChromeDinoStatus.chomping; final listenWhen = state.status == ChromeDinoStatus.chomping;
if (!listenWhen) return; if (!listenWhen) return;
gameRef.read<GameBloc>().add(const BonusActivated(GameBonus.dinoChomp)); bloc.add(const BonusActivated(GameBonus.dinoChomp));
}); });
} }
} }

@ -20,7 +20,7 @@ class DinoDesert extends Component {
ScoringContactBehavior(points: Points.twoHundredThousand) ScoringContactBehavior(points: Points.twoHundredThousand)
..applyTo(['inside_mouth']), ..applyTo(['inside_mouth']),
], ],
)..initialPosition = Vector2(12.6, -6.9), )..initialPosition = Vector2(12.2, -6.9),
_BarrierBehindDino(), _BarrierBehindDino(),
DinoWalls(), DinoWalls(),
Slingshots(), Slingshots(),

@ -1,34 +0,0 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template drain}
/// Area located at the bottom of the board to detect when a [Ball] is lost.
/// {@endtemplate}
// TODO(allisonryan0002): move to components package when possible.
class Drain extends BodyComponent with ContactCallbacks {
/// {@macro drain}
Drain() : super(renderBody: false);
@override
Body createBody() {
final shape = EdgeShape()
..set(
BoardDimensions.bounds.bottomLeft.toVector2(),
BoardDimensions.bounds.bottomRight.toVector2(),
);
final fixtureDef = FixtureDef(shape, isSensor: true);
final bodyDef = BodyDef(userData: this);
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
// TODO(allisonryan0002): move this to ball.dart when BallLost is removed.
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! ControlledBall) return;
other.controller.lost();
}
}

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

@ -0,0 +1,25 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Handles removing a [Ball] from the game.
class DrainingBehavior extends ContactBehavior<Drain> with HasGameRef {
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
other.removeFromParent();
final ballsLeft = gameRef.descendants().whereType<Ball>().length;
if (ballsLeft - 1 == 0) {
ancestors()
.whereType<FlameBlocProvider<GameBloc, GameState>>()
.first
.bloc
.add(const RoundLost());
}
}
}

@ -0,0 +1,36 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'package:pinball/game/components/drain/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template drain}
/// Area located at the bottom of the board.
///
/// Its [DrainingBehavior] handles removing a [Ball] from the game.
/// {@endtemplate}
class Drain extends BodyComponent with ContactCallbacks {
/// {@macro drain}
Drain()
: super(
renderBody: false,
children: [DrainingBehavior()],
);
/// Creates a [Drain] without any children.
///
/// This can be used for testing a [Drain]'s behaviors in isolation.
@visibleForTesting
Drain.test();
@override
Body createBody() {
final shape = EdgeShape()
..set(
BoardDimensions.bounds.bottomLeft.toVector2(),
BoardDimensions.bounds.bottomRight.toVector2(),
);
final fixtureDef = FixtureDef(shape, isSensor: true);
return world.createBody(BodyDef())..createFixture(fixtureDef);
}
}

@ -1,7 +1,9 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.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';
import 'package:pinball_theme/pinball_theme.dart';
/// Bonus obtained at the [FlutterForest]. /// Bonus obtained at the [FlutterForest].
/// ///
@ -9,7 +11,10 @@ import 'package:pinball_flame/pinball_flame.dart';
/// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest] /// progresses. When the [Signpost] fully progresses, the [GameBonus.dashNest]
/// is awarded, and the [DashNestBumper.main] releases a new [Ball]. /// is awarded, and the [DashNestBumper.main] releases a new [Ball].
class FlutterForestBonusBehavior extends Component class FlutterForestBonusBehavior extends Component
with ParentIsA<FlutterForest>, HasGameRef<PinballGame> { with
ParentIsA<FlutterForest>,
HasGameRef,
FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { void onMount() {
super.onMount(); super.onMount();
@ -35,12 +40,11 @@ class FlutterForestBonusBehavior extends Component
} }
if (signpost.bloc.isFullyProgressed()) { if (signpost.bloc.isFullyProgressed()) {
gameRef bloc.add(const BonusActivated(GameBonus.dashNest));
.read<GameBloc>()
.add(const BonusActivated(GameBonus.dashNest));
canvas.add( canvas.add(
ControlledBall.bonus(characterTheme: gameRef.characterTheme) ControlledBall.bonus(
..initialPosition = Vector2(29.5, -24.5), characterTheme: readProvider<CharacterTheme>(),
)..initialPosition = Vector2(29.2, -24.5),
); );
animatronic.playing = true; animatronic.playing = true;
signpost.bloc.onProgressed(); signpost.bloc.onProgressed();

@ -19,27 +19,27 @@ class FlutterForest extends Component with ZIndex {
Signpost( Signpost(
children: [ children: [
ScoringContactBehavior(points: Points.fiveThousand), ScoringContactBehavior(points: Points.fiveThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(8.35, -58.3), )..initialPosition = Vector2(7.95, -58.35),
DashNestBumper.main( DashNestBumper.main(
children: [ children: [
ScoringContactBehavior(points: Points.twoHundredThousand), ScoringContactBehavior(points: Points.twoHundredThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(18.55, -59.35), )..initialPosition = Vector2(18.55, -59.35),
DashNestBumper.a( DashNestBumper.a(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(8.95, -51.95), )..initialPosition = Vector2(8.95, -51.95),
DashNestBumper.b( DashNestBumper.b(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(22.3, -46.75), )..initialPosition = Vector2(21.8, -46.75),
DashAnimatronic()..position = Vector2(20, -66), DashAnimatronic()..position = Vector2(20, -66),
FlutterForestBonusBehavior(), FlutterForestBonusBehavior(),
], ],

@ -0,0 +1,34 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// Listens to the [GameBloc] and updates the game accordingly.
class GameBlocStatusListener extends Component
with FlameBlocListenable<GameBloc, GameState>, HasGameRef {
@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:
readProvider<PinballPlayer>().play(PinballAudio.backgroundMusic);
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
case GameStatus.gameOver:
readProvider<PinballPlayer>().play(PinballAudio.gameOverVoiceOver);
gameRef.descendants().whereType<Backbox>().first.requestInitials(
score: state.displayScore,
character: readProvider<CharacterTheme>(),
);
break;
}
}
}

@ -1,48 +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) {
print("START $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.infoScreen(
score: state?.displayScore ?? 0,
characterIconPath: component.characterTheme.leaderboardIcon.keyName,
);
component.firstChild<CameraController>()!.focusOnGameOverBackbox();
}
/// Puts the game in the playing state.
void start() {
_initialsInput();
/*
component.audio.backgroundMusic();
component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay);*/
}
}

@ -1,11 +1,13 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
/// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated. /// Adds a [GameBonus.googleWord] when all [GoogleLetter]s are activated.
class GoogleWordBonusBehavior extends Component class GoogleWordBonusBehavior extends Component
with HasGameRef<PinballGame>, ParentIsA<GoogleWord> { with ParentIsA<GoogleWord>, FlameBlocReader<GameBloc, GameState> {
@override @override
void onMount() { void onMount() {
super.onMount(); super.onMount();
@ -20,10 +22,8 @@ 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(); readProvider<PinballPlayer>().play(PinballAudio.google);
gameRef bloc.add(const BonusActivated(GameBonus.googleWord));
.read<GameBloc>()
.add(const BonusActivated(GameBonus.googleWord));
for (final letter in googleLetters) { for (final letter in googleLetters) {
letter.bloc.onReset(); letter.bloc.onReset();
} }

@ -14,8 +14,8 @@ class Launcher extends Component {
LaunchRamp(), LaunchRamp(),
Flapper(), Flapper(),
ControlledPlunger(compressionDistance: 9.2) ControlledPlunger(compressionDistance: 9.2)
..initialPosition = Vector2(41.2, 43.7), ..initialPosition = Vector2(41, 43.7),
RocketSpriteComponent()..position = Vector2(43, 62.3), RocketSpriteComponent()..position = Vector2(42.8, 62.3),
], ],
); );
} }

@ -6,10 +6,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Toggle each [Multiball] when there is a bonus ball. /// Toggle each [Multiball] when there is a bonus ball.
class MultiballsBehavior extends Component class MultiballsBehavior extends Component
with with ParentIsA<Multiballs>, FlameBlocListenable<GameBloc, GameState> {
HasGameRef<PinballGame>,
ParentIsA<Multiballs>,
BlocComponent<GameBloc, GameState> {
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
final hasChanged = previousState?.bonusHistory != newState.bonusHistory; final hasChanged = previousState?.bonusHistory != newState.bonusHistory;

@ -6,10 +6,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// Toggle each [Multiplier] when GameState.multiplier changes. /// Toggle each [Multiplier] when GameState.multiplier changes.
class MultipliersBehavior extends Component class MultipliersBehavior extends Component
with with ParentIsA<Multipliers>, FlameBlocListenable<GameBloc, GameState> {
HasGameRef<PinballGame>,
ParentIsA<Multipliers>,
BlocComponent<GameBloc, GameState> {
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.multiplier != newState.multiplier; return previousState?.multiplier != newState.multiplier;

@ -14,23 +14,23 @@ class Multipliers extends Component with ZIndex {
: super( : super(
children: [ children: [
Multiplier.x2( Multiplier.x2(
position: Vector2(-19.5, -2), position: Vector2(-19.6, -2),
angle: -15 * math.pi / 180, angle: -15 * math.pi / 180,
), ),
Multiplier.x3( Multiplier.x3(
position: Vector2(13, -9.4), position: Vector2(12.8, -9.4),
angle: 15 * math.pi / 180, angle: 15 * math.pi / 180,
), ),
Multiplier.x4( Multiplier.x4(
position: Vector2(0, -21.2), position: Vector2(-0.3, -21.2),
angle: 0, angle: 3 * math.pi / 180,
), ),
Multiplier.x5( Multiplier.x5(
position: Vector2(-8.5, -28), position: Vector2(-8.9, -28),
angle: -3 * math.pi / 180, angle: -3 * math.pi / 180,
), ),
Multiplier.x6( Multiplier.x6(
position: Vector2(10, -30.7), position: Vector2(9.8, -30.7),
angle: 8 * math.pi / 180, angle: 8 * math.pi / 180,
), ),
MultipliersBehavior(), MultipliersBehavior(),

@ -18,23 +18,23 @@ class SparkyScorch extends Component {
SparkyBumper.a( SparkyBumper.a(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-22.9, -41.65), )..initialPosition = Vector2(-22.9, -41.65),
SparkyBumper.b( SparkyBumper.b(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-21.25, -57.9), )..initialPosition = Vector2(-21.25, -57.9),
SparkyBumper.c( SparkyBumper.c(
children: [ children: [
ScoringContactBehavior(points: Points.twentyThousand), ScoringContactBehavior(points: Points.twentyThousand),
BumperNoisyBehavior(), BumperNoiseBehavior(),
], ],
)..initialPosition = Vector2(-3.3, -52.55), )..initialPosition = Vector2(-3.3, -52.55),
SparkyComputerSensor()..initialPosition = Vector2(-13, -49.9), SparkyComputerSensor()..initialPosition = Vector2(-13.2, -49.9),
SparkyAnimatronic()..position = Vector2(-13.8, -58.2), SparkyAnimatronic()..position = Vector2(-14, -58.2),
SparkyComputer(), SparkyComputer(),
], ],
); );

@ -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:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
@ -16,18 +17,21 @@ import 'package:pinball_flame/pinball_flame.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends PinballForge2DGame class PinballGame extends PinballForge2DGame
with with HasKeyboardHandlerComponents, MultiTouchTapDetector {
FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController>,
MultiTouchTapDetector {
PinballGame({ PinballGame({
required this.characterTheme, required CharacterTheme characterTheme,
required this.audio, required this.leaderboardRepository,
required this.l10n, required GameBloc gameBloc,
}) : super(gravity: Vector2(0, 30)) { required AppLocalizations l10n,
required PinballPlayer player,
}) : _gameBloc = gameBloc,
_player = player,
_characterTheme = characterTheme,
_l10n = l10n,
super(
gravity: Vector2(0, 30),
) {
images.prefix = ''; images.prefix = '';
controller = _GameBallsController(this);
} }
/// Identifier of the play button overlay /// Identifier of the play button overlay
@ -36,57 +40,64 @@ class PinballGame extends PinballForge2DGame
@override @override
Color backgroundColor() => Colors.transparent; Color backgroundColor() => Colors.transparent;
final CharacterTheme characterTheme; final CharacterTheme _characterTheme;
final PinballPlayer _player;
final PinballAudio audio; final LeaderboardRepository leaderboardRepository;
final AppLocalizations l10n; final AppLocalizations _l10n;
late final GameFlowController gameFlowController; final GameBloc _gameBloc;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await add(gameFlowController = GameFlowController(this));
await add(CameraController(this));
final machine = [
BoardBackgroundSpriteComponent(),
Boundaries(),
Backbox(),
];
final decals = [
GoogleWord(position: Vector2(-4.25, 1.8)),
Multipliers(),
Multiballs(),
SkillShot(
children: [
ScoringContactBehavior(points: Points.oneMillion),
],
),
];
final characterAreas = [
AndroidAcres(),
DinoDesert(),
FlutterForest(),
SparkyScorch(),
];
await add( await add(
CanvasComponent( FlameBlocProvider<GameBloc, GameState>.value(
onSpritePainted: (paint) { value: _gameBloc,
if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [ children: [
ZCanvasComponent( MultiFlameProvider(
providers: [
FlameProvider<PinballPlayer>.value(_player),
FlameProvider<CharacterTheme>.value(_characterTheme),
FlameProvider<LeaderboardRepository>.value(leaderboardRepository),
FlameProvider<AppLocalizations>.value(_l10n),
],
children: [ children: [
...machine, GameBlocStatusListener(),
...decals, BallSpawningBehavior(),
...characterAreas, CameraFocusingBehavior(),
Drain(), CanvasComponent(
BottomGroup(), onSpritePainted: (paint) {
Launcher(), if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [
ZCanvasComponent(
children: [
BoardBackgroundSpriteComponent(),
Boundaries(),
Backbox(leaderboardRepository: leaderboardRepository),
GoogleWord(position: Vector2(-4.45, 1.8)),
Multipliers(),
Multiballs(),
SkillShot(
children: [
ScoringContactBehavior(points: Points.oneMillion),
],
),
AndroidAcres(),
DinoDesert(),
FlutterForest(),
SparkyScorch(),
Drain(),
BottomGroup(),
Launcher(),
],
),
],
),
], ],
), ),
], ],
@ -144,57 +155,20 @@ class PinballGame extends PinballForge2DGame
} }
} }
class _GameBallsController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState> {
_GameBallsController(PinballGame game) : super(game);
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
final notGameOver = !newState.isGameOver;
return noBallsLeft && notGameOver;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
spawnBall();
}
@override
Future<void> onLoad() async {
await super.onLoad();
spawnBall();
}
void spawnBall() {
// TODO(alestiago): Refactor with behavioural pattern.
component.ready().whenComplete(() {
final plunger = parent!.descendants().whereType<Plunger>().single;
final ball = ControlledBall.launch(
characterTheme: component.characterTheme,
)..initialPosition = Vector2(
plunger.body.position.x,
plunger.body.position.y - Ball.size.y,
);
component.descendants().whereType<ZCanvasComponent>().single.add(ball);
});
}
}
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 LeaderboardRepository leaderboardRepository,
required AppLocalizations l10n, required AppLocalizations l10n,
required PinballPlayer player,
required GameBloc gameBloc,
}) : super( }) : super(
characterTheme: characterTheme, characterTheme: characterTheme,
audio: audio, player: player,
leaderboardRepository: leaderboardRepository,
l10n: l10n, l10n: l10n,
) { gameBloc: gameBloc,
controller = _GameBallsController(this); );
}
Vector2? lineStart; Vector2? lineStart;
Vector2? lineEnd; Vector2? lineEnd;

@ -4,6 +4,7 @@ import 'package:flame/game.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/assets_manager/assets_manager.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
@ -36,34 +37,43 @@ 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 leaderboardRepository = context.read<LeaderboardRepository>();
final game = isDebugMode return BlocProvider(
? DebugPinballGame( create: (_) => GameBloc(),
characterTheme: characterTheme, child: Builder(
audio: audio, builder: (context) {
l10n: context.l10n, final gameBloc = context.read<GameBloc>();
) final game = isDebugMode
: PinballGame( ? DebugPinballGame(
characterTheme: characterTheme, characterTheme: characterTheme,
audio: audio, player: player,
l10n: context.l10n, leaderboardRepository: leaderboardRepository,
); l10n: context.l10n,
gameBloc: gameBloc,
)
: PinballGame(
characterTheme: characterTheme,
player: player,
leaderboardRepository: leaderboardRepository,
l10n: context.l10n,
gameBloc: gameBloc,
);
final loadables = [ final loadables = [
...game.preLoadAssets(), ...game.preLoadAssets(),
pinballAudio.load(), ...player.load(),
...BonusAnimation.loadAssets(), ...BonusAnimation.loadAssets(),
...SelectedCharacter.loadAssets(), ...SelectedCharacter.loadAssets(),
]; ];
return MultiBlocProvider( return BlocProvider(
providers: [ create: (_) => AssetsManagerCubit(loadables)..load(),
BlocProvider(create: (_) => GameBloc()), child: PinballGameView(game: game),
BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()), );
], },
child: PinballGameView(game: game), ),
); );
} }
} }
@ -110,9 +120,9 @@ class PinballGameLoadedView extends StatelessWidget {
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 StartGameListener( return StartGameListener(
game: game,
child: Stack( child: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
@ -132,8 +142,8 @@ class PinballGameLoadedView extends StatelessWidget {
), ),
), ),
Positioned( Positioned(
top: 16, top: 0,
left: leftMargin, left: clampedMargin,
child: Visibility( child: Visibility(
visible: isPlaying, 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 {

@ -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,
@ -49,17 +50,19 @@ class _ScoreDisplay extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return Column( return FittedBox(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start,
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Text( children: [
l10n.score.toLowerCase(), Text(
style: Theme.of(context).textTheme.subtitle1, l10n.score.toLowerCase(),
), style: Theme.of(context).textTheme.subtitle1,
const _ScoreText(), ),
const RoundCountDisplay(), const _ScoreText(),
], const RoundCountDisplay(),
],
),
); );
} }
} }

@ -3,8 +3,10 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/gen/gen.dart'; import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
import 'package:platform_helper/platform_helper.dart'; import 'package:platform_helper/platform_helper.dart';
@ -91,12 +93,15 @@ class _HowToPlayDialogState extends State<HowToPlayDialog> {
return WillPopScope( return WillPopScope(
onWillPop: () { onWillPop: () {
widget.onDismissCallback.call(); widget.onDismissCallback.call();
context.read<PinballPlayer>().play(PinballAudio.ioPinballVoiceOver);
return Future.value(true); return Future.value(true);
}, },
child: PinballDialog( child: PinballDialog(
title: l10n.howToPlay, title: l10n.howToPlay,
subtitle: l10n.tipsForFlips, subtitle: l10n.tipsForFlips,
child: isMobile ? const _MobileBody() : const _DesktopBody(), child: FittedBox(
child: isMobile ? const _MobileBody() : const _DesktopBody(),
),
), ),
); );
} }
@ -109,18 +114,16 @@ 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, ),
), child: Column(
child: Column( children: [
children: [ const _MobileLaunchControls(),
const _MobileLaunchControls(), SizedBox(height: paddingHeight),
SizedBox(height: paddingHeight), const _MobileFlipperControls(),
const _MobileFlipperControls(), ],
],
),
), ),
); );
} }
@ -189,13 +192,15 @@ class _DesktopBody extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return Padding(
children: const [ padding: const EdgeInsets.all(16),
SizedBox(height: 16), child: Column(
_DesktopLaunchControls(), children: const [
SizedBox(height: 16), _DesktopLaunchControls(),
_DesktopFlipperControls(), SizedBox(height: 16),
], _DesktopFlipperControls(),
],
),
); );
} }
} }

@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
@ -6,9 +7,9 @@ import 'package:pinball_theme/pinball_theme.dart';
/// player's initials, score, and chosen character. /// player's initials, score, and chosen character.
/// ///
/// {@endtemplate} /// {@endtemplate}
class LeaderboardEntry { class LeaderboardEntry extends Equatable {
/// {@macro leaderboard_entry} /// {@macro leaderboard_entry}
LeaderboardEntry({ const LeaderboardEntry({
required this.rank, required this.rank,
required this.playerInitials, required this.playerInitials,
required this.score, required this.score,
@ -26,6 +27,9 @@ class LeaderboardEntry {
/// [CharacterTheme] for [LeaderboardEntry]. /// [CharacterTheme] for [LeaderboardEntry].
final AssetGenImage character; final AssetGenImage character;
@override
List<Object?> get props => [rank, playerInitials, score, character];
} }
/// Converts [LeaderboardEntryData] from repository to [LeaderboardEntry]. /// Converts [LeaderboardEntryData] from repository to [LeaderboardEntry].

@ -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,
); );
}); });
} }

@ -4,7 +4,6 @@ import 'package:pinball/game/game.dart';
import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/how_to_play/how_to_play.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart'; import 'package:pinball_ui/pinball_ui.dart';
/// {@template start_game_listener} /// {@template start_game_listener}
@ -18,13 +17,10 @@ class StartGameListener extends StatelessWidget {
const StartGameListener({ const StartGameListener({
Key? key, Key? key,
required Widget child, required Widget child,
required PinballGame game,
}) : _child = child, }) : _child = child,
_game = game,
super(key: key); super(key: key);
final Widget _child; final Widget _child;
final PinballGame _game;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -35,7 +31,7 @@ class StartGameListener extends StatelessWidget {
break; break;
case StartGameStatus.selectCharacter: case StartGameStatus.selectCharacter:
_onSelectCharacter(context); _onSelectCharacter(context);
_game.gameFlowController.start(); context.read<GameBloc>().add(const GameStarted());
break; break;
case StartGameStatus.howToPlay: case StartGameStatus.howToPlay:
_onHowToPlay(context); _onHowToPlay(context);
@ -57,14 +53,11 @@ class StartGameListener extends StatelessWidget {
} }
void _onHowToPlay(BuildContext context) { void _onHowToPlay(BuildContext context) {
final audio = context.read<PinballAudio>();
_showPinballDialog( _showPinballDialog(
context: context, context: context,
child: HowToPlayDialog( child: HowToPlayDialog(
onDismissCallback: () { onDismissCallback: () {
context.read<StartGameBloc>().add(const HowToPlayFinished()); context.read<StartGameBloc>().add(const HowToPlayFinished());
audio.ioPinballVoiceOver();
}, },
), ),
); );

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

@ -3,10 +3,31 @@ 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,
/// Game over
gameOverVoiceOver,
/// Launcher
launcher,
}
/// 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,
@ -15,28 +36,109 @@ typedef CreateAudioPool = Future<AudioPool> Function(
String? prefix, String? prefix,
}); });
/// Function that defines the contract for playing a single /// Defines the contract for playing a single audio.
/// audio
typedef PlaySingleAudio = Future<void> Function(String); typedef PlaySingleAudio = Future<void> Function(String);
/// Function that defines the contract for looping a single /// Defines the contract for looping a single audio.
/// audio
typedef LoopSingleAudio = Future<void> Function(String); typedef LoopSingleAudio = Future<void> Function(String);
/// Function that defines the contract for pre fetching an /// Defines the contract for pre fetching an audio.
/// audio
typedef PreCacheSingleAudio = Future<void> Function(String); typedef PreCacheSingleAudio = Future<void> Function(String);
/// Function that defines the contract for configuring /// Defines the contract for configuring 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 +154,39 @@ 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.launcher: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.launcher,
),
PinballAudio.ioPinballVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.ioPinballVoiceOver,
),
PinballAudio.gameOverVoiceOver: _SimplePlayAudio(
preCacheSingleAudio: _preCacheSingleAudio,
playSingleAudio: _playSingleAudio,
path: Assets.sfx.gameOverVoiceOver,
),
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 +200,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. /// Plays the received auido
void ioPinballVoiceOver() { void play(PinballAudio audio) {
_playSingleAudio(_prefixFile(Assets.sfx.ioPinballVoiceOver)); assert(
} audios.containsKey(audio),
'Tried to play unregistered audio $audio',
/// Plays the background music );
void backgroundMusic() { audios[audio]?.play();
_loopSingleAudio(_prefixFile(Assets.music.background));
}
String _prefixFile(String file) {
return 'packages/pinball_audio/$file';
} }
} }

@ -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
@ -146,6 +146,15 @@ void main() {
'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3', 'packages/pinball_audio/assets/sfx/io_pinball_voice_over.mp3',
), ),
).called(1); ).called(1);
verify(
() => preCacheSingleAudio.onCall(
'packages/pinball_audio/assets/sfx/game_over_voice_over.mp3',
),
).called(1);
verify(
() => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/sfx/launcher.mp3'),
).called(1);
verify( verify(
() => preCacheSingleAudio () => preCacheSingleAudio
.onCall('packages/pinball_audio/assets/music/background.mp3'), .onCall('packages/pinball_audio/assets/music/background.mp3'),
@ -184,8 +193,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 +203,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 +213,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
@ -214,10 +223,22 @@ void main() {
}); });
}); });
group('launcher', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.launcher);
verify(
() => playSingleAudio
.onCall('packages/pinball_audio/${Assets.sfx.launcher}'),
).called(1);
});
});
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(
@ -227,10 +248,23 @@ void main() {
}); });
}); });
group('gameOverVoiceOver', () {
test('plays the correct file', () async {
await Future.wait(player.load());
player.play(PinballAudio.gameOverVoiceOver);
verify(
() => playSingleAudio.onCall(
'packages/pinball_audio/${Assets.sfx.gameOverVoiceOver}',
),
).called(1);
});
});
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 +272,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: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

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.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 374 KiB

@ -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,
), ),
); );

@ -9,7 +9,7 @@ class BoardBackgroundSpriteComponent extends SpriteComponent
BoardBackgroundSpriteComponent() BoardBackgroundSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -1), position: Vector2(-0.2, 0.1),
) { ) {
zIndex = ZIndexes.boardBackground; zIndex = ZIndexes.boardBackground;
} }

@ -68,7 +68,7 @@ class _BottomBoundarySpriteComponent extends SpriteComponent with HasGameRef {
_BottomBoundarySpriteComponent() _BottomBoundarySpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-5, 55.6), position: Vector2(-5.2, 55.6),
); );
@override @override

@ -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,

@ -37,46 +37,46 @@ class _DinoTopWall extends BodyComponent with InitialPosition {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topEdgeShape = EdgeShape() final topEdgeShape = EdgeShape()
..set( ..set(
Vector2(29.25, -35.27), Vector2(29.05, -35.27),
Vector2(28.4, -34.77), Vector2(28.2, -34.77),
); );
final topCurveShape = BezierCurveShape( final topCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
topEdgeShape.vertex2, topEdgeShape.vertex2,
Vector2(21.35, -28.72), Vector2(21.15, -28.72),
Vector2(23.45, -24.62), Vector2(23.25, -24.62),
], ],
); );
final tunnelTopEdgeShape = EdgeShape() final tunnelTopEdgeShape = EdgeShape()
..set( ..set(
topCurveShape.vertices.last, topCurveShape.vertices.last,
Vector2(30.35, -27.32), Vector2(30.15, -27.32),
); );
final tunnelBottomEdgeShape = EdgeShape() final tunnelBottomEdgeShape = EdgeShape()
..set( ..set(
Vector2(30.75, -23.17), Vector2(30.55, -23.17),
Vector2(25.45, -21.22), Vector2(25.25, -21.22),
); );
final middleEdgeShape = EdgeShape() final middleEdgeShape = EdgeShape()
..set( ..set(
tunnelBottomEdgeShape.vertex2, tunnelBottomEdgeShape.vertex2,
Vector2(27.45, -19.32), Vector2(27.25, -19.32),
); );
final bottomEdgeShape = EdgeShape() final bottomEdgeShape = EdgeShape()
..set( ..set(
middleEdgeShape.vertex2, middleEdgeShape.vertex2,
Vector2(24.65, -15.02), Vector2(24.45, -15.02),
); );
final undersideEdgeShape = EdgeShape() final undersideEdgeShape = EdgeShape()
..set( ..set(
bottomEdgeShape.vertex2, bottomEdgeShape.vertex2,
Vector2(31.75, -13.77), Vector2(31.55, -13.77),
); );
return [ return [
@ -108,7 +108,7 @@ class _DinoTopWallSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex { with HasGameRef, ZIndex {
_DinoTopWallSpriteComponent() _DinoTopWallSpriteComponent()
: super( : super(
position: Vector2(22.75, -38.07), position: Vector2(22.55, -38.07),
) { ) {
zIndex = ZIndexes.dinoTopWall; zIndex = ZIndexes.dinoTopWall;
} }
@ -129,7 +129,7 @@ class _DinoTopWallSpriteComponent extends SpriteComponent
class _DinoTopWallTunnelSpriteComponent extends SpriteComponent class _DinoTopWallTunnelSpriteComponent extends SpriteComponent
with HasGameRef, ZIndex { with HasGameRef, ZIndex {
_DinoTopWallTunnelSpriteComponent() _DinoTopWallTunnelSpriteComponent()
: super(position: Vector2(23.31, -26.01)) { : super(position: Vector2(23.11, -26.01)) {
zIndex = ZIndexes.dinoTopWallTunnel; zIndex = ZIndexes.dinoTopWallTunnel;
} }
@ -162,28 +162,28 @@ class _DinoBottomWall extends BodyComponent with InitialPosition, ZIndex {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final topEdgeShape = EdgeShape() final topEdgeShape = EdgeShape()
..set( ..set(
Vector2(32.4, -8.8), Vector2(32.2, -8.8),
Vector2(25, -7.7), Vector2(24.8, -7.7),
); );
final topLeftCurveShape = BezierCurveShape( final topLeftCurveShape = BezierCurveShape(
controlPoints: [ controlPoints: [
topEdgeShape.vertex2, topEdgeShape.vertex2,
Vector2(21.8, -7), Vector2(21.6, -7),
Vector2(29.8, 13.8), Vector2(29.6, 13.8),
], ],
); );
final bottomLeftEdgeShape = EdgeShape() final bottomLeftEdgeShape = EdgeShape()
..set( ..set(
topLeftCurveShape.vertices.last, topLeftCurveShape.vertices.last,
Vector2(31.9, 44.1), Vector2(31.7, 44.1),
); );
final bottomEdgeShape = EdgeShape() final bottomEdgeShape = EdgeShape()
..set( ..set(
bottomLeftEdgeShape.vertex2, bottomLeftEdgeShape.vertex2,
Vector2(37.8, 44.1), Vector2(37.6, 44.1),
); );
return [ return [
@ -219,6 +219,6 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef {
); );
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
position = Vector2(23.8, -9.5); position = Vector2(23.6, -9.5);
} }
} }

@ -18,9 +18,9 @@ class Flapper extends Component {
children: [ children: [
FlapperSpinningBehavior(), FlapperSpinningBehavior(),
], ],
)..initialPosition = Vector2(4, -69.3), )..initialPosition = Vector2(3.8, -69.3),
_FlapperStructure(), _FlapperStructure(),
_FlapperExit()..initialPosition = Vector2(-0.6, -33.8), _FlapperExit()..initialPosition = Vector2(-0.8, -33.8),
_BackSupportSpriteComponent(), _BackSupportSpriteComponent(),
_FrontSupportSpriteComponent(), _FrontSupportSpriteComponent(),
FlapSpriteAnimationComponent(), FlapSpriteAnimationComponent(),
@ -73,14 +73,14 @@ class _FlapperStructure extends BodyComponent with Layered {
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final leftEdgeShape = EdgeShape() final leftEdgeShape = EdgeShape()
..set( ..set(
Vector2(1.9, -69.3), Vector2(1.7, -69.3),
Vector2(1.9, -66), Vector2(1.7, -66),
); );
final bottomEdgeShape = EdgeShape() final bottomEdgeShape = EdgeShape()
..set( ..set(
leftEdgeShape.vertex2, leftEdgeShape.vertex2,
Vector2(3.9, -66), Vector2(3.7, -66),
); );
return [ return [
@ -130,7 +130,7 @@ class FlapSpriteAnimationComponent extends SpriteAnimationComponent
FlapSpriteAnimationComponent() FlapSpriteAnimationComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(2.8, -70.7), position: Vector2(2.6, -70.7),
playing: false, playing: false,
) { ) {
zIndex = ZIndexes.flapper; zIndex = ZIndexes.flapper;
@ -173,7 +173,7 @@ class _BackSupportSpriteComponent extends SpriteComponent
_BackSupportSpriteComponent() _BackSupportSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(2.95, -70.6), position: Vector2(2.75, -70.6),
) { ) {
zIndex = ZIndexes.flapperBack; zIndex = ZIndexes.flapperBack;
} }
@ -196,7 +196,7 @@ class _FrontSupportSpriteComponent extends SpriteComponent
_FrontSupportSpriteComponent() _FrontSupportSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(2.9, -67.6), position: Vector2(2.7, -67.7),
) { ) {
zIndex = ZIndexes.flapperFront; zIndex = ZIndexes.flapperFront;
} }

@ -40,22 +40,22 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
final rightStraightShape = EdgeShape() final rightStraightShape = EdgeShape()
..set( ..set(
Vector2(31.4, -61.4), Vector2(31, -61.4),
Vector2(46.5, 68.4), Vector2(46.1, 68.4),
); );
final rightStraightFixtureDef = FixtureDef(rightStraightShape); final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef); fixturesDef.add(rightStraightFixtureDef);
final leftStraightShape = EdgeShape() final leftStraightShape = EdgeShape()
..set( ..set(
Vector2(27.8, -61.4), Vector2(27.4, -61.4),
Vector2(41.5, 68.4), Vector2(41.1, 68.4),
); );
final leftStraightFixtureDef = FixtureDef(leftStraightShape); final leftStraightFixtureDef = FixtureDef(leftStraightShape);
fixturesDef.add(leftStraightFixtureDef); fixturesDef.add(leftStraightFixtureDef);
final topCurveShape = ArcShape( final topCurveShape = ArcShape(
center: Vector2(20.5, -61.1), center: Vector2(20.1, -61.1),
arcRadius: 11, arcRadius: 11,
angle: 1.6, angle: 1.6,
rotation: 0.1, rotation: 0.1,
@ -64,7 +64,7 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
fixturesDef.add(topCurveFixtureDef); fixturesDef.add(topCurveFixtureDef);
final bottomCurveShape = ArcShape( final bottomCurveShape = ArcShape(
center: Vector2(19.3, -60.3), center: Vector2(18.9, -60.3),
arcRadius: 8.5, arcRadius: 8.5,
angle: 1.48, angle: 1.48,
rotation: 0.1, rotation: 0.1,
@ -74,16 +74,16 @@ class _LaunchRampBase extends BodyComponent with Layered, ZIndex {
final topStraightShape = EdgeShape() final topStraightShape = EdgeShape()
..set( ..set(
Vector2(3.7, -70.1), Vector2(3.3, -70.1),
Vector2(19.1, -72.1), Vector2(18.7, -72.1),
); );
final topStraightFixtureDef = FixtureDef(topStraightShape); final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef); fixturesDef.add(topStraightFixtureDef);
final bottomStraightShape = EdgeShape() final bottomStraightShape = EdgeShape()
..set( ..set(
Vector2(3.7, -66.9), Vector2(3.3, -66.9),
Vector2(19.1, -68.8), Vector2(18.7, -68.8),
); );
final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); final bottomStraightFixtureDef = FixtureDef(bottomStraightShape);
fixturesDef.add(bottomStraightFixtureDef); fixturesDef.add(bottomStraightFixtureDef);
@ -113,7 +113,7 @@ class _LaunchRampBaseSpriteComponent extends SpriteComponent with HasGameRef {
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
position = Vector2(25.65, 0.7); position = Vector2(25.25, 0.7);
} }
} }
@ -131,7 +131,7 @@ class _LaunchRampBackgroundRailingSpriteComponent extends SpriteComponent
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
position = Vector2(25.6, -1.3); position = Vector2(25.2, -1.3);
} }
} }
@ -149,14 +149,14 @@ class _LaunchRampForegroundRailing extends BodyComponent with ZIndex {
final rightStraightShape = EdgeShape() final rightStraightShape = EdgeShape()
..set( ..set(
Vector2(27.6, -57.9), Vector2(27.2, -57.9),
Vector2(38.1, 42.6), Vector2(37.7, 42.6),
); );
final rightStraightFixtureDef = FixtureDef(rightStraightShape); final rightStraightFixtureDef = FixtureDef(rightStraightShape);
fixturesDef.add(rightStraightFixtureDef); fixturesDef.add(rightStraightFixtureDef);
final curveShape = ArcShape( final curveShape = ArcShape(
center: Vector2(20.1, -59.3), center: Vector2(19.7, -59.3),
arcRadius: 7.5, arcRadius: 7.5,
angle: 1.8, angle: 1.8,
rotation: -0.13, rotation: -0.13,
@ -166,8 +166,8 @@ class _LaunchRampForegroundRailing extends BodyComponent with ZIndex {
final topStraightShape = EdgeShape() final topStraightShape = EdgeShape()
..set( ..set(
Vector2(3.7, -66.8), Vector2(3.3, -66.8),
Vector2(19.7, -66.8), Vector2(19.3, -66.8),
); );
final topStraightFixtureDef = FixtureDef(topStraightShape); final topStraightFixtureDef = FixtureDef(topStraightShape);
fixturesDef.add(topStraightFixtureDef); fixturesDef.add(topStraightFixtureDef);
@ -198,6 +198,6 @@ class _LaunchRampForegroundRailingSpriteComponent extends SpriteComponent
this.sprite = sprite; this.sprite = sprite;
size = sprite.originalSize / 10; size = sprite.originalSize / 10;
anchor = Anchor.center; anchor = Anchor.center;
position = Vector2(22.8, 0.5); position = Vector2(22.4, 0.5);
} }
} }

@ -36,8 +36,8 @@ class Multiball extends Component {
Multiball.a({ Multiball.a({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
position: Vector2(-23, 7.5), position: Vector2(-23.3, 7.5),
rotation: -24 * math.pi / 180, rotation: -27 * math.pi / 180,
bloc: MultiballCubit(), bloc: MultiballCubit(),
children: children, children: children,
); );
@ -46,8 +46,8 @@ class Multiball extends Component {
Multiball.b({ Multiball.b({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
position: Vector2(-7.2, -6.2), position: Vector2(-7.65, -6.2),
rotation: -5 * math.pi / 180, rotation: -2 * math.pi / 180,
bloc: MultiballCubit(), bloc: MultiballCubit(),
children: children, children: children,
); );
@ -56,8 +56,8 @@ class Multiball extends Component {
Multiball.c({ Multiball.c({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
position: Vector2(-0.7, -9.3), position: Vector2(-1.1, -9.3),
rotation: 2.7 * math.pi / 180, rotation: 6 * math.pi / 180,
bloc: MultiballCubit(), bloc: MultiballCubit(),
children: children, children: children,
); );
@ -66,8 +66,8 @@ class Multiball extends Component {
Multiball.d({ Multiball.d({
Iterable<Component>? children, Iterable<Component>? children,
}) : this._( }) : this._(
position: Vector2(15, 7), position: Vector2(14.8, 7),
rotation: 24 * math.pi / 180, rotation: 27 * math.pi / 180,
bloc: MultiballCubit(), bloc: MultiballCubit(),
children: children, children: children,
); );

@ -1,5 +1,6 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -13,16 +14,23 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// {@macro plunger} /// {@macro plunger}
Plunger({ Plunger({
required this.compressionDistance, required this.compressionDistance,
}) : super(renderBody: false) { }) : super(
renderBody: false,
children: [_PlungerSpriteAnimationGroupComponent()],
) {
zIndex = ZIndexes.plunger; zIndex = ZIndexes.plunger;
layer = Layer.launcher; layer = Layer.launcher;
} }
/// Creates a [Plunger] without any children.
///
/// This can be used for testing [Plunger]'s behaviors in isolation.
@visibleForTesting
Plunger.test({required this.compressionDistance});
/// Distance the plunger can lower. /// Distance the plunger can lower.
final double compressionDistance; final double compressionDistance;
late final _PlungerSpriteAnimationGroupComponent _spriteComponent;
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
@ -78,8 +86,10 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// Set a constant downward velocity on the [Plunger]. /// Set a constant downward velocity on the [Plunger].
void pull() { void pull() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
body.linearVelocity = Vector2(0, 7); body.linearVelocity = Vector2(0, 7);
_spriteComponent.pull(); sprite.pull();
} }
/// Set an upward velocity on the [Plunger]. /// Set an upward velocity on the [Plunger].
@ -87,17 +97,19 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
/// The velocity's magnitude depends on how far the [Plunger] has been pulled /// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [initialPosition]. /// from its original [initialPosition].
void release() { void release() {
final sprite = firstChild<_PlungerSpriteAnimationGroupComponent>()!;
_pullingDownTime = 0; _pullingDownTime = 0;
final velocity = (initialPosition.y - body.position.y) * 11; final velocity = (initialPosition.y - body.position.y) * 11;
body.linearVelocity = Vector2(0, velocity); body.linearVelocity = Vector2(0, velocity);
_spriteComponent.release(); sprite.release();
} }
@override @override
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 {
@ -127,9 +139,6 @@ class Plunger extends BodyComponent with InitialPosition, Layered, ZIndex {
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
await _anchorToJoint(); await _anchorToJoint();
_spriteComponent = _PlungerSpriteAnimationGroupComponent();
await add(_spriteComponent);
} }
} }

@ -13,15 +13,13 @@ class Slingshots extends Component with ZIndex {
: super( : super(
children: [ children: [
Slingshot( Slingshot(
length: 5.64,
angle: -0.017, angle: -0.017,
spritePath: Assets.images.slingshot.upper.keyName, spritePath: Assets.images.slingshot.upper.keyName,
)..initialPosition = Vector2(22.3, -1.58), )..initialPosition = Vector2(22.7, -0.3),
Slingshot( Slingshot(
length: 3.46,
angle: -0.468, angle: -0.468,
spritePath: Assets.images.slingshot.lower.keyName, spritePath: Assets.images.slingshot.lower.keyName,
)..initialPosition = Vector2(24.7, 6.2), )..initialPosition = Vector2(24.6, 6.1),
], ],
) { ) {
zIndex = ZIndexes.slingshots; zIndex = ZIndexes.slingshots;
@ -34,11 +32,9 @@ class Slingshots extends Component with ZIndex {
class Slingshot extends BodyComponent with InitialPosition { class Slingshot extends BodyComponent with InitialPosition {
/// {@macro slingshot} /// {@macro slingshot}
Slingshot({ Slingshot({
required double length,
required double angle, required double angle,
required String spritePath, required String spritePath,
}) : _length = length, }) : _angle = angle,
_angle = angle,
super( super(
children: [ children: [
_SlinghsotSpriteComponent(spritePath, angle: angle), _SlinghsotSpriteComponent(spritePath, angle: angle),
@ -47,29 +43,28 @@ class Slingshot extends BodyComponent with InitialPosition {
renderBody: false, renderBody: false,
); );
final double _length;
final double _angle; final double _angle;
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
const length = 3.46;
const circleRadius = 1.55; const circleRadius = 1.55;
final topCircleShape = CircleShape()..radius = circleRadius; final topCircleShape = CircleShape()..radius = circleRadius;
topCircleShape.position.setValues(0, -_length / 2); topCircleShape.position.setValues(0, -length / 2);
final bottomCircleShape = CircleShape()..radius = circleRadius; final bottomCircleShape = CircleShape()..radius = circleRadius;
bottomCircleShape.position.setValues(0, _length / 2); bottomCircleShape.position.setValues(0, length / 2);
final leftEdgeShape = EdgeShape() final leftEdgeShape = EdgeShape()
..set( ..set(
Vector2(circleRadius, _length / 2), Vector2(circleRadius, length / 2),
Vector2(circleRadius, -_length / 2), Vector2(circleRadius, -length / 2),
); );
final rightEdgeShape = EdgeShape() final rightEdgeShape = EdgeShape()
..set( ..set(
Vector2(-circleRadius, _length / 2), Vector2(-circleRadius, length / 2),
Vector2(-circleRadius, -_length / 2), Vector2(-circleRadius, -length / 2),
); );
return [ return [

@ -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.3, -45.9),
Vector2(-15.3, -49.6), Vector2(-15.7, -49.5),
); );
final topEdge = EdgeShape() final topEdge = EdgeShape()
..set( ..set(
Vector2(-15.3, -49.6), leftEdge.vertex2,
Vector2(-10.7, -50.6), Vector2(-11.1, -50.5),
); );
final rightEdge = EdgeShape() final rightEdge = EdgeShape()
..set( ..set(
Vector2(-10.7, -50.6), topEdge.vertex2,
Vector2(-9, -47.2), Vector2(-9.4, -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.44, -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.86, -49.37),
) { ) {
zIndex = ZIndexes.computerTop; zIndex = ZIndexes.computerTop;
} }
@ -113,7 +113,7 @@ class _ComputerGlowSpriteComponent extends SpriteComponent
_ComputerGlowSpriteComponent() _ComputerGlowSpriteComponent()
: super( : super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(7.4, 10), position: Vector2(4, 11),
) { ) {
zIndex = ZIndexes.computerGlow; zIndex = ZIndexes.computerGlow;
} }

@ -33,7 +33,7 @@ abstract class ZIndexes {
static const outerBoundary = _above + boardBackground; static const outerBoundary = _above + boardBackground;
static const outerBottomBoundary = _above + rocket; static const outerBottomBoundary = _above + bottomBoundary;
// Bottom Group // Bottom Group
@ -77,7 +77,7 @@ abstract class ZIndexes {
static const computerTop = _above + ballOnBoard; static const computerTop = _above + ballOnBoard;
static const computerGlow = _above + ballOnBoard; static const computerGlow = _above + computerTop;
static const sparkyAnimatronic = _above + spaceshipRampForegroundRailing; static const sparkyAnimatronic = _above + spaceshipRampForegroundRailing;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 254 KiB

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

Loading…
Cancel
Save