#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(); // 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(entity.get()) != nullptr) { // Transferim l'ownership al unique_ptr 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 hazard{static_cast(entity.release())}; actors_.push_back(std::move(hazard)); } else { map_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(); // Busquem en els dos sacs. El sarc i la door sempre viuen a // `map_entities_` per disseny; un actor (DynamicHazard) viu a `actors_`. 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; } // 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(); // El sarc és Interactable, viu al sac `map_entities_`. map_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 als sacs), 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 : map_entities_) { e->update(delta_time); } for (auto& a : actors_) { a->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(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) { // 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* { // Cerca lineal sobre `map_entities_`: els floor decals trigger són // exclusivament StaticHazards (Hole; en el futur pinchos plans, // plaques, charcos). Els actors no qualifiquen com a decals — saltem // directament aquest sac, sense `triggersOnOverlap`/`isFloorDecal` per // a cada un. La doble condició filtra dins de map_entities_. for (const auto& entity : map_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 { // 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 out; out.reserve(map_entities_.size() + actors_.size()); 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 { std::vector 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 { std::vector out; out.reserve(actors_.size()); std::ranges::transform(actors_, std::back_inserter(out), [](const auto& a) { return a.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.