feat(game): CONTINUE? + GAME OVER + fade-to-LOGO (Fase 2)
Sub-FSM nova `GameOverFsm` (CONTINUE_PROMPT → GAME_OVER → FADE_OUT → DONE) que viu en paral·lel a la `RoomFsm`. Es dispara quan tots els jugadors queden DISABLED durant PLAYING, mostra el rètol "Continuar?" amb countdown reusant el panel del room text, i si ningú pulsa START acaba en "Fi del Joc" + fade negre i sol·licita la transició a LOGO. Pulsar START durant CONTINUE? cancel·la el countdown i fa join (revive) del jugador. Després de GAME OVER, el HUD passa a "ESPERA / PER JUGAR" i el START queda bloquejat fins a la transició d'escena. Els helpers d'animació del room text (X i altura del panel) ara reben phase + elapsed_ms per paràmetre per poder ser reaprofitats per les dues sub-FSMs sense duplicar la lògica visual. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c13b11c501
commit
78e164fce4
8 files changed
+562
-43
No files matched your search
@@ -36,6 +36,7 @@
|
||||
#include "game/levels/level_manager.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "game/scenes/scene_flow.hpp"
|
||||
#include "game/scenes/start_input.hpp"
|
||||
#include "game/ui/hud.hpp"
|
||||
#include "game/ui/service_menu.hpp"
|
||||
@@ -493,6 +494,10 @@ GameScene::GameScene(Config config)
|
||||
// Timings i corbes de la FSM de sala. Llegits del YAML una sola
|
||||
// vegada; canvis requereixen reiniciar el joc.
|
||||
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::GAME_YAML);
|
||||
// Timings de la sub-FSM de game over (CONTINUE? + GAME OVER + fade
|
||||
// final). Comparteix YAML amb la RoomFsm però viu en una secció
|
||||
// pròpia (`game_over:`).
|
||||
game_over_config_ = GameOverFsm::loadConfig(Defaults::GameScene::GAME_YAML);
|
||||
|
||||
// Cinemàtica d'entrada de la primera sala: arrenca la pista
|
||||
// `room_intro.ogg` i arma el callback de fi que farà spawn + salt
|
||||
@@ -700,17 +705,27 @@ void GameScene::onPlayerDisabled(int player_id) {
|
||||
}
|
||||
|
||||
void GameScene::handleJoinInput() {
|
||||
// El "join in progress" només es processa durant PLAYING. Durant
|
||||
// INTRO la cinemàtica de spawn ja té la seva pròpia coreografia
|
||||
// (vegeu `spawnDuePlayers`); durant OUTRO el cicle de sala ja està
|
||||
// tancant. En tots dos casos el HUD mostra "ESPERA / PARA JUGAR"
|
||||
// perquè quede clar que la pulsació encara no té efecte.
|
||||
if (state_ != RoomFsm::State::PLAYING) { return; }
|
||||
// El "join in progress" es processa durant PLAYING (regla 1) i,
|
||||
// com a cas especial, durant CONTINUE_PROMPT — ahí un START reviu
|
||||
// el jugador i cancel·la el countdown. A partir de GAME_OVER el
|
||||
// START queda bloquejat (regla 5: ja no es pot pulsar start).
|
||||
const bool IN_PLAYING = (state_ == RoomFsm::State::PLAYING);
|
||||
const bool IN_CONTINUE = (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT);
|
||||
const bool IN_GAME_OVER = (game_over_state_ != GameOverFsm::State::OFF && game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT);
|
||||
if (IN_GAME_OVER) { return; }
|
||||
if (!IN_PLAYING && !IN_CONTINUE) { return; }
|
||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||
if (players_active_[i]) { continue; }
|
||||
if (!players_[i]->isDisabled()) { continue; }
|
||||
if (!Input::Detect::pressedStart(static_cast<int>(i))) { continue; }
|
||||
joinPlayer(i);
|
||||
// Si veníem del countdown de CONTINUE?, disparem la sortida
|
||||
// visual del rètol. El jugador ja apareix en escena per la
|
||||
// crida prèvia a `joinPlayer`; el text marxa amb la mateixa
|
||||
// exit anim que el room text.
|
||||
if (IN_CONTINUE) {
|
||||
GameOverFsm::cancelContinuePrompt(game_over_state_, game_over_transition_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,20 +756,29 @@ auto GameScene::activePlayerCount() const -> int {
|
||||
|
||||
void GameScene::refreshJoinPrompt() {
|
||||
if (!hud_) { return; }
|
||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||
Hud::JoinPromptMode mode = Hud::JoinPromptMode::NONE;
|
||||
if (!players_active_[i]) {
|
||||
switch (state_) {
|
||||
case RoomFsm::State::PLAYING:
|
||||
mode = Hud::JoinPromptMode::CAN_JOIN;
|
||||
break;
|
||||
case RoomFsm::State::ROOM_INTRO:
|
||||
case RoomFsm::State::ROOM_OUTRO:
|
||||
mode = Hud::JoinPromptMode::WAIT;
|
||||
break;
|
||||
}
|
||||
// El game over substitueix la decisió purament basada en RoomFsm:
|
||||
// durant CONTINUE_PROMPT els jugadors encara poden entrar (revive),
|
||||
// a partir de GAME_OVER ja no, així que mostrem WAIT (o NONE) per
|
||||
// indicar que el START ja no farà res fins a la transició a LOGO.
|
||||
Hud::JoinPromptMode mode_inactive = Hud::JoinPromptMode::NONE;
|
||||
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
|
||||
mode_inactive = Hud::JoinPromptMode::CAN_JOIN;
|
||||
} else if (game_over_state_ != GameOverFsm::State::OFF) {
|
||||
mode_inactive = Hud::JoinPromptMode::WAIT;
|
||||
} else {
|
||||
switch (state_) {
|
||||
case RoomFsm::State::PLAYING:
|
||||
mode_inactive = Hud::JoinPromptMode::CAN_JOIN;
|
||||
break;
|
||||
case RoomFsm::State::ROOM_INTRO:
|
||||
case RoomFsm::State::ROOM_OUTRO:
|
||||
mode_inactive = Hud::JoinPromptMode::WAIT;
|
||||
break;
|
||||
}
|
||||
hud_->setJoinPrompt(static_cast<int>(i) + 1, mode);
|
||||
}
|
||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||
const auto MODE = players_active_[i] ? Hud::JoinPromptMode::NONE : mode_inactive;
|
||||
hud_->setJoinPrompt(static_cast<int>(i) + 1, MODE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,12 +961,14 @@ void GameScene::renderRoomText() {
|
||||
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
||||
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
||||
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) {
|
||||
const int PHASE = transition_.phase;
|
||||
const int ELAPSED = transition_.elapsed_ms;
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO && PHASE >= 2 && PHASE <= 4) {
|
||||
const int DUR_ENTER = transition_config_.intro_text_enter_ms;
|
||||
const int DUR_EXIT = transition_config_.intro_text_exit_ms;
|
||||
// Panel primer (queda sota el text gràcies a l'ordre de blits).
|
||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||
computeRoomTextPanelHeight(2, 3, 4, DUR_ENTER, DUR_EXIT));
|
||||
computeRoomTextPanelHeight(PHASE, ELAPSED, 2, 3, 4, DUR_ENTER, DUR_EXIT));
|
||||
|
||||
const auto& mgr = Levels::LevelManager::get();
|
||||
const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid());
|
||||
@@ -950,12 +976,16 @@ void GameScene::renderRoomText() {
|
||||
// Direccions oposades: piràmide entra per l'esquerra i surt per
|
||||
// la dreta; sala fa el contrari.
|
||||
const int X_PYRAMID = computeRoomTextX(room_text_font_->length(LINE_PYRAMID, KERNING),
|
||||
PHASE,
|
||||
ELAPSED,
|
||||
2,
|
||||
4,
|
||||
DUR_ENTER,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/false);
|
||||
const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING),
|
||||
PHASE,
|
||||
ELAPSED,
|
||||
2,
|
||||
4,
|
||||
DUR_ENTER,
|
||||
@@ -967,21 +997,25 @@ void GameScene::renderRoomText() {
|
||||
const int GAP = transition_config_.intro_text_line_gap_px;
|
||||
drawRoomTextLine(LINE_PYRAMID, X_PYRAMID, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
||||
drawRoomTextLine(LINE_ROOM, X_ROOM, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
||||
} else if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase >= 0 && transition_.phase <= 2) {
|
||||
} else if (state_ == RoomFsm::State::ROOM_OUTRO && PHASE >= 0 && PHASE <= 2) {
|
||||
const int DUR_ENTER = transition_config_.outro_text_enter_ms;
|
||||
const int DUR_EXIT = transition_config_.outro_text_exit_ms;
|
||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||
computeRoomTextPanelHeight(0, 1, 2, DUR_ENTER, DUR_EXIT));
|
||||
computeRoomTextPanelHeight(PHASE, ELAPSED, 0, 1, 2, DUR_ENTER, DUR_EXIT));
|
||||
|
||||
const std::string LINE_1 = Locale::get().text("game.room_complete_line1");
|
||||
const std::string LINE_2 = Locale::get().text("game.room_complete_line2");
|
||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
||||
PHASE,
|
||||
ELAPSED,
|
||||
0,
|
||||
2,
|
||||
DUR_ENTER,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/false);
|
||||
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
||||
PHASE,
|
||||
ELAPSED,
|
||||
0,
|
||||
2,
|
||||
DUR_ENTER,
|
||||
@@ -993,7 +1027,7 @@ void GameScene::renderRoomText() {
|
||||
}
|
||||
}
|
||||
|
||||
auto GameScene::computeRoomTextX(int width, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int {
|
||||
auto GameScene::computeRoomTextX(int width, int phase, int elapsed_ms, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int {
|
||||
// X animada d'una línia: entra des de fora de pantalla, s'atura al
|
||||
// CENTER_X durant el hold (fora del rang d'aquesta funció — el caller
|
||||
// garanteix que cau a la branca return final) i surt pel costat
|
||||
@@ -1003,16 +1037,16 @@ auto GameScene::computeRoomTextX(int width, int phase_enter, int phase_exit, int
|
||||
const int OFF_RIGHT = GameCanvas::WIDTH;
|
||||
const int OFF_ENTER = from_right ? OFF_RIGHT : OFF_LEFT;
|
||||
const int OFF_EXIT = from_right ? OFF_LEFT : OFF_RIGHT;
|
||||
if (transition_.phase == phase_enter) {
|
||||
if (phase == phase_enter) {
|
||||
const float T = (dur_enter > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||
? std::clamp(static_cast<float>(elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
||||
return Easing::lerpInt(OFF_ENTER, GameCanvas::CENTER_X, CURVE(T));
|
||||
}
|
||||
if (transition_.phase == phase_exit) {
|
||||
if (phase == phase_exit) {
|
||||
const float T = (dur_exit > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||
? std::clamp(static_cast<float>(elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
||||
return Easing::lerpInt(GameCanvas::CENTER_X, OFF_EXIT, CURVE(T));
|
||||
@@ -1020,23 +1054,23 @@ auto GameScene::computeRoomTextX(int width, int phase_enter, int phase_exit, int
|
||||
return GameCanvas::CENTER_X;
|
||||
}
|
||||
|
||||
auto GameScene::computeRoomTextPanelHeight(int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int {
|
||||
auto GameScene::computeRoomTextPanelHeight(int phase, int elapsed_ms, int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int {
|
||||
// Altura animada del panel translúcid sota el rètol. Creix de 0 a
|
||||
// `text_panel_height_px` amb `text_enter_curve` durant l'enter, es
|
||||
// manté plena durant el hold i decreix amb `text_exit_curve`
|
||||
// durant l'exit. Fora d'aquestes tres fases retorna 0.
|
||||
const int MAX = transition_config_.text_panel_height_px;
|
||||
if (transition_.phase == phase_enter) {
|
||||
if (phase == phase_enter) {
|
||||
const float T = (dur_enter > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||
? std::clamp(static_cast<float>(elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
||||
return Easing::lerpInt(0, MAX, CURVE(T));
|
||||
}
|
||||
if (transition_.phase == phase_hold) { return MAX; }
|
||||
if (transition_.phase == phase_exit) {
|
||||
if (phase == phase_hold) { return MAX; }
|
||||
if (phase == phase_exit) {
|
||||
const float T = (dur_exit > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||
? std::clamp(static_cast<float>(elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
||||
return Easing::lerpInt(MAX, 0, CURVE(T));
|
||||
@@ -1069,6 +1103,158 @@ void GameScene::drawRoomTextLine(const std::string& text, int x, int y, std::uin
|
||||
outline_color);
|
||||
}
|
||||
|
||||
void GameScene::setGameOverState(GameOverFsm::State new_state) {
|
||||
if (new_state == game_over_state_) { return; }
|
||||
Log::info("GAMEOVER", "{} -> {}", GameOverFsm::stateName(game_over_state_), GameOverFsm::stateName(new_state));
|
||||
game_over_state_ = new_state;
|
||||
game_over_transition_ = {};
|
||||
if (new_state == GameOverFsm::State::FADE_OUT) {
|
||||
// Arma el fade en el moment que entrem a la fase de fade-out.
|
||||
// Mateix patró que `outro_fade_started_` per a OUTRO de sala.
|
||||
game_over_fade_.begin(game_over_config_.fade_out_ms,
|
||||
Transitions::FadeToBlack::Direction::FADE_OUT);
|
||||
game_over_fade_started_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::updateGameOverFlow(int delta_ms) {
|
||||
// Tick del fade visual: és no-op si encara no s'ha armat (begin
|
||||
// sense duration → tick=no-op gràcies al clamp intern).
|
||||
game_over_fade_.tick(delta_ms);
|
||||
|
||||
// Detecció d'entrada a CONTINUE_PROMPT: tots els jugadors fora de
|
||||
// partida durant PLAYING. Només es dispara una vegada — `setGameOverState`
|
||||
// és idempotent davant del mateix estat. Es vol que la transició
|
||||
// sigui exclusiva de PLAYING perquè un join mid-INTRO/OUTRO ja no
|
||||
// és possible (vegeu `handleJoinInput`); permetent CONTINUE? fora
|
||||
// de PLAYING obriríem races amb la cinemàtica de sala.
|
||||
if (game_over_state_ == GameOverFsm::State::OFF && state_ == RoomFsm::State::PLAYING && activePlayerCount() == 0) {
|
||||
setGameOverState(GameOverFsm::State::CONTINUE_PROMPT);
|
||||
return;
|
||||
}
|
||||
if (game_over_state_ == GameOverFsm::State::OFF) { return; }
|
||||
|
||||
const auto NEXT = GameOverFsm::advance(game_over_state_, game_over_transition_, game_over_config_, delta_ms);
|
||||
if (NEXT != game_over_state_) {
|
||||
setGameOverState(NEXT);
|
||||
// En arribar a DONE, demanem la transició a LOGO. La construcció
|
||||
// de la nova escena la fa el Director llegint `exit_request_`.
|
||||
if (NEXT == GameOverFsm::State::DONE) {
|
||||
exit_request_ = ExitRequest{.next = SceneId::LOGO, .params = LogoParams{}};
|
||||
Log::info("GAMEOVER", "DONE -> requesting LOGO scene");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::renderGameOverText() {
|
||||
if (!room_text_font_) { return; }
|
||||
if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT && game_over_state_ != GameOverFsm::State::GAME_OVER) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr int KERNING = 0;
|
||||
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
||||
const Uint8 FILL = UiConfig::get().roomText();
|
||||
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
||||
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
||||
const int PHASE = game_over_transition_.phase;
|
||||
const int ELAPSED = game_over_transition_.elapsed_ms;
|
||||
|
||||
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
|
||||
const int DUR_ENTER = game_over_config_.continue_text_enter_ms;
|
||||
const int DUR_EXIT = game_over_config_.continue_text_exit_ms;
|
||||
// Phases del CONTINUE_PROMPT: 0=enter, 1=hold, 2=exit. El panel
|
||||
// segueix la mateixa corba enter/hold/exit que el room text.
|
||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||
computeRoomTextPanelHeight(PHASE, ELAPSED, 0, 1, 2, DUR_ENTER, DUR_EXIT));
|
||||
|
||||
const std::string LINE_1 = Locale::get().text("game.continue_prompt");
|
||||
const int SECONDS = GameOverFsm::countdownSecondsRemaining(
|
||||
game_over_state_,
|
||||
game_over_transition_,
|
||||
game_over_config_);
|
||||
// Línia 2 només té text durant la fase de hold (countdown). En
|
||||
// les fases d'enter i exit deixem només el "CONTINUE?" perquè
|
||||
// l'animació dels números no quedi rara.
|
||||
const std::string LINE_2 = (PHASE == 1) ? std::to_string(SECONDS) : std::string{};
|
||||
|
||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
||||
PHASE,
|
||||
ELAPSED,
|
||||
0,
|
||||
2,
|
||||
DUR_ENTER,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/false);
|
||||
const int X_2 = computeRoomTextX(LINE_2.empty() ? 0 : room_text_font_->length(LINE_2, KERNING),
|
||||
PHASE,
|
||||
ELAPSED,
|
||||
0,
|
||||
2,
|
||||
DUR_ENTER,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/true);
|
||||
const int GAP = transition_config_.intro_text_line_gap_px;
|
||||
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
||||
if (!LINE_2.empty()) {
|
||||
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// GAME_OVER: panel sempre amunt durant phase 0/1, decreix en phase 1
|
||||
// (exit). Phase 2 (wait) sense panel ni text — només temps mort
|
||||
// abans del fade.
|
||||
const int DUR_HOLD = game_over_config_.game_over_text_hold_ms;
|
||||
const int DUR_EXIT = game_over_config_.game_over_text_exit_ms;
|
||||
if (PHASE > 1) { return; }
|
||||
|
||||
// Panel: phase 0 = ple, phase 1 = decreix. Reaprofitem
|
||||
// computeRoomTextPanelHeight passant un phase_enter "fora de rang"
|
||||
// (-1) perquè la branca d'enter no s'activi mai.
|
||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||
computeRoomTextPanelHeight(PHASE, ELAPSED, /*phase_enter=*/-1,
|
||||
/*phase_hold=*/0,
|
||||
/*phase_exit=*/1,
|
||||
/*dur_enter=*/0,
|
||||
DUR_EXIT));
|
||||
|
||||
const std::string LINE_1 = Locale::get().text("game.game_over_line1");
|
||||
const std::string LINE_2 = Locale::get().text("game.game_over_line2");
|
||||
// Phase 0 (hold): text quiet al CENTER_X. Phase 1 (exit): el text
|
||||
// marxa pels costats com el room outro. Reaprofitem computeRoomTextX
|
||||
// amb phase_enter=-1 (no enter anim) i phase_exit=1.
|
||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
||||
PHASE,
|
||||
ELAPSED,
|
||||
/*phase_enter=*/-1,
|
||||
/*phase_exit=*/1,
|
||||
/*dur_enter=*/0,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/false);
|
||||
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
||||
PHASE,
|
||||
ELAPSED,
|
||||
/*phase_enter=*/-1,
|
||||
/*phase_exit=*/1,
|
||||
/*dur_enter=*/0,
|
||||
DUR_EXIT,
|
||||
/*from_right=*/true);
|
||||
const int GAP = transition_config_.outro_text_line_gap_px;
|
||||
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
||||
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
||||
|
||||
(void)DUR_HOLD; // Reservat per a futurs efectes de hold (pulse, etc.).
|
||||
}
|
||||
|
||||
void GameScene::renderGameOverFade() {
|
||||
// Sense `begin` previ, `tick` és no-op i `render` pinta alpha=0
|
||||
// (vegeu `Transitions::FadeToBlack`). Així el callsite és
|
||||
// incondicional sense malbaratar pixel ops fora del flux.
|
||||
if (!game_over_fade_started_) { return; }
|
||||
game_over_fade_.render();
|
||||
}
|
||||
|
||||
void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
||||
// Mida del sprite de Dust llegida del seu YAML. Hardcoded per
|
||||
// simplificar — si canvia, només cal tocar dust.yaml + aquesta
|
||||
@@ -1241,6 +1427,12 @@ void GameScene::update(int delta_ms) {
|
||||
// arrenque ja sigui efectiu en el mateix frame.
|
||||
updateRoomTransition(delta_ms);
|
||||
|
||||
// Sub-FSM de game over (CONTINUE? + GAME OVER + fade-to-LOGO).
|
||||
// També en temps real: el countdown de CONTINUE? no es congela
|
||||
// amb la pausa de gameplay. La detecció de "tots fora de partida"
|
||||
// s'arma dins quan estem a PLAYING.
|
||||
updateGameOverFlow(delta_ms);
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Toggle de l'editor: només si no hi ha cap transició de pausa ni
|
||||
// pausa consolidada. L'editor mateix no consumeix la tecla — la
|
||||
@@ -1469,6 +1661,14 @@ void GameScene::render() {
|
||||
// Text de la transició de sala (entrada o sortida): per damunt del
|
||||
// HUD perquè és UI de transició i ha de ser sempre llegible.
|
||||
renderRoomText();
|
||||
|
||||
// Rètol de game over (CONTINUE? + countdown / GAME OVER) — comparteix
|
||||
// panel i font amb el room text però viu en una sub-FSM independent.
|
||||
renderGameOverText();
|
||||
|
||||
// Fade-to-black final del game over: tapeix tot el canvas (HUD
|
||||
// inclòs) durant la fase FADE_OUT i el black hold posterior.
|
||||
renderGameOverFade();
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
Reference in new issue
Block a user