refactor(em): un sol vector entities_ — polimorfisme per la

discriminació

Reverteix la separació en sacs introduïda al commit 62f4b85 (rama
anterior). Amb els virtuals afegits al commit precedent
(`onWhipHit`, `onJustSpawned`, `shouldReceiveContact`, `onRemoved`),
ja no cal separar `map_entities_` i `actors_`: cada entitat decideix
internament què fa, el manager itera un sol vector i confia en el
dispatch virtual.

Canvis:

- `entities_` torna a ser el contenidor únic. Es desfà el dispatch
  `dynamic_cast<DynamicHazard*>` que `add()` feia per enrutar.
- `addSarcophagus`, `removeEntity`, `update`, `notifyPlayerTilePainted`,
  `findFloorTriggerAt`, `renderAll`, `reload` simplificats: un sol
  bucle, sense distingir categories.
- `checkPlayerCollisions` només recorre `entities_` cridant
  `shouldReceiveContact` polimòrfic — la lògica de "saltar SPAWN"
  viu a `DynamicHazard::shouldReceiveContact`.
- `getMapEntities()` i `getActors()` desapareixen. Es manté
  `getEntities()` com a accessor únic.
- `WhipAttack::applyDamage` itera `getEntities()` cridant `onWhipHit`
  polimòrfic. Les entitats no-whippables retornen false (cost: una
  crida virtual buida) i el callsite no discrimina tipus.
- `GameScene::render` mergeja entities + players (dos transforms en
  comptes de tres) per al z-order.

`pending_actors_` es manté: és un concepte ortogonal — entitats que
encara no s'han instanciat al món fins que GameScene les "spawneja"
al text-exit de ROOM_INTRO.

Beneficis del nou disseny:
- Cap dynamic_cast dispers per a la lògica de joc (només queden els
  inevitables: validació de factory i ruteig de la Door).
- Afegir un nou enemic implica `class X : public DynamicHazard` i
  override dels virtuals rellevants — res al manager.
- Si un sarc, antorxa, o decoració futura volgués reaccionar al
  fueteig (trencar-se, encendre's), basta amb un `onWhipHit` override
  i no cal tocar `WhipAttack::applyDamage`.
This commit is contained in:
JailDesigner committed 2026-05-11 13:14:53 +02:00
1 parent 28ff425c89
commit a29ea641ab
5 files changed
+61 -118

No files matched your search

@@ -57,11 +57,10 @@ void EntityManager::renderEntity(const Entity& e) {
}
void EntityManager::renderAll() {
// Sense ordenació per z: l'ordre és el d'inserció dins de cada sac.
// El render del joc real (GameScene) construeix el seu propi vector
// Sense ordenació per z: l'ordre és el d'inserció al vector. El
// render del joc real (GameScene) construeix el seu propi vector
// ordenat per `feet_y`; aquest helper queda per al editor i tests.
for (const auto& e : map_entities_) { renderEntity(*e); }
for (const auto& a : actors_) { renderEntity(*a); }
for (const auto& e : entities_) { renderEntity(*e); }
}
namespace {
@@ -100,19 +99,15 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
}
};
// Tots dos sacs apliquen el mateix filtre polimòrfic
// `shouldReceiveContact()`. Per defecte equival a `collides() ||
// triggersOnOverlap()`; `DynamicHazard` ho afina afegint el filtre de
// SPAWN (mentre està emergint torna false). Sense dynamic_cast,
// sense filtres específics al callsite.
for (const auto& entity : map_entities_) {
// Filtre polimòrfic unificat: cada entitat decideix si ha de rebre
// contacte (`shouldReceiveContact`). Per defecte equival a `collides()
// || triggersOnOverlap()`; `DynamicHazard` afegeix el filtre de SPAWN
// (mentre està emergint torna false). El callsite no fa dynamic_cast
// ni distingeix categories.
for (const auto& entity : entities_) {
if (!entity->shouldReceiveContact()) { continue; }
apply_contacts(entity.get());
}
for (const auto& actor : actors_) {
if (!actor->shouldReceiveContact()) { continue; }
apply_contacts(actor.get());
}
}
namespace {
@@ -410,8 +405,7 @@ void EntityManager::reload(const std::string& level_yaml_path) {
// subscripcions implícites desapareixen amb la seua vida (cap d'elles
// se subscriu directament al bus — només publiquen). El bus i les
// subscripcions externes (PaintLayer, HUD, GameScene) no es toquen.
map_entities_.clear();
actors_.clear();
entities_.clear();
pending_actors_.clear();
door_ = nullptr;
total_sarcs_ = 0;
@@ -475,9 +469,9 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
// perquè tots els sarcòfags ja estan registrats al fan-out (la
// subscripció del manager és al ctor, anterior a qualsevol load).
int decal_broadcast = 0;
// Els decals de sòl (Hole, futurs pinchos plans...) són StaticHazards
// i viuen exclusivament a `map_entities_`. Saltem `actors_` per estalviar.
for (const auto& entity : map_entities_) {
// Els decals de sòl (Hole, futurs pinchos plans...) són StaticHazards;
// iterem tot `entities_` i el filtre per `isFloorDecal()` els tria.
for (const auto& entity : entities_) {
if (!entity->isFloorDecal()) { continue; }
const auto BOX = entity->getCollisionRect();
const auto [COL, ROW] = Map::pixelToTile(BOX.x, BOX.y);
@@ -492,7 +486,7 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
Log::info("EntityManager", "{} tile_painted broadcast inicial (decals de sòl)", decal_broadcast);
}
Log::info("EntityManager", "{} map entities + {} actors pending des de {}", loaded, pending_actors_.size(), level_yaml_path);
Log::info("EntityManager", "{} entitats + {} actors pending des de {}", loaded, pending_actors_.size(), level_yaml_path);
}
void EntityManager::spawnPendingActors() {