#include "game/scenes/game_over_fsm.hpp" #include #include #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(); } } // 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(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_start = readChildInt(n, "continue_count_start", cfg.continue_count_start); 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); if (n.contains("continue_music_speed")) { cfg.continue_music_speed = n["continue_music_speed"].get_value(); } if (n.contains("game_over_music_track")) { cfg.game_over_music_track = n["game_over_music_track"].get_value(); } if (n.contains("countdown_sound_key")) { cfg.countdown_sound_key = n["countdown_sound_key"].get_value(); } } 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 DURS = { cfg.continue_text_enter_ms, cfg.continue_text_hold_ms, cfg.continue_text_exit_ms, }; while (t.phase < static_cast(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(DURS.size())) { return State::OFF; } return current; } if (current == State::GAME_OVER) { const std::array DURS = { cfg.game_over_text_hold_ms, cfg.game_over_text_exit_ms, cfg.game_over_wait_ms, }; while (t.phase < static_cast(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) { t.elapsed_ms -= DURS[t.phase]; ++t.phase; } if (t.phase >= static_cast(DURS.size())) { return State::FADE_OUT; } return current; } // FADE_OUT const std::array DURS = { cfg.fade_out_ms, cfg.black_hold_ms, }; while (t.phase < static_cast(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) { t.elapsed_ms -= DURS[t.phase]; ++t.phase; } if (t.phase >= static_cast(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; } // La sèrie és [start, start-1, ..., 1, 0]: en total `start+1` // graons. El primer número visible és `start` i l'últim 0 es // manté durant el seu graó abans que la fase 1 acabi i la FSM // salti a GAME_OVER. Si `start == 0` només es mostra "0" // durant tot el hold. const int START = std::max(0, cfg.continue_count_start); const int STEP_COUNT = START + 1; const int STEP_MS = std::max(1, cfg.continue_text_hold_ms / STEP_COUNT); const int CONSUMED = t.elapsed_ms / STEP_MS; return std::clamp(START - CONSUMED, 0, START); } 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