#pragma once #include #include #include #include #include #include "external/fkyaml_node.hpp" #include "game/entities/event_bus.hpp" class Entity; class Sarcophagus; class Door; class Player; class DynamicHazard; // Contenidor i coordinador d'entitats d'un nivell. Responsabilitats: // 1. `add` registra una entitat, la connecta al bus i manté propietat. // 2. `update(dt)` propaga el tick a totes. // 3. Comptador de sarcòfags oberts — quan iguala el total registrat, // crida `door_->open()` automàticament. // 4. `notifyPlayerTilePainted(col, row, player_id)` — fan-out del hook // per a tots els sarcòfags (i qualsevol entitat futura que s'hi // subscrigui via `notifyTilePainted`). El `player_id` permet // atribuir l'acció al jugador concret (p.ex. qui tanca un sarc). // // NO és un singleton: es crea una instància per nivell. De moment no // s'usa encara al GameScene (andamiatge pur). class EntityManager { public: EntityManager(); ~EntityManager(); EntityManager(const EntityManager&) = delete; EntityManager(EntityManager&&) = delete; auto operator=(const EntityManager&) -> EntityManager& = delete; auto operator=(EntityManager&&) -> EntityManager& = delete; // Registre d'una entitat. L'ownership és del manager; retorna // l'observador per conveniència (no eximeix que el manager la destrueixi). auto add(std::unique_ptr entity) -> Entity*; // Marca una entitat com a porta principal del nivell. N'hi ha només // una. Si ja n'hi ha una assignada, la substitueix (útil a tests). void setDoor(Door* door); // Treu i destrueix l'entitat observada. Si era la porta o un sarc, // ajusta `door_` / `total_sarcs_` en conseqüència. Pensat per al // editor: no toca `opened_sarcs_` ni publica events — el joc està // congelat i, en sortir del editor, GameScene torna a fer reload(), // així que l'estat es deriva net. auto removeEntity(Entity* observer) -> bool; // Marca una entitat com a sarcòfag — si no es passa per `add` amb // type=sarcophagus no es compta al total. Útil en combinació amb // `add` quan es fa addSarcophagus(unique_ptr). auto addSarcophagus(std::unique_ptr sarc) -> Sarcophagus*; // Tick de totes les entitats. void update(float delta_time); // Dibuix de totes les entitats. Sense ordenació per z: la crida dura // l'ordre d'inserció. GameScene fa el z-order interleaving amb els // players directament via `renderEntity`; `renderAll` queda com a // dreçera quan no hi ha players al render (editor, tests). void renderAll(); // Render d'una única entitat (mateixa lògica de clipping per frame // que renderAll). Exposat perquè GameScene pugui interleavar entitats // i players per `getFeetY()` (REND-01). Estàtic perquè la lògica de // render no depèn de cap estat del manager — ara com ara, l'entitat // porta la seua surface, animació i pixel_rect. static void renderEntity(const Entity& entity); // Accés als punters observacionals a les entitats per a consumidors // externs que necessiten iterar (z-order, aplicar dany, etc.). El // polimorfisme dins de cada entitat fa la discriminació: l'iterador // recorre tot i les entitats que no hagin de respondre tornen no-op // (p.ex. Torch::onWhipHit retorna false sense efecte). Sense sacs // separats — el `if (entity_is_X)` viu dins de l'entitat, no del caller. [[nodiscard]] auto getEntities() const -> std::vector; // Comprova contacte del(s) jugador(s) amb entitats `collides()=true`. // Si la caixa base del jugador solapa amb la caixa de col·lisió de // l'entitat, crida `entity->onPlayerContact(player)`. No fa res si // la llista està buida. Es crida des de GameScene cada frame, després // de `update`, perquè la resposta (p.ex. `player_hit`, `level_complete`) // reflecteixi l'estat del frame actual. void checkPlayerCollisions(const std::vector& players); // Cerca la primera entitat que (a) sigui decal de sòl // (`is_floor_decal_=true`), (b) admeti triggers d'àrea // (`triggers_on_overlap_=true`), i (c) la seva caixa de col·lisió // solape amb el rect donat. Pensat per a `Player::integrateJump`, // que la consulta a l'aterratge per detectar caigudes sobre Hole // (i en el futur pinchos plans, plaques de pressió, charcos...) i // saltar-se el LANDING_STUN normal. Retorna `nullptr` si no hi ha // cap match — el caller usa el camí d'aterratge habitual. [[nodiscard]] auto findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity*; // Fan-out del hook de pintat: per a cada sarcòfag, cridar // notifyTilePainted. Les entitats no-sarcòfag ho ignoren per defecte // (veure Entity::notifyTilePainted). Invocat com a reacció a // `tile_painted` al bus (re-publicat pel PaintLayer un cop filtrat). void notifyPlayerTilePainted(int col, int row, int player_id); // Carrega els spawns d'un YAML de nivell i els afegeix al manager: // 1. Llegeix el bloc `entities:` via EntityLoader. // 2. Per cada spawn: crea via factory, carrega la seva surface al // Resource::Cache, calcula pixel_rect_ segons el tipus // (sarc: Map::getSarcRect; altres: col/row del tilemap) i l'afegeix. // 3. Loguea `Entities: N loaded` per facilitar el smoke d'arrencada. // Requereix que Map::loadFrom ja s'hagi cridat (necessita // `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`). void loadFromLevel(const std::string& level_yaml_path); // Crea i afegeix una entitat a partir d'un node spawn (mateix format // que els items del bloc `entities:` del YAML del nivell). Retorna el // punter observacional a l'entitat creada, o `nullptr` si la plantilla // no es pot carregar, el tipus no està registrat o el spawn no té els // camps requerits (`position`, `sarc_position`, etc. segons el tipus). // Pensat per al editor de Debug: construeix un node sintètic des // d'una posició del cursor i el passa. auto addSpawn(const fkyaml::node& spawn) -> Entity*; // Instancia tots els actors que estan en `pending_actors_` (cua que // `loadFromLevel` omple amb les DynamicHazards declarades al YAML del // mapa). Cada actor neix en estat SPAWN (per defecte des de la // plantilla; veure `Mummy::Mummy`) i s'arma immediatament el // `triggerSpawnExit()` perquè la fase EMERGING corri els seus segons // de `spawn.duration_s` i transiti a ACTIVE. Vacia la cua. Cridada // des de GameScene en una subfase concreta de ROOM_INTRO i des de // `respawnPlayersInstant` (salt debug). void spawnPendingActors(); // Materialitza els pending actors al món sense disparar la cinemàtica // de SPAWN (`onJustSpawned`). Pensat per a l'editor: vol veure les // mòmies a la seva posició inicial per poder editar-les, però no // disparar l'animació EMERGING (l'editor congela el món, l'animació // no avançaria i quedaria invisible). En sortir de l'editor, GameScene // fa un `reload` que torna els actors a `pending_actors_` i el flux // normal els spawneja amb la cinemàtica al final del ROOM_INTRO. void materializePendingActorsStatic(); // Crea un actor on-demand i el deixa en EMERGING (com a // `spawnPendingActors` però per a un sol). Pensat per a futurs // generadors (StaticHazards que produeixen DynamicHazards en runtime, // p.ex. gotes que cauen, roques que rodolen). Retorna el punter // observacional o `nullptr` si la plantilla o el factory fallen. auto spawnActor(const std::string& type, int col, int row) -> DynamicHazard*; // Reset complet del nivell: destrueix totes les entitats, reseteja // els comptadors interns (sarcòfags oberts, total, exited, flag de // level_complete) i torna a cridar `loadFromLevel` per regenerar les // entitats des del YAML. El bus i les seues subscripcions // sobreviuen — això inclou les subscripcions externes (paint_layer_, // hud_, players, GameScene). Cridat des de GameScene a la transició // ROOM_OUTRO → ROOM_INTRO. `setExpectedPlayerCount` cal cridar-lo de // nou després si vols agregació de level_complete al nou cicle. void reload(const std::string& level_yaml_path); // Fixa quants jugadors actius es consideren al recompte d'EXITED. // Cridat per GameScene un cop instanciats els jugadors. Si manca, // `expected_players_ = 0` i mai s'emet `level_complete` automàticament. // Cridar-lo de nou al respawn (futur reload) reseteja el comptador. void setExpectedPlayerCount(int n); [[nodiscard]] auto getBus() -> EventBus& { return bus_; } [[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); } [[nodiscard]] auto pendingActorCount() const -> std::size_t { return pending_actors_.size(); } [[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; } [[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; } [[nodiscard]] auto exitedPlayerCount() const -> std::size_t { return exited_players_; } // Força l'obertura de la porta sense que els sarcòfags estiguin // oberts. Pensat com a hook de debug per a la GameScene: el flux // posterior (creuar la porta → `player_exited` → `level_complete` // → OUTRO) segueix exactament igual perquè depèn de Door, no de // sarcs. No-op si la porta ja està oberta o si el nivell no en té. void forceOpenDoor(); // Hook que `Sarcophagus::onRemoved` crida per decrementar el // comptador `total_sarcs_`. Públic perquè el sarcòfag és la única // entitat que necessita aquest manteniment, però l'API es manté // genèrica: si en el futur Door volgués notificar quelcom semblant, // afegim el seu propi callback. void onSarcophagusRemoved() { if (total_sarcs_ > 0) { --total_sarcs_; } } private: // Suport per al callback registrat a `Map::setEntityCollider`. Itera // les entitats amb `blocksMovement()` i fa overlap estricte (mateixa // convenció que `Map::collides`). El callback el registra el ctor. [[nodiscard]] auto entityCollidesAt(const SDL_FRect& box) const -> bool; EventBus bus_; // Un sol contenidor lògic per a totes les entitats instanciades. La // discriminació de tipus la fan els virtuals (`onPlayerContact`, // `onWhipHit`, `shouldReceiveContact`, `notifyTilePainted`...) — el // manager itera i cada entitat decideix què fa. Hi conviuen // decoracions, static_hazards, interactables i dynamic_hazards // (actors). std::vector> entities_; // Cua d'actors declarats al YAML del mapa però encara no instanciats: // `loadFromLevel` hi empeny tots els DynamicHazards. `spawnPendingActors` // o `spawnActor` consumeixen aquesta cua (o creen un actor on-demand) // i ho buiden. El `fkyaml::node` original es guarda perquè conté la // posició i qualsevol override del map respecte a la plantilla. struct PendingActor { std::string type; fkyaml::node spawn_node; }; std::vector pending_actors_; Door* door_{nullptr}; std::size_t total_sarcs_{0}; std::size_t opened_sarcs_{0}; // Recompte d'`player_exited`: incrementat a la subscripció al bus, // resetejat per `setExpectedPlayerCount`. Quan `exited_players_` arriba // a `expected_players_` (i tots dos > 0), publica `level_complete` // amb `player_id = -1` (event d'agregat, no atribuible a un jugador). int expected_players_{0}; int exited_players_{0}; bool level_complete_emitted_{false}; EventBus::SubId sarc_sub_id_{0}; EventBus::SubId tile_sub_id_{0}; EventBus::SubId exit_sub_id_{0}; };