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
@@ -119,6 +119,7 @@ set(APP_SOURCES
|
|||||||
source/game/scenes/intro_scene.cpp
|
source/game/scenes/intro_scene.cpp
|
||||||
source/game/scenes/game_scene.cpp
|
source/game/scenes/game_scene.cpp
|
||||||
source/game/scenes/room_fsm.cpp
|
source/game/scenes/room_fsm.cpp
|
||||||
|
source/game/scenes/game_over_fsm.cpp
|
||||||
source/game/entities/player.cpp
|
source/game/entities/player.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
source/game/map/tilemap.cpp
|
source/game/map/tilemap.cpp
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ game:
|
|||||||
room_intro_room: "Sala {r}"
|
room_intro_room: "Sala {r}"
|
||||||
room_complete_line1: "Fase"
|
room_complete_line1: "Fase"
|
||||||
room_complete_line2: "Completada"
|
room_complete_line2: "Completada"
|
||||||
|
# Rètol del game over (sub-FSM de la GameScene). El countdown es
|
||||||
|
# pinta com a segona línia (un enter) durant la fase 1 de
|
||||||
|
# CONTINUE_PROMPT — només cal traduir el rètol superior.
|
||||||
|
continue_prompt: "Continuar?"
|
||||||
|
game_over_line1: "Fi del"
|
||||||
|
game_over_line2: "Joc"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ game:
|
|||||||
room_intro_room: "Room {r}"
|
room_intro_room: "Room {r}"
|
||||||
room_complete_line1: "Stage"
|
room_complete_line1: "Stage"
|
||||||
room_complete_line2: "Complete"
|
room_complete_line2: "Complete"
|
||||||
|
# Game over labels (sub-FSM in GameScene). The countdown is drawn as
|
||||||
|
# a numeric second line during CONTINUE_PROMPT phase 1, so only the
|
||||||
|
# top label needs translation.
|
||||||
|
continue_prompt: "Continue?"
|
||||||
|
game_over_line1: "Game"
|
||||||
|
game_over_line2: "Over"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
|
|||||||
@@ -46,3 +46,25 @@ text_panel:
|
|||||||
color: [36, 182, 170]
|
color: [36, 182, 170]
|
||||||
alpha: 128
|
alpha: 128
|
||||||
height_px: 32
|
height_px: 32
|
||||||
|
|
||||||
|
# Sub-FSM de game over: dispara CONTINUE? quan tots els jugadors
|
||||||
|
# queden fora de partida durant PLAYING, fa countdown, i si ningú
|
||||||
|
# pulsa START acaba en GAME OVER + fade-to-black + transició a LOGO.
|
||||||
|
# Tots els valors són ms (excepte `continue_count_steps`, que és el
|
||||||
|
# nombre de tics visibles del countdown). Si manca el fitxer o alguna
|
||||||
|
# clau, GameScene cau als fallbacks compilats (vegeu `GameOverFsm::Config`).
|
||||||
|
game_over:
|
||||||
|
# CONTINUE? — animació d'entrada i sortida idèntica al room text.
|
||||||
|
continue_text_enter_ms: 400
|
||||||
|
continue_text_hold_ms: 5000 # 5 s = `continue_count_steps` graons d'1 s
|
||||||
|
continue_text_exit_ms: 400
|
||||||
|
continue_count_steps: 5
|
||||||
|
# GAME OVER — apareix sense enter anim (panel ja amunt del CONTINUE?),
|
||||||
|
# es manté un temps i marxa amb la mateixa exit anim del room text.
|
||||||
|
game_over_text_hold_ms: 1500
|
||||||
|
game_over_text_exit_ms: 400
|
||||||
|
game_over_wait_ms: 400
|
||||||
|
# Fade final cap a LOGO. `black_hold_ms` és la pausa en negre opac
|
||||||
|
# abans que el GameScene demani la transició d'escena.
|
||||||
|
fade_out_ms: 500
|
||||||
|
black_hold_ms: 500
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#include "game/scenes/game_over_fsm.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Llegeix un sub-camp sencer d'un node YAML. Si manca, retorna el
|
||||||
|
// fallback. Mateix patró que `RoomFsm::loadConfig`.
|
||||||
|
auto readChildInt(const fkyaml::node& parent, const char* key, int fallback) -> int {
|
||||||
|
if (!parent.contains(key)) { return fallback; }
|
||||||
|
return parent[key].get_value<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace GameOverFsm {
|
||||||
|
|
||||||
|
auto loadConfig(const std::string& yaml_path) -> Config {
|
||||||
|
Config cfg{};
|
||||||
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
Log::warn("GameOverFsm", "{} absent — uso fallbacks compilats", yaml_path);
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||||
|
const auto* end = begin + bytes.size();
|
||||||
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
|
|
||||||
|
if (yaml.contains("game_over")) {
|
||||||
|
const auto& n = yaml["game_over"];
|
||||||
|
cfg.continue_text_enter_ms = readChildInt(n, "continue_text_enter_ms", cfg.continue_text_enter_ms);
|
||||||
|
cfg.continue_text_hold_ms = readChildInt(n, "continue_text_hold_ms", cfg.continue_text_hold_ms);
|
||||||
|
cfg.continue_text_exit_ms = readChildInt(n, "continue_text_exit_ms", cfg.continue_text_exit_ms);
|
||||||
|
cfg.continue_count_steps = readChildInt(n, "continue_count_steps", cfg.continue_count_steps);
|
||||||
|
cfg.game_over_text_hold_ms = readChildInt(n, "game_over_text_hold_ms", cfg.game_over_text_hold_ms);
|
||||||
|
cfg.game_over_text_exit_ms = readChildInt(n, "game_over_text_exit_ms", cfg.game_over_text_exit_ms);
|
||||||
|
cfg.game_over_wait_ms = readChildInt(n, "game_over_wait_ms", cfg.game_over_wait_ms);
|
||||||
|
cfg.fade_out_ms = readChildInt(n, "fade_out_ms", cfg.fade_out_ms);
|
||||||
|
cfg.black_hold_ms = readChildInt(n, "black_hold_ms", cfg.black_hold_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("GameOverFsm", "timings carregats des de {} (continue hold {}ms, fade {}ms)", yaml_path, cfg.continue_text_hold_ms, cfg.fade_out_ms);
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State {
|
||||||
|
if (current == State::OFF || current == State::DONE) { return current; }
|
||||||
|
|
||||||
|
t.elapsed_ms += delta_ms;
|
||||||
|
|
||||||
|
if (current == State::CONTINUE_PROMPT) {
|
||||||
|
const std::array<int, 3> DURS = {
|
||||||
|
cfg.continue_text_enter_ms,
|
||||||
|
cfg.continue_text_hold_ms,
|
||||||
|
cfg.continue_text_exit_ms,
|
||||||
|
};
|
||||||
|
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||||
|
t.elapsed_ms -= DURS[t.phase];
|
||||||
|
// Si acabem la fase 1 (hold) sense cancel·lació, saltem
|
||||||
|
// directament a GAME_OVER — el panel queda amunt i no
|
||||||
|
// es reprodueix l'exit anim. La fase 2 (exit) només
|
||||||
|
// s'executa si el GameScene l'ha forçada amb
|
||||||
|
// `cancelContinuePrompt`.
|
||||||
|
if (t.phase == 1) {
|
||||||
|
return State::GAME_OVER;
|
||||||
|
}
|
||||||
|
++t.phase;
|
||||||
|
}
|
||||||
|
if (t.phase >= static_cast<int>(DURS.size())) { return State::OFF; }
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == State::GAME_OVER) {
|
||||||
|
const std::array<int, 3> DURS = {
|
||||||
|
cfg.game_over_text_hold_ms,
|
||||||
|
cfg.game_over_text_exit_ms,
|
||||||
|
cfg.game_over_wait_ms,
|
||||||
|
};
|
||||||
|
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||||
|
t.elapsed_ms -= DURS[t.phase];
|
||||||
|
++t.phase;
|
||||||
|
}
|
||||||
|
if (t.phase >= static_cast<int>(DURS.size())) { return State::FADE_OUT; }
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FADE_OUT
|
||||||
|
const std::array<int, 2> DURS = {
|
||||||
|
cfg.fade_out_ms,
|
||||||
|
cfg.black_hold_ms,
|
||||||
|
};
|
||||||
|
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||||
|
t.elapsed_ms -= DURS[t.phase];
|
||||||
|
++t.phase;
|
||||||
|
}
|
||||||
|
if (t.phase >= static_cast<int>(DURS.size())) { return State::DONE; }
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelContinuePrompt(State current, Transition& t) {
|
||||||
|
if (current != State::CONTINUE_PROMPT) { return; }
|
||||||
|
if (t.phase >= 2) { return; } // ja en exit anim, no-op
|
||||||
|
t.phase = 2;
|
||||||
|
t.elapsed_ms = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int {
|
||||||
|
if (current != State::CONTINUE_PROMPT) { return 0; }
|
||||||
|
if (t.phase != 1) { return 0; }
|
||||||
|
const int STEPS = std::max(1, cfg.continue_count_steps);
|
||||||
|
const int STEP_MS = std::max(1, cfg.continue_text_hold_ms / STEPS);
|
||||||
|
const int CONSUMED = t.elapsed_ms / STEP_MS;
|
||||||
|
return std::clamp(STEPS - CONSUMED, 0, STEPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stateName(State s) -> const char* {
|
||||||
|
switch (s) {
|
||||||
|
case State::OFF:
|
||||||
|
return "OFF";
|
||||||
|
case State::CONTINUE_PROMPT:
|
||||||
|
return "CONTINUE_PROMPT";
|
||||||
|
case State::GAME_OVER:
|
||||||
|
return "GAME_OVER";
|
||||||
|
case State::FADE_OUT:
|
||||||
|
return "FADE_OUT";
|
||||||
|
case State::DONE:
|
||||||
|
return "DONE";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace GameOverFsm
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// FSM secundària de la GameScene que governa el flux "tots els jugadors
|
||||||
|
// fora de partida" — CONTINUE? amb countdown → GAME OVER → fade-to-black
|
||||||
|
// → transició a LOGO. Viu separada de la `RoomFsm` (que gestiona
|
||||||
|
// intro/outro de cada sala) perquè els seus disparadors són ortogonals
|
||||||
|
// i la composició entre les dues queda al GameScene.
|
||||||
|
//
|
||||||
|
// Lògica pura: no toca SDL ni Audio, només temporitzadors. La part
|
||||||
|
// visual (text + panel + fade) viu al GameScene reaprofitant la mateixa
|
||||||
|
// infraestructura del room text.
|
||||||
|
namespace GameOverFsm {
|
||||||
|
|
||||||
|
// Estats canònics. Phase indices per estat:
|
||||||
|
// OFF — joc normal; el GameScene transita a
|
||||||
|
// CONTINUE_PROMPT quan detecta 0 jugadors actius.
|
||||||
|
// CONTINUE_PROMPT — phases: 0=text_enter, 1=text_hold (countdown
|
||||||
|
// visible), 2=text_exit (només si l'usuari
|
||||||
|
// cancel·la pulsant START). Si la fase 1 acaba
|
||||||
|
// sense cancel·lació → salt a GAME_OVER amb el
|
||||||
|
// panel encara amunt (sense exit anim).
|
||||||
|
// GAME_OVER — phases: 0=text_hold (apareix sense enter
|
||||||
|
// anim, panel ja amunt), 1=text_exit, 2=wait
|
||||||
|
// (otro tiempo más abans del fade).
|
||||||
|
// FADE_OUT — phases: 0=fade, 1=black_hold (perquè el
|
||||||
|
// negre quedi un instant abans de cedir el
|
||||||
|
// pas a la LogoScene).
|
||||||
|
// DONE — sentinel: el caller (GameScene) ha de
|
||||||
|
// demanar la transició cap a LOGO.
|
||||||
|
enum class State : std::uint8_t {
|
||||||
|
OFF,
|
||||||
|
CONTINUE_PROMPT,
|
||||||
|
GAME_OVER,
|
||||||
|
FADE_OUT,
|
||||||
|
DONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sub-fase i temps acumulat dins de l'estat actual. Es reseteja a
|
||||||
|
// {0, 0} quan es canvia d'estat (`GameScene::setGameOverState`).
|
||||||
|
struct Transition {
|
||||||
|
int phase{0};
|
||||||
|
int elapsed_ms{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Timings llegits de `data/scenes/game/game.yaml` (secció
|
||||||
|
// `game_over:`). Si el fitxer no porta cap d'aquests camps, els
|
||||||
|
// valors per defecte coincideixen amb els del .yaml versionat.
|
||||||
|
struct Config {
|
||||||
|
// CONTINUE? — animació d'entrada/sortida idèntica al room text.
|
||||||
|
int continue_text_enter_ms{500};
|
||||||
|
// Hold = duració total del countdown (es divideix en
|
||||||
|
// `continue_count_steps` graons d'1 segon visualment).
|
||||||
|
int continue_text_hold_ms{5000};
|
||||||
|
int continue_text_exit_ms{500};
|
||||||
|
// Quants "tics" mostra el countdown de CONTINUE?. 5 → "5,4,3,2,1".
|
||||||
|
int continue_count_steps{5};
|
||||||
|
|
||||||
|
// GAME OVER — apareix sense enter anim (el panel ja és amunt
|
||||||
|
// del CONTINUE?) i s'esvaeix amb la mateixa exit anim que el
|
||||||
|
// room text. `wait_ms` és el "otro tiempo más" abans del fade.
|
||||||
|
int game_over_text_hold_ms{2000};
|
||||||
|
int game_over_text_exit_ms{500};
|
||||||
|
int game_over_wait_ms{500};
|
||||||
|
|
||||||
|
// Fade final cap a LOGO. `black_hold_ms` és el temps amb el
|
||||||
|
// canvas negre opac abans que el GameScene demani la transició.
|
||||||
|
int fade_out_ms{500};
|
||||||
|
int black_hold_ms{500};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Llegeix la configuració des del YAML. Si el fitxer manca o falta
|
||||||
|
// alguna clau, els camps no presents conserven els seus fallbacks.
|
||||||
|
[[nodiscard]] auto loadConfig(const std::string& yaml_path) -> Config;
|
||||||
|
|
||||||
|
// Avança la transició consumint `delta_ms` des del temps real.
|
||||||
|
// Retorna l'estat resultant. Si retorna un estat diferent, el
|
||||||
|
// caller ha de resetejar la `Transition` a {0, 0} abans del següent
|
||||||
|
// frame. Funció pura sobre l'argument.
|
||||||
|
//
|
||||||
|
// Particularitats:
|
||||||
|
// - CONTINUE_PROMPT phase 1 (hold) arribar a la fi → GAME_OVER
|
||||||
|
// (NO reprodueix exit anim; el caller ja té el panel amunt).
|
||||||
|
// - CONTINUE_PROMPT phase 2 (exit) arribar a la fi → OFF (cas
|
||||||
|
// "cancel·lació via START": el GameScene ha forçat la fase 2
|
||||||
|
// amb `cancelContinuePrompt`).
|
||||||
|
// - GAME_OVER phase 2 (wait) arribar a la fi → FADE_OUT.
|
||||||
|
// - FADE_OUT phase 1 (black_hold) arribar a la fi → DONE.
|
||||||
|
// - DONE i OFF no avancen ni canvien d'estat (sumiders).
|
||||||
|
[[nodiscard]] auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State;
|
||||||
|
|
||||||
|
// Salta a la fase 2 (text_exit) del CONTINUE_PROMPT. No fa res si
|
||||||
|
// l'estat no és CONTINUE_PROMPT o ja som a la fase 2 (idempotent).
|
||||||
|
// Disparat pel GameScene quan un jugador pulsa START dins del
|
||||||
|
// countdown — el join real també el fa GameScene; aquesta funció
|
||||||
|
// només dispara la sortida visual del rètol.
|
||||||
|
void cancelContinuePrompt(State current, Transition& t);
|
||||||
|
|
||||||
|
// Quants segons "queden" del countdown de CONTINUE?. Retorna un
|
||||||
|
// enter en el rang [0, continue_count_steps] perquè es renderitzi
|
||||||
|
// com a segona línia del rètol durant la fase 1 (hold). Fora de
|
||||||
|
// CONTINUE_PROMPT phase 1, retorna 0 (no s'hauria de pintar).
|
||||||
|
[[nodiscard]] auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int;
|
||||||
|
|
||||||
|
// Nom textual per a logs i tests.
|
||||||
|
[[nodiscard]] auto stateName(State s) -> const char*;
|
||||||
|
|
||||||
|
} // namespace GameOverFsm
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
#include "game/levels/level_manager.hpp"
|
#include "game/levels/level_manager.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
#include "game/scenes/scene_flow.hpp"
|
||||||
#include "game/scenes/start_input.hpp"
|
#include "game/scenes/start_input.hpp"
|
||||||
#include "game/ui/hud.hpp"
|
#include "game/ui/hud.hpp"
|
||||||
#include "game/ui/service_menu.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
|
// Timings i corbes de la FSM de sala. Llegits del YAML una sola
|
||||||
// vegada; canvis requereixen reiniciar el joc.
|
// vegada; canvis requereixen reiniciar el joc.
|
||||||
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::GAME_YAML);
|
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
|
// Cinemàtica d'entrada de la primera sala: arrenca la pista
|
||||||
// `room_intro.ogg` i arma el callback de fi que farà spawn + salt
|
// `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() {
|
void GameScene::handleJoinInput() {
|
||||||
// El "join in progress" només es processa durant PLAYING. Durant
|
// El "join in progress" es processa durant PLAYING (regla 1) i,
|
||||||
// INTRO la cinemàtica de spawn ja té la seva pròpia coreografia
|
// com a cas especial, durant CONTINUE_PROMPT — ahí un START reviu
|
||||||
// (vegeu `spawnDuePlayers`); durant OUTRO el cicle de sala ja està
|
// el jugador i cancel·la el countdown. A partir de GAME_OVER el
|
||||||
// tancant. En tots dos casos el HUD mostra "ESPERA / PARA JUGAR"
|
// START queda bloquejat (regla 5: ja no es pot pulsar start).
|
||||||
// perquè quede clar que la pulsació encara no té efecte.
|
const bool IN_PLAYING = (state_ == RoomFsm::State::PLAYING);
|
||||||
if (state_ != RoomFsm::State::PLAYING) { return; }
|
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) {
|
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||||
if (players_active_[i]) { continue; }
|
if (players_active_[i]) { continue; }
|
||||||
if (!players_[i]->isDisabled()) { continue; }
|
if (!players_[i]->isDisabled()) { continue; }
|
||||||
if (!Input::Detect::pressedStart(static_cast<int>(i))) { continue; }
|
if (!Input::Detect::pressedStart(static_cast<int>(i))) { continue; }
|
||||||
joinPlayer(i);
|
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() {
|
void GameScene::refreshJoinPrompt() {
|
||||||
if (!hud_) { return; }
|
if (!hud_) { return; }
|
||||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
// El game over substitueix la decisió purament basada en RoomFsm:
|
||||||
Hud::JoinPromptMode mode = Hud::JoinPromptMode::NONE;
|
// durant CONTINUE_PROMPT els jugadors encara poden entrar (revive),
|
||||||
if (!players_active_[i]) {
|
// a partir de GAME_OVER ja no, així que mostrem WAIT (o NONE) per
|
||||||
switch (state_) {
|
// indicar que el START ja no farà res fins a la transició a LOGO.
|
||||||
case RoomFsm::State::PLAYING:
|
Hud::JoinPromptMode mode_inactive = Hud::JoinPromptMode::NONE;
|
||||||
mode = Hud::JoinPromptMode::CAN_JOIN;
|
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
|
||||||
break;
|
mode_inactive = Hud::JoinPromptMode::CAN_JOIN;
|
||||||
case RoomFsm::State::ROOM_INTRO:
|
} else if (game_over_state_ != GameOverFsm::State::OFF) {
|
||||||
case RoomFsm::State::ROOM_OUTRO:
|
mode_inactive = Hud::JoinPromptMode::WAIT;
|
||||||
mode = Hud::JoinPromptMode::WAIT;
|
} else {
|
||||||
break;
|
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 Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
||||||
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
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_ENTER = transition_config_.intro_text_enter_ms;
|
||||||
const int DUR_EXIT = transition_config_.intro_text_exit_ms;
|
const int DUR_EXIT = transition_config_.intro_text_exit_ms;
|
||||||
// Panel primer (queda sota el text gràcies a l'ordre de blits).
|
// Panel primer (queda sota el text gràcies a l'ordre de blits).
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
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 auto& mgr = Levels::LevelManager::get();
|
||||||
const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid());
|
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
|
// Direccions oposades: piràmide entra per l'esquerra i surt per
|
||||||
// la dreta; sala fa el contrari.
|
// la dreta; sala fa el contrari.
|
||||||
const int X_PYRAMID = computeRoomTextX(room_text_font_->length(LINE_PYRAMID, KERNING),
|
const int X_PYRAMID = computeRoomTextX(room_text_font_->length(LINE_PYRAMID, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
2,
|
2,
|
||||||
4,
|
4,
|
||||||
DUR_ENTER,
|
DUR_ENTER,
|
||||||
DUR_EXIT,
|
DUR_EXIT,
|
||||||
/*from_right=*/false);
|
/*from_right=*/false);
|
||||||
const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING),
|
const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
2,
|
2,
|
||||||
4,
|
4,
|
||||||
DUR_ENTER,
|
DUR_ENTER,
|
||||||
@@ -967,21 +997,25 @@ void GameScene::renderRoomText() {
|
|||||||
const int GAP = transition_config_.intro_text_line_gap_px;
|
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_PYRAMID, X_PYRAMID, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
||||||
drawRoomTextLine(LINE_ROOM, X_ROOM, PLAY_CENTER_Y + (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_ENTER = transition_config_.outro_text_enter_ms;
|
||||||
const int DUR_EXIT = transition_config_.outro_text_exit_ms;
|
const int DUR_EXIT = transition_config_.outro_text_exit_ms;
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
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_1 = Locale::get().text("game.room_complete_line1");
|
||||||
const std::string LINE_2 = Locale::get().text("game.room_complete_line2");
|
const std::string LINE_2 = Locale::get().text("game.room_complete_line2");
|
||||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
0,
|
0,
|
||||||
2,
|
2,
|
||||||
DUR_ENTER,
|
DUR_ENTER,
|
||||||
DUR_EXIT,
|
DUR_EXIT,
|
||||||
/*from_right=*/false);
|
/*from_right=*/false);
|
||||||
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
||||||
|
PHASE,
|
||||||
|
ELAPSED,
|
||||||
0,
|
0,
|
||||||
2,
|
2,
|
||||||
DUR_ENTER,
|
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
|
// 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
|
// 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
|
// 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_RIGHT = GameCanvas::WIDTH;
|
||||||
const int OFF_ENTER = from_right ? OFF_RIGHT : OFF_LEFT;
|
const int OFF_ENTER = from_right ? OFF_RIGHT : OFF_LEFT;
|
||||||
const int OFF_EXIT = from_right ? OFF_LEFT : OFF_RIGHT;
|
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)
|
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;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
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));
|
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)
|
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;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
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));
|
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;
|
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
|
// 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
|
// `text_panel_height_px` amb `text_enter_curve` durant l'enter, es
|
||||||
// manté plena durant el hold i decreix amb `text_exit_curve`
|
// manté plena durant el hold i decreix amb `text_exit_curve`
|
||||||
// durant l'exit. Fora d'aquestes tres fases retorna 0.
|
// durant l'exit. Fora d'aquestes tres fases retorna 0.
|
||||||
const int MAX = transition_config_.text_panel_height_px;
|
const int MAX = transition_config_.text_panel_height_px;
|
||||||
if (transition_.phase == phase_enter) {
|
if (phase == phase_enter) {
|
||||||
const float T = (dur_enter > 0)
|
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;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
||||||
return Easing::lerpInt(0, MAX, CURVE(T));
|
return Easing::lerpInt(0, MAX, CURVE(T));
|
||||||
}
|
}
|
||||||
if (transition_.phase == phase_hold) { return MAX; }
|
if (phase == phase_hold) { return MAX; }
|
||||||
if (transition_.phase == phase_exit) {
|
if (phase == phase_exit) {
|
||||||
const float T = (dur_exit > 0)
|
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;
|
: 1.0F;
|
||||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
||||||
return Easing::lerpInt(MAX, 0, CURVE(T));
|
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);
|
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) {
|
void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
||||||
// Mida del sprite de Dust llegida del seu YAML. Hardcoded per
|
// Mida del sprite de Dust llegida del seu YAML. Hardcoded per
|
||||||
// simplificar — si canvia, només cal tocar dust.yaml + aquesta
|
// 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.
|
// arrenque ja sigui efectiu en el mateix frame.
|
||||||
updateRoomTransition(delta_ms);
|
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
|
#ifdef _DEBUG
|
||||||
// Toggle de l'editor: només si no hi ha cap transició de pausa ni
|
// 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
|
// 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
|
// Text de la transició de sala (entrada o sortida): per damunt del
|
||||||
// HUD perquè és UI de transició i ha de ser sempre llegible.
|
// HUD perquè és UI de transició i ha de ser sempre llegible.
|
||||||
renderRoomText();
|
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
|
#ifdef _DEBUG
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/map/paint_layer.hpp"
|
#include "game/map/paint_layer.hpp"
|
||||||
#include "game/map/tilemap.hpp"
|
#include "game/map/tilemap.hpp"
|
||||||
|
#include "game/scenes/game_over_fsm.hpp"
|
||||||
#include "game/scenes/room_fsm.hpp"
|
#include "game/scenes/room_fsm.hpp"
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
@@ -291,15 +292,51 @@ class GameScene : public Scene {
|
|||||||
void renderRoomFade();
|
void renderRoomFade();
|
||||||
void renderRoomText();
|
void renderRoomText();
|
||||||
|
|
||||||
// Helpers de `renderRoomText`. Tots consulten `transition_` i
|
// Configuració, estat i transició de la sub-FSM de game over
|
||||||
// `transition_config_` directament a través de `this` — així les
|
// (CONTINUE? + GAME OVER). Lligada al mateix `game.yaml` que la
|
||||||
// signatures es queden netes i la funció pública s'orquestra amb
|
// RoomFsm via `GameOverFsm::loadConfig`.
|
||||||
// poques línies. Tres tasques separades:
|
GameOverFsm::Config game_over_config_{};
|
||||||
// - X animada d'una línia (entra per un costat, surt per l'altre).
|
GameOverFsm::State game_over_state_{GameOverFsm::State::OFF};
|
||||||
// - Altura animada del panel translúcid (creix, manté, decreix).
|
GameOverFsm::Transition game_over_transition_{};
|
||||||
// - Pintat efectiu del panel i d'una línia de text.
|
|
||||||
[[nodiscard]] auto computeRoomTextX(int width, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int;
|
// Fade final cap a LOGO. Independent del `outro_fade_` perquè el
|
||||||
[[nodiscard]] auto computeRoomTextPanelHeight(int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int;
|
// game over passa per damunt de la RoomFsm i pot disparar-se durant
|
||||||
|
// PLAYING sense haver d'arrencar un OUTRO de sala. Comença a la
|
||||||
|
// fase 0 de FADE_OUT i es manté pintant alpha=255 durant el
|
||||||
|
// `black_hold` final (mateix patró que `outro_fade_`).
|
||||||
|
Transitions::FadeToBlack game_over_fade_;
|
||||||
|
bool game_over_fade_started_{false};
|
||||||
|
|
||||||
|
// Avança la sub-FSM de game over consumint `delta_ms`. Detecta la
|
||||||
|
// transició de "tots els jugadors fora de partida" durant PLAYING
|
||||||
|
// (entrada a CONTINUE_PROMPT), arma el fade de FADE_OUT i, en
|
||||||
|
// arribar a DONE, demana la transició a `LOGO`.
|
||||||
|
void updateGameOverFlow(int delta_ms);
|
||||||
|
|
||||||
|
// Substitueix `setState` per a la sub-FSM de game over: log
|
||||||
|
// d'entrada/sortida i reset de la `Transition`. També arma el
|
||||||
|
// `game_over_fade_` quan entrem a FADE_OUT, en analogia amb
|
||||||
|
// `outro_fade_`.
|
||||||
|
void setGameOverState(GameOverFsm::State new_state);
|
||||||
|
|
||||||
|
// Pintat del rètol de game over (CONTINUE? + countdown durant
|
||||||
|
// CONTINUE_PROMPT; "GAME OVER" durant GAME_OVER). Reaprofita el
|
||||||
|
// panel translúcid i la font del room text.
|
||||||
|
void renderGameOverText();
|
||||||
|
|
||||||
|
// Pintat del fade negre final (FADE_OUT i black hold). Cridat
|
||||||
|
// després del HUD perquè cobreixi tota la pantalla, inclòs el
|
||||||
|
// marcador.
|
||||||
|
void renderGameOverFade();
|
||||||
|
|
||||||
|
// Helpers d'animació del rètol de transició. Reutilitzats pel
|
||||||
|
// rètol de sala (`renderRoomText`) i pel de game over
|
||||||
|
// (`renderGameOverText`). Reben `phase` i `elapsed_ms` per
|
||||||
|
// paràmetre perquè la mateixa lògica visual serveix les dues
|
||||||
|
// sub-FSMs sense saber a quina pertanyen — només es lliguen al
|
||||||
|
// `transition_config_` per a corbes i geometria del panel.
|
||||||
|
[[nodiscard]] auto 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;
|
||||||
|
[[nodiscard]] auto computeRoomTextPanelHeight(int phase, int elapsed_ms, int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int;
|
||||||
void drawRoomTextPanel(int center_y, int panel_height) const;
|
void drawRoomTextPanel(int center_y, int panel_height) const;
|
||||||
void drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const;
|
void drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user