#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" // 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); } }); // 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, }); } }); } EntityManager::~EntityManager() { bus_.unsubscribe(sarc_sub_id_); bus_.unsubscribe(tile_sub_id_); bus_.unsubscribe(exit_sub_id_); } 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); const bool WAS_SARC = (observer->getName() == "sarcophagus"); 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; } if (WAS_SARC && total_sarcs_ > 0) { --total_sarcs_; } // 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) { for (auto& e : entities_) { e->notifyTilePainted(col, row); } } auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* { // Cerca lineal sobre `entities_`: cost menyspreable amb poques // desenes d'entitats per sala i una crida per frame des dels // consumidors actius (Player a l'aterratge, Mòmia a la decisió de // direcció). Si esdevé calent, es podria indexar per cel·la del // tilemap. La doble condició filtra exclusivament decals de sòl que // volen rebre triggers — avui Hole; en el futur pinchos plans, // plaques de pressió, charcos. 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::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.