#pragma once #include #include #include #include #include #include "core/scene/scene.hpp" #include "core/ui/ui_config.hpp" #include "game/debug_config.hpp" #include "game/entities/entity_manager.hpp" #include "game/map/paint_layer.hpp" #include "game/map/tilemap.hpp" #include "game/scenes/room_fsm.hpp" #ifdef _DEBUG #include "game/editor/editor.hpp" #endif class Hud; class Player; class Text; class GameScene : public Scene { public: // Conjunt de rutes que defineixen una piràmide concreta (mapa + tilemap // + sprites dels jugadors + música). Cada piràmide serà un Config distint; // `defaultConfig()` retorna la primera (Defaults::Pyramid). `starting_players` // controla quins jugadors instancia el ctor: només P1, només P2 o els dos. struct Config { std::string map_yaml; std::string player_sprite; std::string player_yaml; std::string player2_sprite; std::string player2_yaml; std::string music_track; DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH}; }; [[nodiscard]] static auto defaultConfig() -> Config; explicit GameScene(Config config = defaultConfig()); ~GameScene() override; void update(int delta_ms) override; void render() override; private: // Punt centralitzat per canviar d'estat de sala. Imprimeix la // transició i reinicialitza els acumuladors de la `RoomFsm::Transition`. // Cap estat s'ha de canviar fora d'aquí. void setState(RoomFsm::State new_state); // Avança la transició activa via `RoomFsm::advance`. S'executa cada // frame amb delta real (no escalat per la pausa) perquè la transició // viu en temps de UI. Si l'estat retornat és diferent de l'actual, // crida `setState` (que reseteja la `Transition`). void updateRoomTransition(int delta_ms); // Consumeix events d'aquest frame (F11 del KeyConfig "pause" o botó // START del gamepad primari) per disparar les transicions de pausa. // No alterna un flag binari: inicia (o inverteix mid-flight) un // "dial" de temps que frena el món amb easing. void handlePauseInput(); // Avança la transició activa, recalcula `pause_.scale` i aplica el // fade de volum de música. S'executa cada frame amb el delta real // (no escalat) perquè la transició viu en temps real. void updatePauseTransition(int delta_ms); // Spawna un Dust transitori centrat a (`feet_x_center`, `feet_y`). // Cridat per la subscripció del bus a `player_jumped` / // `player_landed` per pintar el polvet de despegue / aterratge. // `is_landing` selecciona la variant de l'animació al `Dust`. void spawnDust(float feet_x_center, float feet_y, bool is_landing); // Spawna un `WhipAttack` a partir del payload publicat al bus quan // un Player entra a `ATTACKING`. Llig la plantilla cacheada del YAML // i delega la mida + surface al `WhipAttack::configure()` segons // direcció. void spawnWhip(const struct WhipSpawnPayload& payload); Config config_; Tilemap tilemap_; // Gestor d'entitats del nivell (sarcòfags, porta, momia, antorxa, // etc.). Viu aquí perquè la seua vida coincideix amb la de la escena; // cada entitat del nivell es carrega al ctor via loadFromLevel. EntityManager entity_manager_; // Vector des del principi: AEE suportarà multijugador local; cada Player // té els seus propis Controls i entra al vector amb una crida extra. // Sempre conté **dos** entrades (P1 a [0], P2 a [1]) tant en mode BOTH // com en single-player: l'slot inactiu es queda en estat DISABLED de // manera permanent, deixant el camí preparat per al "join in progress" // futur (el segon jugador podrà entrar a meitat de partida sense // recrear estructures). Veure `players_starts_active_`. std::vector> players_; // Quins slots juguen aquesta partida segons `config_.starting_players`. // Index = id del jugador (0 = P1, 1 = P2). Determina a qui aplica el // spawn aeri al final de la fase 0 d'INTRO; un slot amb `false` es // queda DISABLED tot el cicle de sala. std::array players_starts_active_{false, false}; // Mil·lisegons des de l'entrada en ROOM_INTRO, per disparar la // cinemàtica de spawn aeri quan s'arriba al delay corresponent al // slot. Es reseteja a `setState(ROOM_INTRO)`. Cada slot té el seu // propi flag de "ja he caigut" — així podem caure en moments // distints en mode BOTH (`player_spawn_delay_ms` per a P1 i // `player2_spawn_delay_ms` per a P2). int room_intro_elapsed_ms_{0}; std::array players_spawned_in_intro_{false, false}; // One-shot del so del rètol d'INTRO (`text_sound` a game.yaml). // Es disparen quan la FSM passa a phase 2 (text_enter); el flag // evita repeticions si phase 2 s'estén multi-frame. Es reseteja a // `enterRoomIntro` / `setState(ROOM_INTRO)`. Si la clau del so és // buida o el cache no l'ha carregat, queda inutilitzat. bool intro_text_sound_played_{false}; // Cinemàtica de spawn d'un slot concret. Posa el slot indicat en // AIRBORNE des d'una altura fora de pantalla. Cridat per // `updateRoomTransition` quan `room_intro_elapsed_ms_` supera el // delay específic de l'slot. Idempotent (consulta el flag corresponent // a `players_spawned_in_intro_`). També és el punt al qual es // connectarà el respawn post-mort en el futur. void triggerPlayerSpawn(std::size_t slot_index); // Vista plana de punters crus — la fa servir Player com a llista // d'obstacles mòbils a la callback de col·lisió. Es construeix un sol // cop al ctor (els punters són estables perquè `players_` no es // redimensiona dins de la vida de l'escena). std::vector players_view_; // Factor multiplicatiu del push inter-jugador. Carregat de la clau // `push_factor` del YAML del mapa; 0.30 per defecte (veure // `Defaults::Player::PUSH_FACTOR`). Constant dins de la vida de // l'escena — una piràmide, una física. float push_factor_{0.30F}; // Capa de "pintat" del terra. Escolta `on_tile_entered` al bus, // filtra pintabilitat (sòl transitable) i re-publica `tile_painted` // per als sarcòfags. `unique_ptr` per controlar l'ordre de // construcció/destrucció: es crea al ctor un cop `entity_manager_` i // `tilemap_` estan llestos, i es destrueix abans que `entity_manager_` // (on viu el bus al qual està subscrit). Declarat després d'aquests // dos membres per garantir l'ordre invers a la destrucció. std::unique_ptr paint_layer_; // Marcador de la franja superior (24 px). `unique_ptr` per controlar // l'ordre de construcció/destrucció igual que `paint_layer_`: es crea // al ctor un cop `entity_manager_` ja té el bus, i es destrueix abans // que `entity_manager_` (on viu el bus al qual està subscrit). // Declarat després d'`entity_manager_` per garantir l'ordre invers a // la destrucció. std::unique_ptr hud_; // Pausa de gameplay com a "dial" de temps. En lloc d'un interruptor, // `pause_.scale` va de 1 (món corrent) a 0 (món congelat) amb una // corba d'easing durant ENTERING, i al revés durant EXITING. El // `delta_ms` del món (players + EntityManager) es multiplica per // aquesta escala; HUD, Notifier i GlobalInputs reben sempre el // delta real. Les inversions mid-flight capturen la `scale` actual // com a `from_scale` i escalen la durada a la distància restant per // mantenir la sensació de velocitat. La font és la mateixa que // abans — ja cachejada al Director. std::shared_ptr pause_font_; // Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). // Distinta de `pause_font_` perquè el rètol de fase té estètica // pròpia (CaesarDressing per defecte, configurable via // `data/ui.yaml > fonts.room_text`). La pausa segueix amb la // placeholder. std::shared_ptr room_text_font_; enum class PauseState : std::uint8_t { IDLE, // Joc corrent; cap transició en marxa. scale == 1. ENTERING, // Frenant cap a la pausa. scale decreix a 0. PAUSED, // Món congelat. scale == 0. EXITING, // Accelerant de tornada. scale creix a 1. }; struct PauseTransition { PauseState state{PauseState::IDLE}; float scale{1.0F}; // 0 = parat, 1 = corrent. float from_scale{1.0F}; // Valor de `scale` al començar la transició. float to_scale{1.0F}; // Valor objectiu (0.0 o 1.0). int elapsed_ms{0}; // Temps transcorregut dins la transició. int duration_ms{0}; // Durada total (escalada a la distància). UiConfig::CurveFn curve{nullptr}; // Corba activa. } pause_; // Configuració de la FSM de sala (intro/outro). Carregada al ctor des // de `data/scenes/game.yaml`; valors per defecte si manca el YAML. RoomFsm::Config transition_config_{}; // Estat actual de la FSM de sala. S'inicialitza a ROOM_INTRO al ctor // perquè cada vegada que entres al joc passes per la presentació de // la sala. La transició a PLAYING l'orquestra `updateRoomTransition`. RoomFsm::State state_{RoomFsm::State::ROOM_INTRO}; // Sub-fase i temps acumulat dins de l'estat actual. `setState` el // reseteja a {0, 0}. RoomFsm::Transition transition_{}; // Píxels del fade negre i del text de la transició de sala. // Cridats des de `render()` entre el bloc d'actors i el HUD — // així marcador, notificacions i service menu queden per damunt. void renderRoomFade(); void renderRoomText(); // Helpers de `renderRoomText`. Tots consulten `transition_` i // `transition_config_` directament a través de `this` — així les // signatures es queden netes i la funció pública s'orquestra amb // poques línies. Tres tasques separades: // - X animada d'una línia (entra per un costat, surt per l'altre). // - Altura animada del panel translúcid (creix, manté, decreix). // - 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; [[nodiscard]] auto computeRoomTextPanelHeight(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 drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const; // Posició inicial dels jugadors derivada de `Map::door_col`. Tots // els jugadors comparteixen Y (centre del corredor exterior superior); // X depèn del nombre actiu: amb un jugador queda centrat sota la // porta; amb dos, separats ±tile_size respecte al centre. Estructura // exposada perquè els tests puguen verificar-la sense entrar al ctor // sencer. struct PlayerSpawn { float p1_x{0.0F}; float p2_x{0.0F}; float y{0.0F}; }; [[nodiscard]] static auto computeSpawn() -> PlayerSpawn; // Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria, // porta tancada), entitats (sarcs/porta/momies des de zero), neteja // el PaintLayer i deixa els jugadors a DISABLED esperant la cinemàtica // d'spawn aeri del nou cicle d'INTRO. També actualitza // `config_.map_yaml` perquè futurs consumidors (l'Editor en guardar, // logs, etc.) sàpiguen quina sala està viva. void loadRoom(const std::string& yaml_path); // Demana al LevelManager el següent nivell i el carrega via // `loadRoom`, deixant la FSM al començament d'un nou cicle d'INTRO. // Si ja era a l'última entrada, fa loop a la primera (política // temporal mentre no hi haja pantalla de fi de joc). Cridat des de // `updateRoomTransition` quan l'OUTRO arriba a la fase final. void advanceToNextRoom(); // Inicia (o reinicia) el cicle d'entrada de sala: posa l'estat a // ROOM_INTRO i reseteja `transition_`, `room_intro_elapsed_ms_` i el // flag d'spawn aeri. Idempotent quan ja s'estava a INTRO (els // timers tornen a començar des de zero — útil per als salts debug // que recarreguen la sala estant ja en INTRO). void enterRoomIntro(); #ifdef _DEBUG // Editor modal del tilemap. Mútuament excloent amb la pausa: si la // pausa no està IDLE la tecla de toggle de l'editor s'ignora, i // viceversa. Quan `editor_.active()` és true, saltem entitats i // jugador (update i render) i deixem el tilemap visible perquè es // pugui editar. Editor editor_; #endif };