#pragma once #include #include #include #include #include "game/entities/event_bus.hpp" class Entity; class Sarcophagus; class Door; class Player; // 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)` — fan-out del hook per a tots // els sarcòfags (i qualsevol entitat futura que s'hi subscrigui via // `notifyTilePainted`). // // 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); // 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 construir una llista ordenada (z-order). [[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); // 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); // 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); // 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 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_; } private: EventBus bus_; std::vector> entities_; 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}; };