refactor(entities): polimorfisme per substituir dynamic_casts

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_`.
This commit is contained in:
JailDesigner committed 2026-05-11 13:01:26 +02:00
1 parent e8418f89e0
commit 28ff425c89
11 files changed
+177 -58

No files matched your search

+26 -34
View File
@@ -262,19 +262,10 @@ void WhipAttack::update(float delta_time) {
namespace {
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction {
switch (d) {
case WhipAttack::Direction::NORTH:
return Mummy::Direction::NORTH;
case WhipAttack::Direction::SOUTH:
return Mummy::Direction::SOUTH;
case WhipAttack::Direction::EAST:
return Mummy::Direction::EAST;
case WhipAttack::Direction::WEST:
return Mummy::Direction::WEST;
}
return Mummy::Direction::NONE;
}
// (El mapping WhipAttack::Direction → Mummy::Direction viu ara a
// mummy.cpp, on s'usa des de `Mummy::onWhipHit`. Aquí només queda el
// mapping a Player::Cardinal — usat per al friendly-fire entre
// jugadors, que no és polimòrfic via Entity.)
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
switch (d) {
@@ -296,25 +287,24 @@ namespace {
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
}
// Tracta una mòmia candidata: si és whippable i la seua caja solapa
// amb `damage_rect`, la posa a HURT i la marca al set. Extret per
// acotar la complexitat cognitiva d'`applyDamage` (clang-tidy llindar).
// Rep un `DynamicHazard*` (l'iterador d'`applyDamage` ja filtra el
// sac d'actors); només cal el dynamic_cast a Mummy per descartar
// altres hermanes (RollingRock i futurs DynamicHazards no whippables).
void hitMummyIfApplicable(
DynamicHazard* actor,
// Tracta una entitat candidata: si la seva caixa solapa amb
// `damage_rect`, delega la decisió a `entity->onWhipHit(attack)`.
// Cada entitat decideix què fer (Mummy entra HURT, Torch retorna
// false sense efecte, RollingRock no override → no-op). El retorn
// true marca el set `already_hit` per evitar segons impactes dins del
// mateix swing. Polimorfisme net — sense dynamic_cast.
void hitEntityIfApplicable(
Entity* entity,
const SDL_FRect& damage_rect,
WhipAttack::Direction dir,
WhipAttack* attack,
std::unordered_set<void*>& already_hit) {
if (actor == nullptr || actor->isDead()) { return; }
if (already_hit.contains(static_cast<void*>(actor))) { return; }
auto* mummy = dynamic_cast<Mummy*>(actor);
if (mummy == nullptr || !mummy->isWhippable()) { return; }
const auto BOX = mummy->getCollisionRect();
if (entity == nullptr || entity->isDead()) { return; }
if (already_hit.contains(static_cast<void*>(entity))) { return; }
const auto BOX = entity->getCollisionRect();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
mummy->enterHurt(whipDirToMummyDir(dir));
already_hit.insert(static_cast<void*>(actor));
if (entity->onWhipHit(attack)) {
already_hit.insert(static_cast<void*>(entity));
}
}
void hitPlayerIfApplicable(
@@ -339,12 +329,14 @@ namespace {
void WhipAttack::applyDamage() {
if (entity_manager_ != nullptr) {
// Només els actors poden rebre el fuetejo (decoracions, sarcs,
// forats... no són hits del whip). Iterem el sac `actors_`
// directament — sense recórrer tot el mapa per descartar-ne la
// major part.
// Iterem el sac d'actors (DynamicHazards). Les entitats
// estàtiques (sarc, antorxa, forat, traps) no reben fueteig per
// disseny — la decisió viu en el flag de saco, no en filtres
// dispersos. Si en el futur cal que un sarc reaccioni al fueteig
// (p.ex. trencar-se), basta amb afegir un override de
// `onWhipHit` allà i iterar també getMapEntities().
for (auto* actor : entity_manager_->getActors()) {
hitMummyIfApplicable(actor, damage_rect_, direction_, already_hit_);
hitEntityIfApplicable(actor, damage_rect_, this, already_hit_);
}
}
if (players_ != nullptr) {