feat(em): spawn diferit de DynamicHazards (pending_actors_)

`loadFromLevel` ja no instancia immediatament les DynamicHazards del
YAML del mapa: les acumula a `pending_actors_` (estructura {type,
spawn_node}). El loader decideix per la categoria semàntica de la
plantilla (`type:` del YAML d'entity: "dynamic_hazard" → pending; la
resta → addSpawn d'instant).

Dues APIs noves consumeixen pending_actors_:
- `spawnPendingActors()` — itera la cua, instancia tots via addSpawn
  i arma `triggerSpawnExit()` a cada un. Vacia la cua. El path
  d'aparició massiva al final del text-exit de ROOM_INTRO.
- `spawnActor(type, col, row)` — instancia un únic actor on-demand
  des d'un spawn YAML sintètic. Pensat per a futurs generadors
  (StaticHazards que produeixen DynamicHazards en runtime: gotes,
  roques, etc.). Cada actor neix ja en EMERGING.

`GameScene::triggerHazardSpawnExit` ara és un wrapper de
`spawnPendingActors()` — el dispar segueix en la subfase actual
(fase 5 de ROOM_INTRO) i a `respawnPlayersInstant`. El canvi de timing
ve en el commit següent.

`reload` també buida pending_actors_. Log: "N map entities + M actors
pending des de PATH" — visible per a diagnòstic.

Sense canvis de comportament observables al joc: les mòmies segueixen
apareixent al final del text-exit, però ara la "aparició" és la seua
instanciació real (abans ja existien quietes des del fade-in).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
JailDesignerandClaude Opus 4.7 committed 2026-05-11 10:35:02 +02:00
1 parent 62f4b857ef
commit 6a6a1a52c7
3 files changed
+106 -13

No files matched your search

+28
View File
@@ -126,6 +126,23 @@ class EntityManager {
// d'una posició del cursor i el passa. // d'una posició del cursor i el passa.
auto addSpawn(const fkyaml::node& spawn) -> Entity*; 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 // Reset complet del nivell: destrueix totes les entitats, reseteja
// els comptadors interns (sarcòfags oberts, total, exited, flag de // els comptadors interns (sarcòfags oberts, total, exited, flag de
// level_complete) i torna a cridar `loadFromLevel` per regenerar les // level_complete) i torna a cridar `loadFromLevel` per regenerar les
@@ -144,6 +161,7 @@ class EntityManager {
[[nodiscard]] auto getBus() -> EventBus& { return bus_; } [[nodiscard]] auto getBus() -> EventBus& { return bus_; }
[[nodiscard]] auto entityCount() const -> std::size_t { return map_entities_.size() + actors_.size(); } [[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 sarcophagusCount() const -> std::size_t { return total_sarcs_; }
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; } [[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
[[nodiscard]] auto exitedPlayerCount() const -> std::size_t { return exited_players_; } [[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. // `add()` enruta automàticament segons sigui o no DynamicHazard.
std::vector<std::unique_ptr<Entity>> map_entities_; std::vector<std::unique_ptr<Entity>> map_entities_;
std::vector<std::unique_ptr<DynamicHazard>> actors_; std::vector<std::unique_ptr<DynamicHazard>> 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<PendingActor> pending_actors_;
Door* door_{nullptr}; Door* door_{nullptr};
std::size_t total_sarcs_{0}; std::size_t total_sarcs_{0};
std::size_t opened_sarcs_{0}; std::size_t opened_sarcs_{0};
@@ -215,6 +215,24 @@ namespace {
return tmpl["anchor"].get_value<std::string>(); return tmpl["anchor"].get_value<std::string>();
} }
// 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<std::string>(); }
} 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 // Coordenades píxel (canvas) d'una cel·la (col, row) del tilemap. El
// grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que // grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que
// pot ser negatiu per overscan CPC. // 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. // subscripcions externes (PaintLayer, HUD, GameScene) no es toquen.
map_entities_.clear(); map_entities_.clear();
actors_.clear(); actors_.clear();
pending_actors_.clear();
door_ = nullptr; door_ = nullptr;
total_sarcs_ = 0; total_sarcs_ = 0;
opened_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) { void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path); const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path);
const int LOADED = static_cast<int>(std::ranges::count_if(SPAWNS, int loaded = 0;
[this](const fkyaml::node& spawn) { return addSpawn(spawn) != nullptr; })); for (const auto& spawn : SPAWNS) {
if (!spawn.contains("type")) { continue; }
const auto TYPE = spawn["type"].get_value<std::string>();
// 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 // Broadcast inicial: marca com a "pintats" tots els tiles ocupats per
// decals de sòl (Hole, i futurs pinchos plans, plaques, charcos...). // 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", "{} 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<DynamicHazard*>(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<DynamicHazard*>(observer);
if (dh != nullptr) { dh->triggerSpawnExit(); }
return dh;
} }
+6 -10
View File
@@ -687,16 +687,12 @@ void GameScene::respawnPlayersInstant() {
#endif #endif
void GameScene::triggerHazardSpawnExit() { void GameScene::triggerHazardSpawnExit() {
int count = 0; // Instancia els actors diferits (els que `loadFromLevel` ha empès a
// `getActors()` retorna directament `DynamicHazard*` — sense // `pending_actors_`) i els arma el `triggerSpawnExit()` perquè entrin
// dynamic_cast (els actors viuen en el seu sac dedicat). // ja en EMERGING. L'aparició visual de la mòmia és aquesta crida.
for (auto* actor : entity_manager_.getActors()) { const std::size_t PENDING = entity_manager_.pendingActorCount();
if (actor->isSpawning()) { entity_manager_.spawnPendingActors();
actor->triggerSpawnExit(); Log::info("ROOM", "INTRO: spawn diferit de {} actors", PENDING);
++count;
}
}
Log::info("ROOM", "INTRO: hazard spawn-exit armat ({} DynamicHazards)", count);
} }
void GameScene::triggerPlayerSpawn(std::size_t slot_index) { void GameScene::triggerPlayerSpawn(std::size_t slot_index) {