#include "game/scenes/room_fsm.hpp" #include #include #include #include "core/locale/locale.hpp" #include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" namespace { // Llegeix un sub-camp sencer d'un node YAML. Si manca, retorna el // fallback. Dissenyat per al consum puntual de `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 RoomFsm { auto loadConfig(const std::string& yaml_path) -> Config { Config cfg{}; cfg.text_enter_curve = UiConfig::curveByName("outCubic"); cfg.text_exit_curve = UiConfig::curveByName("inCubic"); auto bytes = Resource::Helper::loadFile(yaml_path); if (bytes.empty()) { std::cout << "RoomFsm: " << yaml_path << " absent — uso fallbacks compilats\n"; 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("room_intro")) { const auto& n = yaml["room_intro"]; cfg.intro_fade_in_ms = readChildInt(n, "fade_in_ms", cfg.intro_fade_in_ms); cfg.intro_text_delay_ms = readChildInt(n, "text_delay_ms", cfg.intro_text_delay_ms); cfg.intro_text_enter_ms = readChildInt(n, "text_enter_ms", cfg.intro_text_enter_ms); cfg.intro_text_hold_ms = readChildInt(n, "text_hold_ms", cfg.intro_text_hold_ms); cfg.intro_text_exit_ms = readChildInt(n, "text_exit_ms", cfg.intro_text_exit_ms); cfg.player_spawn_delay_ms = readChildInt(n, "player_spawn_delay_ms", cfg.player_spawn_delay_ms); cfg.intro_text_line_gap_px = readChildInt(n, "line_gap_px", cfg.intro_text_line_gap_px); } if (yaml.contains("room_outro")) { const auto& n = yaml["room_outro"]; cfg.outro_text_enter_ms = readChildInt(n, "text_enter_ms", cfg.outro_text_enter_ms); cfg.outro_text_hold_ms = readChildInt(n, "text_hold_ms", cfg.outro_text_hold_ms); cfg.outro_text_exit_ms = readChildInt(n, "text_exit_ms", cfg.outro_text_exit_ms); cfg.outro_fade_out_ms = readChildInt(n, "fade_out_ms", cfg.outro_fade_out_ms); cfg.outro_text_line_gap_px = readChildInt(n, "line_gap_px", cfg.outro_text_line_gap_px); cfg.outro_black_hold_ms = readChildInt(n, "black_hold_ms", cfg.outro_black_hold_ms); } if (yaml.contains("text")) { const auto& n = yaml["text"]; if (n.contains("enter_curve")) { cfg.text_enter_curve = UiConfig::curveByName(n["enter_curve"].get_value()); } if (n.contains("exit_curve")) { cfg.text_exit_curve = UiConfig::curveByName(n["exit_curve"].get_value()); } } std::cout << "RoomFsm: timings carregats des de " << yaml_path << " (intro fade " << cfg.intro_fade_in_ms << "ms, outro fade " << cfg.outro_fade_out_ms << "ms)\n"; return cfg; } auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State { if (current == State::PLAYING) { return current; } t.elapsed_ms += delta_ms; if (current == State::ROOM_INTRO) { const std::array DURS = { cfg.intro_fade_in_ms, cfg.intro_text_delay_ms, cfg.intro_text_enter_ms, cfg.intro_text_hold_ms, cfg.intro_text_exit_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::PLAYING; } return current; } // ROOM_OUTRO const std::array DURS = { cfg.outro_text_enter_ms, cfg.outro_text_hold_ms, cfg.outro_text_exit_ms, cfg.outro_fade_out_ms, // Pausa final en negre, després del fade-out i abans del // canvi a INTRO. Si és 0 ms, el while-loop la consumeix // sense esperar — comportament idèntic a l'històric. cfg.outro_black_hold_ms, }; while (t.phase < static_cast(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) { t.elapsed_ms -= DURS[t.phase]; ++t.phase; } // Final de l'OUTRO → ROOM_INTRO de la sala SEGÜENT. La FSM no // sap quina és — només avisa del canvi d'estat. La GameScene, // dins `setState(ROOM_INTRO)` amb PREV=OUTRO, demana al // LevelManager el següent nivell i recarrega tilemap/entitats. if (t.phase >= static_cast(DURS.size())) { return State::ROOM_INTRO; } return current; } namespace { // Substitueix la primera ocurrència de `placeholder` dins de `tpl` // pel valor d'`n` convertit a string. Utilitzat per les dues // formatadores d'INTRO; si la clau del Locale no porta el // placeholder (traducció buida o ja substituïda), retorna la // plantilla intacta. auto substitute(std::string tpl, std::string_view placeholder, int n) -> std::string { auto pos = tpl.find(placeholder); if (pos != std::string::npos) { tpl.replace(pos, placeholder.size(), std::to_string(n)); } return tpl; } } // namespace auto formatIntroPyramidLine(int pyramid) -> std::string { return substitute(Locale::get().text("game.room_intro_pyramid"), "{p}", pyramid); } auto formatIntroRoomLine(int room) -> std::string { return substitute(Locale::get().text("game.room_intro_room"), "{r}", room); } auto stateName(State s) -> const char* { switch (s) { case State::ROOM_INTRO: return "ROOM_INTRO"; case State::PLAYING: return "PLAYING"; case State::ROOM_OUTRO: return "ROOM_OUTRO"; } return "?"; } } // namespace RoomFsm