Quatre virtuals nous a `Entity` capten decisions que abans vivien al manager com a discriminacions de tipus: - `onWhipHit(WhipAttack*)` → bool. Default no-op/false. Override a `Mummy` per entrar HURT si és whippable. WhipAttack::applyDamage passa de `dynamic_cast<Mummy*>` + `isWhippable()` a una crida polimòrfica neta. RollingRock no override → immune al látigo per disseny, sense flag. - `onJustSpawned()`. Default no-op. Override a `DynamicHazard` per disparar `triggerSpawnExit()`. `spawnPendingActors` i `spawnActor` passen de `dynamic_cast<DynamicHazard*>(observer)->triggerSpawnExit()` a `observer->onJustSpawned()`. - `onRemoved()`. Default no-op. Override a `Sarcophagus` per cridar `entity_manager_->onSarcophagusRemoved()`. `removeEntity` passa de `getName() == "sarcophagus"` a dispatch polimòrfic. - `shouldReceiveContact()`. Default `collides() || triggersOnOverlap()`. Override a `DynamicHazard` per excloure SPAWN. `checkPlayerCollisions` passa de tres condicions inline per a sacs distints a una crida unificada. `whipDirToMummyDir` es mou de whip_attack.cpp a mummy.cpp — el mapping viu on s'usa (dins de `Mummy::onWhipHit`). `hitMummyIfApplicable` es renomena a `hitEntityIfApplicable`: ja no discrimina tipus, aplica `entity->onWhipHit` i marca `already_hit_` segons el retorn. Comportament idèntic, tests verds. Aquest commit prepara el següent: amb el polimorfisme madurat, el sac dual ja no aporta valor i podem tornar a un únic `entities_`.
206 lines
8.9 KiB
C++
206 lines
8.9 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();
|
|
// 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
|
|
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<Sarcophagus> 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<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;
|
|
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<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;
|
|
}
|
|
|
|
// 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.
|