#pragma once #include #include #include #include #include #include "core/rendering/transitions/fade_to_black.hpp" #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/game_over_fsm.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") per // disparar les transicions de pausa. El mando ja no participa: el // botó START és ara una acció remapeable per dispositiu i el menú // de servei fa de pausa quan cal. 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 i // pot entrar mid-partida via "join in progress" (`handleJoinInput`). std::vector> players_; // Quins slots estan en partida ARA MATEIX (jugant o DYING). Mutable // durant la sala: el ctor el sembra des de `config_.starting_players`, // `joinPlayer` el posa a true quan un slot inactiu pulsa START, i la // subscripció a `player_disabled` el torna a false quan un slot // esgota vides. `loadRoom` recalcula `setExpectedPlayerCount` a // partir d'aquesta vista — un slot DISABLED al final d'una sala no // entra automàticament a la següent (el jugador ha de re-pulsar // START si vol re-entrar). std::array players_active_{false, false}; // Cinemàtica de spawn d'un slot concret. Posa el slot indicat en // AIRBORNE des d'una altura fora de pantalla. Cridat per // `spawnDuePlayers` quan el timer post-callback supera el delay // del slot. Idempotent (consulta `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); // Cos del callback de fi de música d'INTRO. Idempotent: si ja s'ha // disparat aquest cicle (`intro_completed_`), no fa res. Marca el // flag, reseteja el timer post-callback i intenta spawn dels slots // actius amb delay <= 0 ara mateix; els altres cauen frame a frame // dins `updateRoomTransition` quan el timer els assoleix. Finalment // commuta a PLAYING — la música del joc arrenca amb el primer slot. void completeIntro(); // Dispatch del cicle DYING al jugador identificat per `player_id`. // Crida `Player::enterDying()` (idempotent — un segon impacte mentre // ja és DYING és no-op). Subscriptor de l'event `player_killed` // publicat pels hazards letals (mòmia amb `kills_outright`). // Extret a un mètode per acotar la complexitat cognitiva del ctor. void dispatchPlayerKilled(int player_id); // Handler de l'event `player_disabled` (un slot acaba d'esgotar // vides). Apaga `players_active_[id]` perquè el `loadRoom` següent // no compti aquest slot i perquè el "join in progress" del frame // següent torni a oferir-li `setJoinPrompt(CAN_JOIN)`. Extret a // mètode per acotar la complexitat cognitiva del subscriber/ctor. void onPlayerDisabled(int player_id); // Pre-load dels recursos d'àudio que s'usen mid-partida i no els // toca el Director (so del countdown, etc.). Si la clau és buida // o ja està al cache, és no-op. Extret a mètode per acotar la // complexitat cognitiva del ctor. void preloadGameOverAssets() const; // Itera els slots actius i, si el delay configurat ha expirat des // del callback de fi de música d'INTRO, dispara `triggerPlayerSpawn`. // Cridada cada frame durant la fase d'spawn escalonat (entre el // callback i la caiguda de l'últim slot). No fa res si ja han // caigut tots o si encara no estem dins la finestra (intro_completed_). void spawnDuePlayers(); // Atén el "join in progress" (regla 1 de la FSM de joc): si un slot // està DISABLED i el jugador corresponent pulsa START dins de PLAYING, // dispara `joinPlayer`. La detecció de tecla/botó la fa // `Input::detect::pressedStart`. Ignorat fora de PLAYING (durant // INTRO/OUTRO el slot inactiu mostra "ESPERA / PARA JUGAR" al HUD). void handleJoinInput(); // Materialitza l'entrada d'un slot inactiu: spawn aeri al costat // corresponent de la porta (`computeSpawn().pX_x`), reset de vides // al valor inicial via `Player::resetForJoin`, marcatge de // `players_active_[i]=true` i activació del bloc del HUD. La // cinemàtica reaprofita `spawnFromAir` (mateixa caiguda visual que // l'INTRO de sala i el respawn post-mort). void joinPlayer(std::size_t slot_index); // Recompte de jugadors "en partida" (no DISABLED ni EXITED, o bé // DYING — DYING encara compta com a jugant fins que es resol). Hook // exposat per a la futura sub-FSM CONTINUE? + GAME OVER, que vigila // la transició a 0 jugadors actius. No s'usa dins de la Fase 1. [[nodiscard]] auto activePlayerCount() const -> int; // Refresca el prompt mostrat al bloc HUD de cada slot inactiu. Es // crida cada frame des d'`update()`. La decisió viu ací (que coneix // `state_` de la RoomFsm); el HUD només pinta. Per a slots actius, // posa el prompt a NONE per ser defensiu davant de canvis d'estat. void refreshJoinPrompt(); // Sentinel que marca el moment "música d'INTRO acabada" dins del // timeline. Ja no ve d'un callback d'àudio: el setja el GameScene // quan la FSM ha consumit l'última fase d'INTRO (la `music_tail`, // calculada perquè coincideixi amb el final del .ogg). Habilita el // tick de `intro_spawn_elapsed_ms_` i el spawn aeri escalonat dels // slots actius. bool intro_completed_{false}; // Mil·lisegons acumulats des del callback de fi de música d'INTRO, // per gatejar l'spawn aeri escalonat dels slots actius. P1 fa // servir `player_spawn_delay_ms` (típicament 0); P2 en mode BOTH // fa servir `player2_spawn_delay_ms` (offset respecte P1). Es // reseteja a 0 dins `completeIntro` i no torna a córrer fins al // següent cicle d'INTRO. int intro_spawn_elapsed_ms_{0}; // Per slot: ja ha caigut aquest cicle d'INTRO? Garanteix // idempotència de `triggerPlayerSpawn` davant múltiples ticks del // timer mentre el delay no s'ha mogut. Es reseteja a `enterRoomIntro` // i a `setState(ROOM_INTRO)` (camí defensiu no canònic). std::array players_spawned_in_intro_{false, false}; // 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/config/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/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_{}; // Fade-in d'INTRO: alpha 255 → 0 durant `intro_fade_in_ms`. Es comença // a `enterRoomIntro` i tica cada frame; un cop finished, `render()` // pinta alpha=0 (no-op) durant la resta del cicle. Transitions::FadeToBlack intro_fade_; // Fade-out d'OUTRO: alpha 0 → 255 durant `outro_fade_out_ms`, només // arrencat quan la FSM entra a la fase corresponent (detectada via // `outro_fade_started_`). Un cop finished, `render()` segueix // pintant alpha=255: això cobreix de manera natural la fase final // de `black_hold` sense lògica especial. Transitions::FadeToBlack outro_fade_; bool outro_fade_started_{false}; // 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(); // Configuració, estat i transició de la sub-FSM de game over // (CONTINUE? + GAME OVER). Lligada al mateix `game.yaml` que la // RoomFsm via `GameOverFsm::loadConfig`. GameOverFsm::Config game_over_config_{}; GameOverFsm::State game_over_state_{GameOverFsm::State::OFF}; GameOverFsm::Transition game_over_transition_{}; // Fade final cap a LOGO. Independent del `outro_fade_` perquè el // game over passa per damunt de la RoomFsm i pot disparar-se durant // PLAYING sense haver d'arrencar un OUTRO de sala. Comença a la // fase 0 de FADE_OUT i es manté pintant alpha=255 durant el // `black_hold` final (mateix patró que `outro_fade_`). Transitions::FadeToBlack game_over_fade_; bool game_over_fade_started_{false}; // Últim valor del countdown reproduït amb so. -1 com a sentinel // perquè la primera detecció del valor inicial (`continue_count_start`) // dispari el "tick". Es reseteja a -1 cada vegada que entrem a // CONTINUE_PROMPT (vegeu `setGameOverState`). int last_countdown_value_{-1}; // Avança la sub-FSM de game over consumint `delta_ms`. Detecta la // transició de "tots els jugadors fora de partida" durant PLAYING // (entrada a CONTINUE_PROMPT), arma el fade de FADE_OUT i, en // arribar a DONE, demana la transició a `LOGO`. void updateGameOverFlow(int delta_ms); // Substitueix `setState` per a la sub-FSM de game over: log // d'entrada/sortida i reset de la `Transition`. També arma el // `game_over_fade_` quan entrem a FADE_OUT, en analogia amb // `outro_fade_`. void setGameOverState(GameOverFsm::State new_state); // Pintat del rètol de game over (CONTINUE? + countdown durant // CONTINUE_PROMPT; "GAME OVER" durant GAME_OVER). Reaprofita el // panel translúcid i la font del room text. void renderGameOverText(); // Pintat del fade negre final (FADE_OUT i black hold). Cridat // després del HUD perquè cobreixi tota la pantalla, inclòs el // marcador. void renderGameOverFade(); // Helpers d'animació del rètol de transició. Reutilitzats pel // rètol de sala (`renderRoomText`) i pel de game over // (`renderGameOverText`). Reben `phase` i `elapsed_ms` per // paràmetre perquè la mateixa lògica visual serveix les dues // sub-FSMs sense saber a quina pertanyen — només es lliguen al // `transition_config_` per a corbes i geometria del panel. [[nodiscard]] auto computeRoomTextX(int width, int phase, int elapsed_ms, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int; [[nodiscard]] auto computeRoomTextPanelHeight(int phase, int elapsed_ms, 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, reseteja `transition_` i `intro_completed_`, comença // la pista `room_intro.ogg` i resol el `intro_music_tail_ms` perquè // el timeline acabi just al final del .ogg. Sense callback de fi de // música — la transició a PLAYING la dispara `updateRoomTransition` // quan la FSM consumeix l'última fase. Si la música no arrenca, la // cua queda a 0 i l'INTRO acaba al final del text. Idempotent quan // ja s'estava a INTRO — útil per als salts debug. void enterRoomIntro(); #ifdef _DEBUG // Spawn instantani sense cinemàtica: usat al sortir del editor per // fer un "fresh start" de la sala (jugadors al spawn, sense la // cinemàtica `room_intro.ogg`). Posa l'estat directament a PLAYING. void respawnPlayersInstant(); #endif #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 };