refactor(em): separa entitats en dos sacs (map_entities_/actors_)
EntityManager guanya dues llistes lògiques: - `map_entities_` — decoració, static_hazards (Hole, FloorTrap, WallTrap) i interactables (Sarcophagus, Door). Persistents tota la sala. - `actors_` — DynamicHazards (Mummy, RollingRock, futurs enemics). Tipats com `unique_ptr<DynamicHazard>`, sense dynamic_cast. `add()` enruta automàticament segons sigui DynamicHazard o no. `update`, `notifyPlayerTilePainted`, `findFloorTriggerAt`, `removeEntity`, `reload` i `renderAll` iteren el(s) sac(s) que toca. `checkPlayerCollisions`: el filtre de SPAWN sobre el sac d'actors ja no necessita dynamic_cast — `actor->isSpawning()` és directe. El filtre per floor decals només toca map_entities_ (els decals viuen exclusivament allà). Filtres per-jugador extrets a helpers per acotar complexitat. API nova: `getMapEntities()` i `getActors()` exposen cada sac per separat. `getEntities()` es manté com a concatenació per retro-compat (editor, z-sort debug). `GameScene::triggerHazardSpawnExit` itera `getActors()` directe — fora dynamic_cast. `GameScene::render` mergeja els tres orígens (map + actors + players) en un sol vector temporal abans del stable_sort per `feet_y` — mateix patró que ja s'usava amb players, ara amb una transformació més. Comportament idèntic: les momies segueixen instanciant-se al loadFromLevel. La preparació per al spawn diferit ve en el commit següent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3b378e2b29
commit
62f4b857ef
4 files changed
+149
-70
No files matched your search
@@ -74,7 +74,19 @@ auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
|||||||
entity->setEventBus(&bus_);
|
entity->setEventBus(&bus_);
|
||||||
entity->setEntityManager(this);
|
entity->setEntityManager(this);
|
||||||
Entity* raw = entity.get();
|
Entity* raw = entity.get();
|
||||||
entities_.push_back(std::move(entity));
|
// Enrutament per categoria semàntica: si l'entitat és (o deriva de)
|
||||||
|
// DynamicHazard va al sac `actors_`, la resta al sac `map_entities_`.
|
||||||
|
// Així els consumidors que només volen actors itera el sac petit i
|
||||||
|
// ens estalviem dynamic_casts dispersos al codi (colisió, whip...).
|
||||||
|
if (dynamic_cast<DynamicHazard*>(entity.get()) != nullptr) {
|
||||||
|
// Transferim l'ownership al unique_ptr<DynamicHazard> tipat fort.
|
||||||
|
// El `release()` consumeix el seu valor com a argument del ctor —
|
||||||
|
// sense crida nua que el linter pugui marcar com a ignorada.
|
||||||
|
std::unique_ptr<DynamicHazard> hazard{static_cast<DynamicHazard*>(entity.release())};
|
||||||
|
actors_.push_back(std::move(hazard));
|
||||||
|
} else {
|
||||||
|
map_entities_.push_back(std::move(entity));
|
||||||
|
}
|
||||||
// cppcheck-suppress returnDanglingLifetime
|
// cppcheck-suppress returnDanglingLifetime
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
@@ -91,9 +103,15 @@ auto EntityManager::removeEntity(Entity* observer) -> bool {
|
|||||||
if (observer == nullptr) { return false; }
|
if (observer == nullptr) { return false; }
|
||||||
const bool WAS_DOOR = (door_ == observer);
|
const bool WAS_DOOR = (door_ == observer);
|
||||||
const bool WAS_SARC = (observer->getName() == "sarcophagus");
|
const bool WAS_SARC = (observer->getName() == "sarcophagus");
|
||||||
auto it = std::ranges::find_if(entities_, [observer](const auto& up) { return up.get() == observer; });
|
// Busquem en els dos sacs. El sarc i la door sempre viuen a
|
||||||
if (it == entities_.end()) { return false; }
|
// `map_entities_` per disseny; un actor (DynamicHazard) viu a `actors_`.
|
||||||
entities_.erase(it);
|
if (auto it = std::ranges::find_if(map_entities_, [observer](const auto& up) { return up.get() == observer; }); it != map_entities_.end()) {
|
||||||
|
map_entities_.erase(it);
|
||||||
|
} else if (auto it_a = std::ranges::find_if(actors_, [observer](const auto& up) { return up.get() == observer; }); it_a != actors_.end()) {
|
||||||
|
actors_.erase(it_a);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (WAS_DOOR) { door_ = nullptr; }
|
if (WAS_DOOR) { door_ = nullptr; }
|
||||||
if (WAS_SARC && total_sarcs_ > 0) { --total_sarcs_; }
|
if (WAS_SARC && total_sarcs_ > 0) { --total_sarcs_; }
|
||||||
// No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot
|
// No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot
|
||||||
@@ -107,7 +125,8 @@ auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcoph
|
|||||||
sarc->setEventBus(&bus_);
|
sarc->setEventBus(&bus_);
|
||||||
sarc->setEntityManager(this);
|
sarc->setEntityManager(this);
|
||||||
Sarcophagus* raw = sarc.get();
|
Sarcophagus* raw = sarc.get();
|
||||||
entities_.push_back(std::move(sarc));
|
// El sarc és Interactable, viu al sac `map_entities_`.
|
||||||
|
map_entities_.push_back(std::move(sarc));
|
||||||
++total_sarcs_;
|
++total_sarcs_;
|
||||||
// cppcheck-suppress returnDanglingLifetime
|
// cppcheck-suppress returnDanglingLifetime
|
||||||
return raw;
|
return raw;
|
||||||
@@ -115,30 +134,34 @@ auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcoph
|
|||||||
|
|
||||||
void EntityManager::update(float delta_time) {
|
void EntityManager::update(float delta_time) {
|
||||||
// ATENCIÓ: si en el futur una entitat publica al bus events que
|
// ATENCIÓ: si en el futur una entitat publica al bus events que
|
||||||
// resulten en `entity_manager_.add()` (push_back a `entities_`), açò
|
// resulten en `entity_manager_.add()` (push_back als sacs), açò
|
||||||
// invalidaria l'iterador de la ranged-for. Avui no passa: els spawns
|
// invalidaria l'iterador de la ranged-for. Avui no passa: els spawns
|
||||||
// runtime venen de fora del cicle d'`update` (p.ex. el polvet del
|
// runtime venen de fora del cicle d'`update` (p.ex. el polvet del
|
||||||
// saltador es spawneja durant `Player::tick`, abans d'aquest mètode).
|
// saltador es spawneja durant `Player::tick`, abans d'aquest mètode).
|
||||||
for (auto& e : entities_) { e->update(delta_time); }
|
for (auto& e : map_entities_) { e->update(delta_time); }
|
||||||
|
for (auto& a : actors_) { a->update(delta_time); }
|
||||||
// Purga d'entitats marcades mortes aquest frame (efectes acabats,
|
// Purga d'entitats marcades mortes aquest frame (efectes acabats,
|
||||||
// p.ex. polvet d'aterratge). Les persistents (sarcs, momies, etc.)
|
// p.ex. polvet d'aterratge). Les persistents (sarcs, momies, etc.)
|
||||||
// mai no es marquen.
|
// mai no es marquen.
|
||||||
std::erase_if(entities_, [](const auto& e) { return e->isDead(); });
|
std::erase_if(map_entities_, [](const auto& e) { return e->isDead(); });
|
||||||
|
std::erase_if(actors_, [](const auto& a) { return a->isDead(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::notifyPlayerTilePainted(int col, int row) {
|
void EntityManager::notifyPlayerTilePainted(int col, int row) {
|
||||||
for (auto& e : entities_) { e->notifyTilePainted(col, row); }
|
// Fan-out als dos sacs. Avui només Sarcophagus consumeix (viu a
|
||||||
|
// map_entities_), però els actors també reben la notificació per
|
||||||
|
// si en el futur volen reaccionar (cost: una crida virtual buida).
|
||||||
|
for (auto& e : map_entities_) { e->notifyTilePainted(col, row); }
|
||||||
|
for (auto& a : actors_) { a->notifyTilePainted(col, row); }
|
||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* {
|
auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* {
|
||||||
// Cerca lineal sobre `entities_`: cost menyspreable amb poques
|
// Cerca lineal sobre `map_entities_`: els floor decals trigger són
|
||||||
// desenes d'entitats per sala i una crida per frame des dels
|
// exclusivament StaticHazards (Hole; en el futur pinchos plans,
|
||||||
// consumidors actius (Player a l'aterratge, Mòmia a la decisió de
|
// plaques, charcos). Els actors no qualifiquen com a decals — saltem
|
||||||
// direcció). Si esdevé calent, es podria indexar per cel·la del
|
// directament aquest sac, sense `triggersOnOverlap`/`isFloorDecal` per
|
||||||
// tilemap. La doble condició filtra exclusivament decals de sòl que
|
// a cada un. La doble condició filtra dins de map_entities_.
|
||||||
// volen rebre triggers — avui Hole; en el futur pinchos plans,
|
for (const auto& entity : map_entities_) {
|
||||||
// plaques de pressió, charcos.
|
|
||||||
for (const auto& entity : entities_) {
|
|
||||||
if (!entity->triggersOnOverlap()) { continue; }
|
if (!entity->triggersOnOverlap()) { continue; }
|
||||||
if (!entity->isFloorDecal()) { continue; }
|
if (!entity->isFloorDecal()) { continue; }
|
||||||
const auto BOX = entity->getCollisionRect();
|
const auto BOX = entity->getCollisionRect();
|
||||||
@@ -149,9 +172,27 @@ auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
||||||
|
// Concatenació map + actors per retro-compat (editor, z-sort debug).
|
||||||
|
// Els callers que ja saben quin sac volen poden cridar
|
||||||
|
// `getMapEntities` o `getActors` i estalviar la construcció del temp.
|
||||||
std::vector<Entity*> out;
|
std::vector<Entity*> out;
|
||||||
out.reserve(entities_.size());
|
out.reserve(map_entities_.size() + actors_.size());
|
||||||
std::ranges::transform(entities_, std::back_inserter(out), [](const auto& e) { return e.get(); });
|
std::ranges::transform(map_entities_, std::back_inserter(out), [](const auto& e) { return e.get(); });
|
||||||
|
std::ranges::transform(actors_, std::back_inserter(out), [](const auto& a) -> Entity* { return a.get(); });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto EntityManager::getMapEntities() const -> std::vector<Entity*> {
|
||||||
|
std::vector<Entity*> out;
|
||||||
|
out.reserve(map_entities_.size());
|
||||||
|
std::ranges::transform(map_entities_, std::back_inserter(out), [](const auto& e) { return e.get(); });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto EntityManager::getActors() const -> std::vector<DynamicHazard*> {
|
||||||
|
std::vector<DynamicHazard*> out;
|
||||||
|
out.reserve(actors_.size());
|
||||||
|
std::ranges::transform(actors_, std::back_inserter(out), [](const auto& a) { return a.get(); });
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/hazard.hpp"
|
||||||
|
|
||||||
class Entity;
|
class Entity;
|
||||||
class Sarcophagus;
|
class Sarcophagus;
|
||||||
@@ -74,7 +75,13 @@ class EntityManager {
|
|||||||
|
|
||||||
// Accés als punters observacionals a les entitats per a consumidors
|
// Accés als punters observacionals a les entitats per a consumidors
|
||||||
// externs que necessiten construir una llista ordenada (z-order).
|
// externs que necessiten construir una llista ordenada (z-order).
|
||||||
|
// `getEntities()` concatena els dos sacs interns (mapa + actors) per
|
||||||
|
// a consumidors retro-compat (editor, z-sort debug). `getMapEntities`
|
||||||
|
// i `getActors` exposen cada sac per separat — útil per a iteracions
|
||||||
|
// que ja saben quin tipus volen i estalvien filtres O(n).
|
||||||
[[nodiscard]] auto getEntities() const -> std::vector<Entity*>;
|
[[nodiscard]] auto getEntities() const -> std::vector<Entity*>;
|
||||||
|
[[nodiscard]] auto getMapEntities() const -> std::vector<Entity*>;
|
||||||
|
[[nodiscard]] auto getActors() const -> std::vector<DynamicHazard*>;
|
||||||
|
|
||||||
// Comprova contacte del(s) jugador(s) amb entitats `collides()=true`.
|
// Comprova contacte del(s) jugador(s) amb entitats `collides()=true`.
|
||||||
// Si la caixa base del jugador solapa amb la caixa de col·lisió de
|
// Si la caixa base del jugador solapa amb la caixa de col·lisió de
|
||||||
@@ -136,7 +143,7 @@ class EntityManager {
|
|||||||
void setExpectedPlayerCount(int n);
|
void setExpectedPlayerCount(int n);
|
||||||
|
|
||||||
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
||||||
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
|
[[nodiscard]] auto entityCount() const -> std::size_t { return map_entities_.size() + 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_; }
|
||||||
@@ -150,7 +157,17 @@ class EntityManager {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
EventBus bus_;
|
EventBus bus_;
|
||||||
std::vector<std::unique_ptr<Entity>> entities_;
|
// Dos sacs lògics:
|
||||||
|
// `map_entities_` — decoració, static_hazards (Hole, FloorTrap,
|
||||||
|
// WallTrap) i interactables (Sarcophagus, Door).
|
||||||
|
// Cargades al loadFromLevel i persistents durant
|
||||||
|
// tota la sala.
|
||||||
|
// `actors_` — DynamicHazards (Mummy, RollingRock, futurs
|
||||||
|
// enemics). Separats per eliminar dynamic_casts
|
||||||
|
// i preparar el spawn diferit (commit posterior).
|
||||||
|
// `add()` enruta automàticament segons sigui o no DynamicHazard.
|
||||||
|
std::vector<std::unique_ptr<Entity>> map_entities_;
|
||||||
|
std::vector<std::unique_ptr<DynamicHazard>> 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};
|
||||||
|
|||||||
@@ -57,56 +57,64 @@ void EntityManager::renderEntity(const Entity& e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::renderAll() {
|
void EntityManager::renderAll() {
|
||||||
for (const auto& e : entities_) { renderEntity(*e); }
|
// Sense ordenació per z: l'ordre és el d'inserció dins de cada sac.
|
||||||
|
// El render del joc real (GameScene) construeix el seu propi vector
|
||||||
|
// ordenat per `feet_y`; aquest helper queda per al editor i tests.
|
||||||
|
for (const auto& e : map_entities_) { renderEntity(*e); }
|
||||||
|
for (const auto& a : actors_) { renderEntity(*a); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Filtres "per què aquest jugador no rep contacte d'aquest hazard ara
|
||||||
|
// mateix": fora de joc, DYING, caient per forat (la transició a mort
|
||||||
|
// per forat la mana `integrateJump`, no la pot interrompre cap
|
||||||
|
// hazard), o salt per damunt de l'alçada del cos. Extret a funció
|
||||||
|
// per acotar la complexitat cognitiva del callsite. Veure la nota a
|
||||||
|
// `isFallingThroughHole`: si algun dia s'elimina aquest skip, cal
|
||||||
|
// revisar la cadena DYING(HOLE) → respawn.
|
||||||
|
auto playerSkipsHazard(const Player& p, int hazard_height_px) -> bool {
|
||||||
|
if (p.isOutOfPlay() || p.isDying()) { return true; }
|
||||||
|
if (p.isFallingThroughHole()) { return true; }
|
||||||
|
return p.isAirborne() && hazard_height_px > 0 && p.getJumpZ() > static_cast<float>(hazard_height_px);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto playerOverlapsEntityBox(const Player& p, const SDL_FRect& box) -> bool {
|
||||||
|
// Hit-box petita del jugador (`getHitBox`) en lloc de la caja base
|
||||||
|
// 16×16 — per a "tocar una entitat" volem precisió, no rebot físic.
|
||||||
|
const auto PBOX = p.getHitBox();
|
||||||
|
return PBOX.x + PBOX.w > box.x && box.x + box.w > PBOX.x && PBOX.y + PBOX.h > box.y && box.y + box.h > PBOX.y;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
||||||
if (players.empty()) { return; }
|
if (players.empty()) { return; }
|
||||||
for (const auto& entity : entities_) {
|
// Helper local: aplica el contacte d'una entitat sobre tots els
|
||||||
// Acceptem entitats sòlides (xoc físic) i triggers d'àrea (no
|
// jugadors, amb els filtres per-jugador a `playerSkipsHazard`. Es
|
||||||
// bloquegen però volen rebre `onPlayerContact`). Tanquem el gap
|
// comparteix entre els dos sacs (map_entities_ i actors_).
|
||||||
// latent de FloorTrap/WallTrap (que tenien `collides_=false` i
|
auto apply_contacts = [&players](Entity* entity) {
|
||||||
// mai no es disparaven) i obrim la porta a hazards no-físics
|
|
||||||
// com `Hole`.
|
|
||||||
if (!entity->collides() && !entity->triggersOnOverlap()) { continue; }
|
|
||||||
// DynamicHazard en SPAWN: visible però "no juga" — no dispara
|
|
||||||
// onPlayerContact, així no fa dany ni triggers durant la fase
|
|
||||||
// inicial de ROOM_INTRO (o futurs spawns en runtime).
|
|
||||||
if (const auto* dh = dynamic_cast<const DynamicHazard*>(entity.get()); dh != nullptr && dh->isSpawning()) { continue; }
|
|
||||||
const auto BOX = entity->getCollisionRect();
|
const auto BOX = entity->getCollisionRect();
|
||||||
const int E_HEIGHT = entity->getHeight();
|
const int E_HEIGHT = entity->getHeight();
|
||||||
for (auto* p : players) {
|
for (auto* p : players) {
|
||||||
// Filtrem també DYING: el jugador en cicle de mort no rep
|
if (p == nullptr || playerSkipsHazard(*p, E_HEIGHT)) { continue; }
|
||||||
// contacte amb hazards (un segon toc de mòmia mentre la roda
|
if (playerOverlapsEntityBox(*p, BOX)) { entity->onPlayerContact(p); }
|
||||||
// ja és no-op; cap altra entitat l'ha de fer transicionar).
|
|
||||||
if (p == nullptr || p->isOutOfPlay() || p->isDying()) { continue; }
|
|
||||||
// Mentre el jugador cau per un forat, ignorem qualsevol altre
|
|
||||||
// hazard: la transició a `enterDying()` ja la dispara
|
|
||||||
// `integrateJump` quan supera `HOLE_FALL_DEPTH`.
|
|
||||||
//
|
|
||||||
// NOTA: aquest skip és la defensa que evita que un jugador
|
|
||||||
// caient per un forat reba contacte d'altres hazards
|
|
||||||
// "subterranis" durant la caiguda. La cadena de mort per
|
|
||||||
// forat (`enterDying(HOLE)` → respawn) depén de no ser
|
|
||||||
// interrompuda per un event `player_hit`/`player_killed`
|
|
||||||
// mentre `falling_through_hole_=true`. Si el dia que es
|
|
||||||
// toque aquest skip s'elimina, cal revisar la cadena
|
|
||||||
// mort/respawn per forat.
|
|
||||||
if (p->isFallingThroughHole()) { continue; }
|
|
||||||
// Filtre per altura: si el jugador està en l'aire i està per
|
|
||||||
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
|
|
||||||
// És el mecanisme del salt per esquivar momies, roques, etc.
|
|
||||||
// Quan height és 0 (entitat sense altura declarada), el filtre
|
|
||||||
// no es dispara mai i la col·lisió funciona com abans.
|
|
||||||
if (p->isAirborne() && E_HEIGHT > 0 && p->getJumpZ() > static_cast<float>(E_HEIGHT)) { continue; }
|
|
||||||
// Contacte actor-actor amb hit-box petita: la caixa base
|
|
||||||
// 16×16 servia per moure (xoc amb murs), però per a l'event
|
|
||||||
// "toca una entitat" volem precisió — que la momia mate quan
|
|
||||||
// els píxels centrals es solapen, no quan els caps es freguen.
|
|
||||||
const auto PBOX = p->getHitBox();
|
|
||||||
const bool OVERLAP = PBOX.x + PBOX.w > BOX.x && BOX.x + BOX.w > PBOX.x && PBOX.y + PBOX.h > BOX.y && BOX.y + BOX.h > PBOX.y;
|
|
||||||
if (OVERLAP) { entity->onPlayerContact(p); }
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sac de mapa: filtre per col·lisió / trigger. Tanquem el gap latent
|
||||||
|
// de FloorTrap/WallTrap (que tenien `collides_=false` i mai no es
|
||||||
|
// disparaven) i obrim la porta a hazards no-físics com `Hole`.
|
||||||
|
for (const auto& entity : map_entities_) {
|
||||||
|
if (!entity->collides() && !entity->triggersOnOverlap()) { continue; }
|
||||||
|
apply_contacts(entity.get());
|
||||||
|
}
|
||||||
|
// Sac d'actors (DynamicHazards): tots fan dany quan són actius.
|
||||||
|
// Saltem els que estan en SPAWN: visibles però sense interacció
|
||||||
|
// durant la fase inicial de ROOM_INTRO i durant futurs spawns en
|
||||||
|
// runtime. Sense dynamic_cast: el tipus ja és DynamicHazard.
|
||||||
|
for (const auto& actor : actors_) {
|
||||||
|
if (actor->isSpawning()) { continue; }
|
||||||
|
if (!actor->collides() && !actor->triggersOnOverlap()) { continue; }
|
||||||
|
apply_contacts(actor.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +395,8 @@ void EntityManager::reload(const std::string& level_yaml_path) {
|
|||||||
// subscripcions implícites desapareixen amb la seua vida (cap d'elles
|
// subscripcions implícites desapareixen amb la seua vida (cap d'elles
|
||||||
// se subscriu directament al bus — només publiquen). El bus i les
|
// se subscriu directament al bus — només publiquen). El bus i les
|
||||||
// subscripcions externes (PaintLayer, HUD, GameScene) no es toquen.
|
// subscripcions externes (PaintLayer, HUD, GameScene) no es toquen.
|
||||||
entities_.clear();
|
map_entities_.clear();
|
||||||
|
actors_.clear();
|
||||||
door_ = nullptr;
|
door_ = nullptr;
|
||||||
total_sarcs_ = 0;
|
total_sarcs_ = 0;
|
||||||
opened_sarcs_ = 0;
|
opened_sarcs_ = 0;
|
||||||
@@ -437,7 +446,9 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
|||||||
// perquè tots els sarcòfags ja estan registrats al fan-out (la
|
// perquè tots els sarcòfags ja estan registrats al fan-out (la
|
||||||
// subscripció del manager és al ctor, anterior a qualsevol load).
|
// subscripció del manager és al ctor, anterior a qualsevol load).
|
||||||
int decal_broadcast = 0;
|
int decal_broadcast = 0;
|
||||||
for (const auto& entity : entities_) {
|
// Els decals de sòl (Hole, futurs pinchos plans...) són StaticHazards
|
||||||
|
// i viuen exclusivament a `map_entities_`. Saltem `actors_` per estalviar.
|
||||||
|
for (const auto& entity : map_entities_) {
|
||||||
if (!entity->isFloorDecal()) { continue; }
|
if (!entity->isFloorDecal()) { continue; }
|
||||||
const auto BOX = entity->getCollisionRect();
|
const auto BOX = entity->getCollisionRect();
|
||||||
const auto [COL, ROW] = Map::pixelToTile(BOX.x, BOX.y);
|
const auto [COL, ROW] = Map::pixelToTile(BOX.x, BOX.y);
|
||||||
|
|||||||
@@ -688,9 +688,11 @@ void GameScene::respawnPlayersInstant() {
|
|||||||
|
|
||||||
void GameScene::triggerHazardSpawnExit() {
|
void GameScene::triggerHazardSpawnExit() {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (auto* entity : entity_manager_.getEntities()) {
|
// `getActors()` retorna directament `DynamicHazard*` — sense
|
||||||
if (auto* hazard = dynamic_cast<DynamicHazard*>(entity); hazard != nullptr && hazard->isSpawning()) {
|
// dynamic_cast (els actors viuen en el seu sac dedicat).
|
||||||
hazard->triggerSpawnExit();
|
for (auto* actor : entity_manager_.getActors()) {
|
||||||
|
if (actor->isSpawning()) {
|
||||||
|
actor->triggerSpawnExit();
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1788,10 +1790,18 @@ void GameScene::render() {
|
|||||||
};
|
};
|
||||||
std::vector<Renderable> order;
|
std::vector<Renderable> order;
|
||||||
order.reserve(entity_manager_.entityCount() + players_.size());
|
order.reserve(entity_manager_.entityCount() + players_.size());
|
||||||
const auto ENTITIES = entity_manager_.getEntities();
|
// Mergem els dos sacs (map + actors) i els jugadors en un sol
|
||||||
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
// vector temporal — el `stable_sort` per `feet_y` posterior fa el
|
||||||
|
// z-order interleaving entre els tres orígens sense importar el
|
||||||
|
// sac d'on venen.
|
||||||
|
const auto MAP = entity_manager_.getMapEntities();
|
||||||
|
std::ranges::transform(MAP, std::back_inserter(order), [](Entity* e) {
|
||||||
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||||||
});
|
});
|
||||||
|
const auto ACTORS = entity_manager_.getActors();
|
||||||
|
std::ranges::transform(ACTORS, std::back_inserter(order), [](DynamicHazard* a) -> Renderable {
|
||||||
|
return Renderable{.feet_y = a->getFeetY(), .entity = a, .player = nullptr};
|
||||||
|
});
|
||||||
// Tots els jugadors entren al z-sort. Els fora de joc fan no-op a
|
// Tots els jugadors entren al z-sort. Els fora de joc fan no-op a
|
||||||
// `render()`; els actius (inclosos AIRBORNE durant l'spawn aeri)
|
// `render()`; els actius (inclosos AIRBORNE durant l'spawn aeri)
|
||||||
// es pinten amb el seu feet_y normal. La sombra que afecta la
|
// es pinten amb el seu feet_y normal. La sombra que afecta la
|
||||||
|
|||||||
Reference in new issue
Block a user