diff --git a/lib/game/components/backbox/backbox.dart b/lib/game/components/backbox/backbox.dart index 0ef85fba..f435ebdf 100644 --- a/lib/game/components/backbox/backbox.dart +++ b/lib/game/components/backbox/backbox.dart @@ -1,17 +1,23 @@ import 'dart:async'; import 'package:flame/components.dart'; +import 'package:flutter_bloc/flutter_bloc.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_components/pinball_components.dart'; import 'package:pinball_flame/pinball_flame.dart'; +import 'package:pinball_theme/pinball_theme.dart' hide Assets; /// {@template backbox} /// The [Backbox] of the pinball machine. /// {@endtemplate} class Backbox extends PositionComponent with HasGameRef, ZIndex { /// {@macro backbox} - Backbox() - : super( + Backbox({ + LeaderboardRepository? leaderboardRepository, + }) : _leaderboardRepository = leaderboardRepository, + super( position: Vector2(0, -87), anchor: Anchor.bottomCenter, children: [ @@ -19,20 +25,66 @@ class Backbox extends PositionComponent with HasGameRef, ZIndex { ], ) { zIndex = ZIndexes.backbox; + add(_display = Component()); + } + + late final Component _display; + late final LeaderboardRepository? _leaderboardRepository; + late BackboxBloc _bloc; + late StreamSubscription _subscription; + + @override + Future onLoad() async { + final repository = _leaderboardRepository ?? + gameRef.buildContext!.read(); + _bloc = BackboxBloc(leaderboardRepository: repository); + _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( + PlayerInitialsSubmited( + 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]. - Future initialsInput({ + void requestInitials({ required int score, - required String characterIconPath, - InitialsOnSubmit? onSubmit, - }) async { - removeAll(children.where((child) => child is! _BackboxSpriteComponent)); - await add( - InitialsInputDisplay( + required CharacterTheme character, + }) { + _bloc.add( + PlayerInitialsRequested( score: score, - characterIconPath: characterIconPath, - onSubmit: onSubmit, + character: character, ), ); } diff --git a/lib/game/components/backbox/bloc/backbox_bloc.dart b/lib/game/components/backbox/bloc/backbox_bloc.dart new file mode 100644 index 00000000..f51af960 --- /dev/null +++ b/lib/game/components/backbox/bloc/backbox_bloc.dart @@ -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 { + /// {@macro backbox_bloc} + BackboxBloc({ + required LeaderboardRepository leaderboardRepository, + }) : _leaderboardRepository = leaderboardRepository, + super(LoadingState()) { + on(_onPlayerInitialsRequested); + on(_onPlayerInitialsSubmited); + } + + final LeaderboardRepository _leaderboardRepository; + + void _onPlayerInitialsRequested( + PlayerInitialsRequested event, + Emitter emit, + ) { + emit( + InitialsFormState( + score: event.score, + character: event.character, + ), + ); + } + + Future _onPlayerInitialsSubmited( + PlayerInitialsSubmited event, + Emitter emit, + ) async { + try { + emit(LoadingState()); + await _leaderboardRepository.addLeaderboardEntry( + LeaderboardEntryData( + playerInitials: event.initials, + score: event.score, + character: event.character.toType, + ), + ); + emit(InitialsSuccessState()); + } catch (e, s) { + addError(e, s); + emit(InitialsFailureState()); + } + } +} diff --git a/lib/game/components/backbox/bloc/backbox_event.dart b/lib/game/components/backbox/bloc/backbox_event.dart new file mode 100644 index 00000000..bbee0be3 --- /dev/null +++ b/lib/game/components/backbox/bloc/backbox_event.dart @@ -0,0 +1,50 @@ +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 { + /// {@template 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 get props => [props, character]; +} + +/// {@template player_initials_submited} +/// Event that submits the user score and initials +/// {@endtemplate} +class PlayerInitialsSubmited extends BackboxEvent { + /// {@template player_initials_requested} + const PlayerInitialsSubmited({ + 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 get props => [props, initials, character]; +} diff --git a/lib/game/components/backbox/bloc/backbox_state.dart b/lib/game/components/backbox/bloc/backbox_state.dart new file mode 100644 index 00000000..1cd07320 --- /dev/null +++ b/lib/game/components/backbox/bloc/backbox_state.dart @@ -0,0 +1,65 @@ +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 get props => []; +} + +/// Failure state for the backbox +class FailureState extends BackboxState { + @override + List get props => []; +} + +/// State when the leaderboard was successfully loaded +class LeaderboardSuccessState extends BackboxState { + @override + List get props => []; +} + +/// State when the leaderboard was successfully loaded +class LeaderboardFailureState extends BackboxState { + @override + List 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 get props => [score, character]; +} + +/// State when the leaderboard was successfully loaded +class InitialsSuccessState extends BackboxState { + @override + List get props => []; +} + +/// State when the leaderboard was successfully loaded +class InitialsFailureState extends BackboxState { + @override + List get props => []; +} diff --git a/lib/game/components/backbox/displays/displays.dart b/lib/game/components/backbox/displays/displays.dart index 194212ab..a516587d 100644 --- a/lib/game/components/backbox/displays/displays.dart +++ b/lib/game/components/backbox/displays/displays.dart @@ -1 +1,4 @@ export 'initials_input_display.dart'; +export 'initials_submission_failure_display.dart'; +export 'initials_submission_success_display.dart'; +export 'loading_display.dart'; diff --git a/lib/game/components/backbox/displays/initials_submission_failure_display.dart b/lib/game/components/backbox/displays/initials_submission_failure_display.dart new file mode 100644 index 00000000..dceee1dd --- /dev/null +++ b/lib/game/components/backbox/displays/initials_submission_failure_display.dart @@ -0,0 +1,28 @@ +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} +/// Display when the initials failed to submitted +/// {@endtemplate} +class InitialsSubmissionFailureDisplay extends TextComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + position = Vector2(0, -10); + anchor = Anchor.center; + text = 'Failure!'; + textRenderer = _bodyTextPaint; + } +} diff --git a/lib/game/components/backbox/displays/initials_submission_success_display.dart b/lib/game/components/backbox/displays/initials_submission_success_display.dart new file mode 100644 index 00000000..92772e6e --- /dev/null +++ b/lib/game/components/backbox/displays/initials_submission_success_display.dart @@ -0,0 +1,28 @@ +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} +/// Display when the initials were successfully submitted +/// {@endtemplate} +class InitialsSubmissionSuccessDisplay extends TextComponent + with HasGameRef { + @override + Future onLoad() async { + await super.onLoad(); + position = Vector2(0, -10); + anchor = Anchor.center; + text = 'Success!'; + textRenderer = _bodyTextPaint; + } +} diff --git a/lib/game/components/backbox/displays/loading_display.dart b/lib/game/components/backbox/displays/loading_display.dart new file mode 100644 index 00000000..8cf2f9a4 --- /dev/null +++ b/lib/game/components/backbox/displays/loading_display.dart @@ -0,0 +1,45 @@ +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 loading_display} +/// +/// {@endtemplate} +class LoadingDisplay extends TextComponent with HasGameRef { + late final String _label; + + @override + Future onLoad() async { + await super.onLoad(); + + position = Vector2(0, -10); + anchor = Anchor.center; + text = _label = gameRef.l10n.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.'; + } + }, + ), + ); + } +} diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index 0012f62b..1984a523 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -22,9 +22,9 @@ class GameBlocStatusListener extends Component gameRef.overlays.remove(PinballGame.playButtonOverlay); break; case GameStatus.gameOver: - gameRef.descendants().whereType().first.initialsInput( + gameRef.descendants().whereType().first.requestInitials( score: state.displayScore, - characterIconPath: gameRef.characterTheme.leaderboardIcon.keyName, + character: gameRef.characterTheme, ); gameRef.firstChild()!.focusOnGameOverBackbox(); break; diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 65a19f29..817e9d28 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -7,6 +7,7 @@ import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:pinball/game/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; @@ -203,6 +204,18 @@ class DebugPinballGame extends PinballGame with FPSCounter, PanDetector { await add(PreviewLine()); await add(_DebugInformation()); + await add( + KeyboardInputController( + keyUp: { + LogicalKeyboardKey.escape: () { + read().add(const RoundLost()); + read().add(const RoundLost()); + read().add(const RoundLost()); + return true; + }, + }, + ), + ); } @override