#include "game/entities/entity_manager.hpp" #include #include #include #include "game/entities/concrete/door.hpp" #include "game/entities/concrete/sarcophagus.hpp" #include "game/entities/entity.hpp" #include "game/map/map.hpp" // NOTA: `loadFromLevel` viu a `entity_manager_loadlevel.cpp` per evitar // arrossegar `Resource::Cache`, `Surface` i les seues dependències (GIF, // àudio) a consumidors que només volen la base del manager — en // particular, el smoke test stand-alone. EntityManager::EntityManager() { // Subscripció al bus: cada sarcòfag obert incrementa el comptador; // quan iguala el total registrat, obrim la porta. La subscripció // sobreviu tot el nivell. sarc_sub_id_ = bus_.subscribe("sarcophagus_opened", [this](const Event& /*ev*/) { ++opened_sarcs_; if (total_sarcs_ > 0 && opened_sarcs_ >= total_sarcs_ && door_ != nullptr) { door_->open(); } }); // Subscripció al pintat: el PaintLayer publica `tile_painted` amb // payload `TilePos` un cop ha filtrat pintabilitat i aplicat la regla // "primer que pinta guanya". Fem fan-out a totes les entitats perquè // els sarcòfags puguin actualitzar el seu `visited_`. tile_sub_id_ = bus_.subscribe("tile_painted", [this](const Event& ev) { const auto* tile = std::get_if(&ev.payload); if (tile != nullptr) { notifyPlayerTilePainted(tile->col, tile->row, ev.player_id); } }); // Subscripció a `player_exited` (publicat per Door en travessar la // porta oberta amb un jugador): comptem una vegada per jugador i, // quan tots els jugadors actius han eixit, emetem `level_complete` // — l'event d'agregat que GameScene escolta per disparar la // transició FSM a ROOM_OUTRO. La idempotència de Door (check // `isExited`) garanteix que cada jugador suma com a màxim un cop. exit_sub_id_ = bus_.subscribe("player_exited", [this](const Event& /*ev*/) { ++exited_players_; if (level_complete_emitted_) { return; } if (expected_players_ > 0 && exited_players_ >= expected_players_) { level_complete_emitted_ = true; bus_.publish(Event{ .name = "level_complete", .payload = std::monostate{}, .player_id = -1, }); } }); // Connectar amb el check global de col·lisió del Map: les entitats // sòlides genèriques (debris, columnes, futurs murs) viuen al manager, // no a la graella fixa de sarcs, així que el `Map::collides` del Player // les ha de consultar via callback. Es desregistra al dtor. Map::setEntityCollider([this](const SDL_FRect& box) { return entityCollidesAt(box); }); } EntityManager::~EntityManager() { bus_.unsubscribe(sarc_sub_id_); bus_.unsubscribe(tile_sub_id_); bus_.unsubscribe(exit_sub_id_); // Desconnectar del Map: si quedem en namespace global apuntant a un // `this` que mor, qualsevol `Map::collides` posterior cauria en UB. Map::setEntityCollider(nullptr); } void EntityManager::setExpectedPlayerCount(int n) { expected_players_ = n; exited_players_ = 0; level_complete_emitted_ = false; } void EntityManager::forceOpenDoor() { if (door_ != nullptr) { door_->open(); } } auto EntityManager::add(std::unique_ptr entity) -> Entity* { if (entity == nullptr) { return nullptr; } entity->setEventBus(&bus_); entity->setEntityManager(this); Entity* raw = entity.get(); entities_.push_back(std::move(entity)); // cppcheck-suppress returnDanglingLifetime return raw; } void EntityManager::setDoor(Door* door) { door_ = door; if (door_ != nullptr) { door_->setEventBus(&bus_); door_->setEntityManager(this); } } auto EntityManager::removeEntity(Entity* observer) -> bool { if (observer == nullptr) { return false; } const bool WAS_DOOR = (door_ == observer); // `onRemoved` és polimòrfic: cada subclasse decideix què cal // notificar quan se l'esborra. Avui només Sarcophagus l'override per // decrementar el seu comptador. La door es detecta per comparació de // punter (és única, no cal RTTI). observer->onRemoved(); auto it = std::ranges::find_if(entities_, [observer](const auto& up) { return up.get() == observer; }); if (it == entities_.end()) { return false; } entities_.erase(it); if (WAS_DOOR) { door_ = nullptr; } // No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot // disparar-se cap esdeveniment derivat. Quan el GameScene faça el // reload posterior, els comptadors es re-deriven d'inici. return true; } auto EntityManager::addSarcophagus(std::unique_ptr sarc) -> Sarcophagus* { if (sarc == nullptr) { return nullptr; } sarc->setEventBus(&bus_); sarc->setEntityManager(this); Sarcophagus* raw = sarc.get(); entities_.push_back(std::move(sarc)); ++total_sarcs_; // cppcheck-suppress returnDanglingLifetime return raw; } void EntityManager::update(float delta_time) { // ATENCIÓ: si en el futur una entitat publica al bus events que // resulten en `entity_manager_.add()` (push_back a `entities_`), açò // 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 // saltador es spawneja durant `Player::tick`, abans d'aquest mètode). for (auto& e : entities_) { e->update(delta_time); } // Purga d'entitats marcades mortes aquest frame (efectes acabats, // p.ex. polvet d'aterratge). Les persistents (sarcs, momies, etc.) // mai no es marquen. std::erase_if(entities_, [](const auto& e) { return e->isDead(); }); } void EntityManager::notifyPlayerTilePainted(int col, int row, int player_id) { // Fan-out polimòrfic a totes les entitats: cada una decideix què fer // (`Sarcophagus` actualitza el seu perímetre i recorda qui va tancar // el cercle, la resta queden en no-op). `player_id` ve del bus // (`ev.player_id`) — pot ser -1 al broadcast de càrrega inicial de // nivell, els consumidors filtren si necessiten atribució. for (auto& e : entities_) { e->notifyTilePainted(col, row, player_id); } } auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* { // Cerca lineal: només `Hole` (i futurs decals trigger) compleixen la // doble condició `triggersOnOverlap` + `isFloorDecal`. Costos típics // (poques desenes d'entitats) menyspreables, una crida per frame des // dels consumidors actius (Player a l'aterratge, Mòmia a la decisió // de direcció). for (const auto& entity : entities_) { if (!entity->triggersOnOverlap()) { continue; } if (!entity->isFloorDecal()) { continue; } const auto BOX = entity->getCollisionRect(); const bool OVERLAP = rect.x + rect.w > BOX.x && BOX.x + BOX.w > rect.x && rect.y + rect.h > BOX.y && BOX.y + BOX.h > rect.y; if (OVERLAP) { return entity.get(); } } return nullptr; } auto EntityManager::entityCollidesAt(const SDL_FRect& box) const -> bool { // Iteració lineal sobre entitats amb `blocksMovement()`. La convenció // d'overlap és la mateixa que `Map::collides` (estricta: el toc al // píxel no compta) per coherència en els passadissos del laberint. return std::ranges::any_of(entities_, [&box](const auto& e) { if (!e->blocksMovement()) { return false; } const auto R = e->getCollisionRect(); return (R.x + R.w) > box.x && (box.x + box.w) > R.x && (R.y + R.h) > box.y && (box.y + box.h) > R.y; }); } auto EntityManager::getEntities() const -> std::vector { std::vector out; out.reserve(entities_.size()); std::ranges::transform(entities_, std::back_inserter(out), [](const auto& e) { return e.get(); }); return out; } // NOTA: `renderAll` es defineix a `entity_manager_loadlevel.cpp`. Conté // blits via `Surface::render`, que dragaria el pipeline complet de render // (GIF, Canvas, Screen, àudio...) al smoke test. Separar-lo manté el smoke // autocontingut.