diff --git a/source/game/entities/entity_manager.hpp b/source/game/entities/entity_manager.hpp index be5003e..affc0b5 100644 --- a/source/game/entities/entity_manager.hpp +++ b/source/game/entities/entity_manager.hpp @@ -126,6 +126,23 @@ class EntityManager { // 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(); + + // 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 @@ -144,6 +161,7 @@ class EntityManager { [[nodiscard]] auto getBus() -> EventBus& { return bus_; } [[nodiscard]] auto entityCount() const -> std::size_t { return map_entities_.size() + actors_.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_; } @@ -168,6 +186,16 @@ class EntityManager { // `add()` enruta automàticament segons sigui o no DynamicHazard. std::vector> map_entities_; std::vector> actors_; + // 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}; diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index e5db232..d3b2bf8 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -215,6 +215,24 @@ namespace { return tmpl["anchor"].get_value(); } + // Llig el camp `type:` de la plantilla (la categoria semàntica: + // "decoration", "static_hazard", "interactable", "dynamic_hazard", + // "effect"). Tornarà buit si la plantilla no es pot carregar o no + // declara el camp. S'usa al loader per decidir quin sac rep l'entitat. + auto readTemplateCategory(const std::string& type) -> std::string { + try { + const auto TMPL = EntityLoader::loadTemplate(type); + if (TMPL.contains("type")) { return TMPL["type"].get_value(); } + } catch (const std::exception& e) { + // Plantilla absent / parse error: deixem el caller que ho + // gestione (típicament `buildEntity` també fallarà i deixarà + // el seu propi log). Aquí només avisem perquè el silenci + // total amaga problemes de YAML mal formats. + Log::warn("EntityManager", "readTemplateCategory({}) ha fallat: {}", type, e.what()); + } + return {}; + } + // Coordenades píxel (canvas) d'una cel·la (col, row) del tilemap. El // grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que // pot ser negatiu per overscan CPC. @@ -397,6 +415,7 @@ void EntityManager::reload(const std::string& level_yaml_path) { // subscripcions externes (PaintLayer, HUD, GameScene) no es toquen. map_entities_.clear(); actors_.clear(); + pending_actors_.clear(); door_ = nullptr; total_sarcs_ = 0; opened_sarcs_ = 0; @@ -433,8 +452,21 @@ auto EntityManager::addSpawn(const fkyaml::node& spawn) -> Entity* { void EntityManager::loadFromLevel(const std::string& level_yaml_path) { const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path); - const int LOADED = static_cast(std::ranges::count_if(SPAWNS, - [this](const fkyaml::node& spawn) { return addSpawn(spawn) != nullptr; })); + int loaded = 0; + for (const auto& spawn : SPAWNS) { + if (!spawn.contains("type")) { continue; } + const auto TYPE = spawn["type"].get_value(); + // Spawn diferit: els DynamicHazards (mòmies, roques, futurs + // enemics) no s'instancien aquí. GameScene crida + // `spawnPendingActors()` al final del text-exit de ROOM_INTRO + // (i, defensivament, en salts debug). La instanciació = la + // seua aparició visual. + if (readTemplateCategory(TYPE) == "dynamic_hazard") { + pending_actors_.push_back(PendingActor{.type = TYPE, .spawn_node = spawn}); + continue; + } + if (addSpawn(spawn) != nullptr) { ++loaded; } + } // Broadcast inicial: marca com a "pintats" tots els tiles ocupats per // decals de sòl (Hole, i futurs pinchos plans, plaques, charcos...). @@ -463,5 +495,42 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) { Log::info("EntityManager", "{} tile_painted broadcast inicial (decals de sòl)", decal_broadcast); } - Log::info("EntityManager", "{} entitats carregades des de {}", LOADED, level_yaml_path); + Log::info("EntityManager", "{} map entities + {} actors pending des de {}", loaded, pending_actors_.size(), level_yaml_path); +} + +void EntityManager::spawnPendingActors() { + // Itera els pending i els instancia via `addSpawn`. El dispatch d'`add()` + // els enruta a `actors_`. El `triggerSpawnExit()` posterior arma el + // comptador de SPAWN — la mòmia neix ja en EMERGING, sense esperes. + for (const auto& pa : pending_actors_) { + Entity* observer = addSpawn(pa.spawn_node); + if (observer == nullptr) { continue; } + // Per categoria YAML (`type: dynamic_hazard`) sabem que és + // DynamicHazard — el dynamic_cast aquí val zero i evita + // assumptions implícites sobre la jerarquia. + if (auto* dh = dynamic_cast(observer); dh != nullptr) { + dh->triggerSpawnExit(); + } + } + pending_actors_.clear(); +} + +auto EntityManager::spawnActor(const std::string& type, int col, int row) -> DynamicHazard* { + // Construïm un node de spawn sintètic amb el mínim que `placeGeneric` + // necessita: `type` i `position: [col, row]`. Si el caller vol + // overrides addicionals (orientació inicial, durada de spawn...), + // afegirà una variant amb fkyaml::node pre-construït més endavant. + fkyaml::node spawn; + try { + const std::string YAML_TEXT = "type: " + type + "\nposition: [" + std::to_string(col) + ", " + std::to_string(row) + "]\n"; + spawn = fkyaml::node::deserialize(YAML_TEXT.begin(), YAML_TEXT.end()); + } catch (const std::exception&) { + Log::error("EntityManager", "spawnActor: no s'ha pogut construir spawn YAML per {}", type); + return nullptr; + } + Entity* observer = addSpawn(spawn); + if (observer == nullptr) { return nullptr; } + auto* dh = dynamic_cast(observer); + if (dh != nullptr) { dh->triggerSpawnExit(); } + return dh; } diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 38aad3a..f509f13 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -687,16 +687,12 @@ void GameScene::respawnPlayersInstant() { #endif void GameScene::triggerHazardSpawnExit() { - int count = 0; - // `getActors()` retorna directament `DynamicHazard*` — sense - // dynamic_cast (els actors viuen en el seu sac dedicat). - for (auto* actor : entity_manager_.getActors()) { - if (actor->isSpawning()) { - actor->triggerSpawnExit(); - ++count; - } - } - Log::info("ROOM", "INTRO: hazard spawn-exit armat ({} DynamicHazards)", count); + // Instancia els actors diferits (els que `loadFromLevel` ha empès a + // `pending_actors_`) i els arma el `triggerSpawnExit()` perquè entrin + // ja en EMERGING. L'aparició visual de la mòmia és aquesta crida. + const std::size_t PENDING = entity_manager_.pendingActorCount(); + entity_manager_.spawnPendingActors(); + Log::info("ROOM", "INTRO: spawn diferit de {} actors", PENDING); } void GameScene::triggerPlayerSpawn(std::size_t slot_index) {