@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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';
|
||||||
|
@ -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.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);*/
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 616 KiB After Width: | Height: | Size: 636 KiB |
Before Width: | Height: | Size: 735 KiB After Width: | Height: | Size: 259 KiB |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 886 KiB After Width: | Height: | Size: 1012 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 374 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 218 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 254 KiB |