Files
aee-arcade/source/game/entities/entity_manager.cpp
T

162 lines
6.5 KiB
C++

#include "game/entities/entity_manager.hpp"
#include <algorithm>
#include <iterator>
#include <utility>
#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<TilePos>(&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) -> 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<Sarcophagus> 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<Entity*> {
std::vector<Entity*> 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.