#pragma once #include #include #include "core/ui/ui_config.hpp" // FSM de sala (intro/outro) extreta de GameScene per a testabilitat. La // lògica que viu aquí és pura: no toca SDL, ni Audio, ni cap singleton // excepte `Locale::get()` (per a la formatació del text). El consumidor // actual és `GameScene`, que cridarà `RoomFsm::advance` cada frame. namespace RoomFsm { enum class State : std::uint8_t { ROOM_INTRO, // Entrada de sala: fade in + text d'entrada. PLAYING, // Joc normal. ROOM_OUTRO, // Sortida de sala: text de sortida + fade out. }; // Sub-fase i temps acumulat dins de l'estat actual. Es reseteja a // {0, 0} quan es canvia d'estat. Significat de `phase` per estat: // ROOM_INTRO: 0=fade_in, 1=text_delay, 2=text_enter, 3=text_hold, // 4=text_exit, 5=music_tail // ROOM_OUTRO: 0=text_enter, 1=text_hold, 2=text_exit, 3=music_tail, // 4=fade_out, 5=black_hold // PLAYING: no s'usa. // Les fases `music_tail` són la cua silenciosa que sincronitza el // timeline visual amb el final del .ogg de música corresponent: el // GameScene les calcula a `enterRoomIntro` / `setState(ROOM_OUTRO)` // a partir de la durada precarregada del .ogg menys el sumatori de // les fases de text. Si la música no arrenca, la cua és 0 i la fase // es consumeix instantàniament (la transició no s'embussa mai). struct Transition { int phase{0}; int elapsed_ms{0}; }; // Timings i corbes carregats de `data/scenes/game/game.yaml`. Els valors // per defecte coincideixen amb els del YAML versionat: si manca el // fitxer, la FSM funciona igualment amb aquests fallbacks. struct Config { int intro_fade_in_ms{500}; int intro_text_delay_ms{500}; int intro_text_enter_ms{500}; int intro_text_hold_ms{1000}; int intro_text_exit_ms{500}; int outro_text_enter_ms{500}; int outro_text_hold_ms{1000}; int outro_text_exit_ms{500}; int outro_fade_out_ms{500}; // Pausa en negre opac entre el final de l'OUTRO i el començament // de l'INTRO de la sala següent. 0 = sense pausa (transició // immediata, comportament històric). El YAML pot sobreescriure // sense recompilar — útil per donar respir abans del nou rètol. int outro_black_hold_ms{0}; // Cua silenciosa entre el text d'INTRO/OUTRO i la fi del .ogg de // música corresponent. NO es llig del YAML — el GameScene la // calcula a `enterRoomIntro` / `setState(ROOM_OUTRO)` com // `max(0, music_duration - text_total)`. Així el timeline acaba // exactament quan la música drena i no cal cap callback de fi // de música. 0 quan no hi ha música o quan el text dura més que // la pista. int intro_music_tail_ms{0}; int outro_music_tail_ms{0}; // Mil·lisegons des del callback de fi de la música d'INTRO // (`room_intro.ogg`) en què cau l'spawn aeri del primer slot // actiu. 0 = cau al moment exacte que la música acaba. El // respawn post-mort tindrà el seu propi timing quan toqui // dissenyar-lo. int player_spawn_delay_ms{0}; // Delay independent per al segon jugador, **només quan els dos // slots estan actius** (mode BOTH). En single-player (P1 o P2 // sols) s'usa sempre `player_spawn_delay_ms`. Si el YAML no // fixa aquest valor, `loadConfig` el copia de `player_spawn_delay_ms` // perquè els dos players caiguin alhora — comportament neutre. int player2_spawn_delay_ms{0}; // Píxels extra entre les dues línies del text d'INTRO (piràmide // damunt, sala davall). Es sumen al `lineHeight` natural del // font per separar visualment les dues línies. 0 = stack // immediat (com l'antic single-line). El YAML pot afinar-lo // sense recompilar. int intro_text_line_gap_px{8}; // Equivalent per a l'OUTRO ("fase" damunt, "completada" davall). // Es manté independent del de l'INTRO per si el ritme visual // demana un espaiat distint en cada banda de la transició. int outro_text_line_gap_px{8}; UiConfig::CurveFn text_enter_curve{nullptr}; UiConfig::CurveFn text_exit_curve{nullptr}; // Preset de reverb (catàleg a `data/config/sounds.yaml`) que GameScene // arma al jugador al spawn aeri d'INTRO. Es consumeix al primer // aterratge cinemàtic; els salts posteriors del joc i els // respawns post-mort cauen al so sec. Buit = sense reverb (la // cinemàtica d'entrada sona igual que un salt qualsevol). std::string intro_land_reverb_preset; // Panel translúcid sota el rètol de transició (INTRO i OUTRO). // Centrat verticalment al centre del playfield, ocupa tota // l'amplada del canvas. Creix d'altura 0 → height_px durant la // fase d'enter del text (amb `text_enter_curve`) i decreix // durant la fase d'exit (amb `text_exit_curve`). 0 d'alpha o // 0 d'alçada el deixa invisible. Color RGB (sense alpha — la // transparència viu al camp `alpha`). std::uint32_t text_panel_color{0x000000U}; std::uint8_t text_panel_alpha{128}; int text_panel_height_px{40}; }; // Llegeix la configuració des del YAML. Si el fitxer manca o falten // claus, els camps no presents conserven els seus fallbacks. Les // corbes es resolen amb `UiConfig::curveByName` (mateix mecanisme // que la pausa); si el nom és desconegut cau a `Easing::linear`. [[nodiscard]] auto loadConfig(const std::string& yaml_path) -> Config; // Avança la transició consumint `delta_ms` des del temps real. // Retorna l'estat resultant (potser el mateix que l'entrant si // encara queden sub-fases). Si retorna un estat diferent, el caller // ha de resetejar la `Transition` a {0, 0} abans del següent frame // (això ho fa `GameScene::setState`). Funció pura sobre l'argument. [[nodiscard]] auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State; // Substitueix `{p}` de la clau "game.room_intro_pyramid" del Locale // per l'enter passat. Pensat per a la primera línia del text d'INTRO. // Si la clau no està traduïda, retorna la pròpia clau (fallback de // Locale::text). [[nodiscard]] auto formatIntroPyramidLine(int pyramid) -> std::string; // Equivalent per a `game.room_intro_room` amb el placeholder `{r}`. // Es renderitza com a segona línia del text d'INTRO. [[nodiscard]] auto formatIntroRoomLine(int room) -> std::string; // Nom textual per a logs i tests. No es fa servir per a lògica. [[nodiscard]] auto stateName(State s) -> const char*; } // namespace RoomFsm