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:
1 parent
62f4b857ef
commit
6a6a1a52c7
3 files changed
+106
-13
No files matched your search
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in new issue
Block a user