Files
aee-arcade/source/game/scenes/game_over_fsm.cpp
T
JailDesignerandClaude Opus 4.7 78e164fce4 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>
2026-05-08 20:30:37 +02:00

138 lines
5.4 KiB
C++

#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