#pragma once #include #include #include #include #include #include #include "game/entities/event_bus.hpp" class Text; // Marcador (HUD) de la GameScene. Ocupa la franja superior de 24 px del // canvas i es divideix en tres "blocs" simètrics (P1 · COMÚ · P2) que // funcionen com a panells independents — estil panell de pinball: cada un // té el seu propi mode visible, animació d'entrada, shake i flash. // // Vida: instanciat per `GameScene` via `unique_ptr` (mateix patró que // `PaintLayer`). Cada partida nova crea un HUD net; el destructor // desubscriu del bus automàticament. **No és singleton** — és estat de // partida, no configuració. // // Comunicació: subscrit al `EventBus` de l'`EntityManager`. Reacciona a // events del joc (`player_hit`, `level_complete`, ...) sense que les // entitats hagin de saber que existeix. class Hud { public: // Identificador semàntic de bloc (també índex dins `blocks_`). enum class BlockId : std::uint8_t { LEFT = 0, CENTER = 1, RIGHT = 2 }; // Mode actual d'un bloc. Tret d'`IDLE`, els altres modes són // *placeholders* per a la primera versió (el render fa res); la API // ja està fixada perquè afegir-los més endavant no toqui callers. enum class Mode : std::uint8_t { IDLE, DYING, COUNTDOWN, WIN, EMPTY }; // Prompt mostrat al bloc d'un slot inactiu (en lloc dels zeros). El // GameScene fixa aquest valor cada frame segons el `RoomFsm::State` // perquè el missatge sigui coherent amb si el jugador pot pulsar // START per entrar ara mateix o si ha d'esperar al canvi de fase. // - NONE → render històric (zeros). Per a slots actius o per // quan el GameScene no vol ensenyar prompt. // - CAN_JOIN → "PULSA START / PARA JUGAR". Slot inactiu en PLAYING. // - WAIT → "ESPERA / PARA JUGAR". Slot inactiu durant // ROOM_INTRO/ROOM_OUTRO i, en fases futures, després // de GAME OVER. enum class JoinPromptMode : std::uint8_t { NONE, CAN_JOIN, WAIT }; // `initial_lives` es la cantidad de vides amb què s'activa un slot // (P1 al ctor i, més tard, els que arriben via "join in progress"). // Ve del YAML del jugador (`Player::death_config.initial_lives`), // així el marcador segueix automàticament la configuració del joc. Hud(EventBus& bus, std::shared_ptr font, int initial_lives); ~Hud(); Hud(const Hud&) = delete; Hud(Hud&&) = delete; auto operator=(const Hud&) -> Hud& = delete; auto operator=(Hud&&) -> Hud& = delete; // Cada frame: avança les animacions actives (entrada, shake, flash). void update(int delta_ms); // Pinta els tres blocs sobre el canvas en la seva posició actual. // El caller decideix l'ordre respecte a la resta de capes (en general // cal cridar-lo a sobre del tilemap perquè quedi visible). void render() const; // Substitueix el mode d'un bloc (commutar el que es veu sense canviar // la geometria — útil per a Dying/Countdown/Win quan estiguin fets). void setMode(BlockId block, Mode mode); // Actualitza l'identificador de piràmide i sala (apareix al bloc // central). El caller en farà ús quan canvi de pantalla amb scroll o // entre sales. void setPyramidRoom(int pyramid, int room); // Activa el bloc d'un jugador (1-based: 1 → P1, 2 → P2). Mentre està // inactiu, el bloc mostra zeros i no rep flash/shake; quan s'activa, // arrenca amb les vides per defecte. void activatePlayer(int player_one_based); // Fixa el prompt visible al bloc d'un slot inactiu (1-based: 1 → P1, // 2 → P2). Cridat des de `GameScene::update()` cada frame perquè el // missatge depèn de l'estat de la `RoomFsm` (vegeu enum // `JoinPromptMode`). Sobre un slot actiu el prompt no es renderitza // — el camp existeix per simetria i per evitar branques al caller. // Idempotent: cridar amb el mateix mode és no-op. void setJoinPrompt(int player_one_based, JoinPromptMode mode); // Geometria pública perquè la GameScene puga calcular si el HUD // s'està movent encara (p. ex. retardar l'spawn fins que estiga al // seu lloc) — opcional, no obligatori. static constexpr int HEIGHT_PX = 24; private: // Amplades dels tres blocs. Sumen exactament `GameCanvas::WIDTH` (320). // Centrats: el bloc del mig agafa els píxels sobrants (108) i els // laterals queden idèntics (106 cadascun) per garantir simetria. static constexpr int LEFT_W = 106; static constexpr int CENTER_W = 108; static constexpr int RIGHT_W = 106; // Files de text dins del bloc (y relatiu al bloc), per a una font de // 8 px d'alçada amb un petit padding visual a dalt i a baix. static constexpr int LINE1_Y = 4; static constexpr int LINE2_Y = 13; struct Block { Mode mode{Mode::IDLE}; // Coordenades de pantalla on viu el bloc (constants durant la // partida; només varien els offsets transitoris d'entrada/shake). int x{0}; int y{0}; int w{0}; int h{0}; // Animació d'entrada: ms restants fins a quedar-se a la posició // final. Quan val 0 el bloc està quiet. El Hud ctor l'inicialitza // amb la durada viva a data/config/ui.yaml (hud.entry_ms). int entry_ms_left{0}; // Vector inicial des d'on entra: {-LEFT_W,0} per al bloc esquerre, // {0,-HEIGHT_PX} per al central i {+RIGHT_W,0} per al dret. int entry_offset_x{0}; int entry_offset_y{0}; // Shake actiu: amplitud i ms restants. Decau linealment. int shake_ms_left{0}; float shake_intensity{0.0F}; // Flash actiu: mentre val >0, el text del valor es pinta amb el // color de flash en compte del de valor. int flash_ms_left{0}; }; struct PlayerStats { int lives{0}; int score{0}; int treasures{0}; // `false` → el bloc mostra el prompt actiu (`join_prompt`) o // bé zeros si el prompt és NONE. Sempre es renderitza per // mantenir el layout fix. bool active{false}; // Missatge a mostrar quan `active==false`. El GameScene el // refresca cada frame segons l'estat de la RoomFsm (vegeu // `Hud::setJoinPrompt`). JoinPromptMode join_prompt{JoinPromptMode::NONE}; }; // Pinta el bloc indicat amb el seu fons + el contingut del seu mode // actual. Aplica l'offset (entrada + shake) abans de dibuixar. void renderBlock(BlockId id) const; // Subrutines del mode `Idle` per als tres blocs. Reben el rectangle // ja desplaçat (x, y) on han d'escriure. void renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const std::string& label) const; void renderIdleCenter(int x, int y, int w) const; // Calcula l'offset transitori (entrada + shake) en píxels per a un // bloc. Static perquè depèn només dels camps del propi `Block` i de // les constants de classe (no toca cap altre membre). [[nodiscard]] static auto computeOffset(const Block& b) -> SDL_FPoint; EventBus& bus_; std::vector subs_; std::shared_ptr font_; std::array blocks_; std::array players_; int pyramid_{1}; int room_{1}; // Vides amb què s'arrenca un slot (`activatePlayer`). Cachejat al // ctor des de `Player::death_config.initial_lives` per no llegir el // singleton estàtic en el camí calent del bus de events. int initial_lives_{0}; };