From 28ff425c8915add4080feb0098ac8932f01b67d8 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 11 May 2026 13:01:26 +0200 Subject: [PATCH 1/2] refactor(entities): polimorfisme per substituir dynamic_casts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` + `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(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_`. --- source/game/entities/concrete/mummy.cpp | 28 +++++++++ source/game/entities/concrete/mummy.hpp | 6 ++ source/game/entities/concrete/sarcophagus.cpp | 13 ++++ source/game/entities/concrete/sarcophagus.hpp | 1 + source/game/entities/concrete/whip_attack.cpp | 60 ++++++++----------- source/game/entities/entity.cpp | 25 ++++++++ source/game/entities/entity.hpp | 33 ++++++++++ source/game/entities/entity_manager.cpp | 7 ++- source/game/entities/entity_manager.hpp | 9 +++ .../entities/entity_manager_loadlevel.cpp | 39 ++++++------ source/game/entities/hazard.hpp | 14 +++++ 11 files changed, 177 insertions(+), 58 deletions(-) diff --git a/source/game/entities/concrete/mummy.cpp b/source/game/entities/concrete/mummy.cpp index 11d54d1..b293655 100644 --- a/source/game/entities/concrete/mummy.cpp +++ b/source/game/entities/concrete/mummy.cpp @@ -10,6 +10,7 @@ #include "core/audio/audio.hpp" #include "core/resources/resource_cache.hpp" +#include "game/entities/concrete/whip_attack.hpp" #include "game/entities/entity_factory.hpp" #include "game/entities/entity_manager.hpp" #include "game/entities/event_bus.hpp" @@ -65,6 +66,24 @@ namespace { // més d'una lane (cas marginal a velocitats típiques, però pot passar // amb dt grans). Comparació estricta a un costat: no dispara quan la // caixa ja es troba alineada a la lane (hi està després del snap). + // Mapatge WhipAttack::Direction → Mummy::Direction. Viuen amb noms i + // valors paral·lels però són dos enums distints (cada classe modela el + // seu propi domini). Conversió 1-a-1; `NONE` no apareix com a possible + // sortida del whip, però la deixem per coherència defensiva. + 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; + } + auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional { const float NEXT = prev + delta; if (delta > 0.0F) { @@ -520,6 +539,15 @@ void Mummy::applyChoice(Direction dir) { } } +auto Mummy::onWhipHit(WhipAttack* attack) -> bool { + // Defensiu: el filtre `shouldReceiveContact` ja salta DynamicHazards en + // SPAWN, però blindem aquesta crida si arribara per un camí no central. + if (isSpawning()) { return false; } + if (!whippable_ || attack == nullptr) { return false; } + enterHurt(whipDirToMummyDir(attack->getDirection())); + return true; +} + void Mummy::onPlayerContact(Player* player) { // Defensiu: el filtre central a EntityManager::checkPlayerCollisions ja // salta DynamicHazards en SPAWN, però blindem la crida directa. diff --git a/source/game/entities/concrete/mummy.hpp b/source/game/entities/concrete/mummy.hpp index c2c246e..3a766a9 100644 --- a/source/game/entities/concrete/mummy.hpp +++ b/source/game/entities/concrete/mummy.hpp @@ -64,6 +64,12 @@ class Mummy : public DynamicHazard { // Contacte amb el jugador: publica `player_hit` amb el dany. void onPlayerContact(Player* player) override; + // Contacte amb el fueteig: si la mòmia és whippable, entra a HURT amb + // la direcció derivada del whip. Retorna true si efectivament ha + // rebut dany — el WhipAttack ho usa per al `already_hit_`. Substitueix + // el `dynamic_cast` que feia el whip abans per discriminar. + auto onWhipHit(WhipAttack* attack) -> bool override; + // Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`. // Si està bloquejada, prova perpendiculars; si totes estan bloquejades, // la mòmia queda quieta però en HURT durant tot el timer. diff --git a/source/game/entities/concrete/sarcophagus.cpp b/source/game/entities/concrete/sarcophagus.cpp index 44b4147..d001b7b 100644 --- a/source/game/entities/concrete/sarcophagus.cpp +++ b/source/game/entities/concrete/sarcophagus.cpp @@ -4,6 +4,7 @@ #include #include "game/entities/entity_factory.hpp" +#include "game/entities/entity_manager.hpp" #include "game/entities/event_bus.hpp" namespace { @@ -124,4 +125,16 @@ void Sarcophagus::notifyTilePainted(int col, int row) { } } +void Sarcophagus::onRemoved() { + // El manager comptabilitza els sarcòfags actius en `total_sarcs_` per + // saber quan ha d'obrir la porta automàticament. Quan el editor + // n'esborra un, cal informar-lo: sense aquesta crida, `total_sarcs_` + // quedaria descompassat i la porta no s'obriria mai. Substitueix el + // `getName() == "sarcophagus"` que el manager feia abans — + // polimorfisme net, no comparació de strings. + if (entity_manager_ != nullptr) { + entity_manager_->onSarcophagusRemoved(); + } +} + REGISTER_ENTITY("sarcophagus", Sarcophagus) diff --git a/source/game/entities/concrete/sarcophagus.hpp b/source/game/entities/concrete/sarcophagus.hpp index 201701b..df70bfc 100644 --- a/source/game/entities/concrete/sarcophagus.hpp +++ b/source/game/entities/concrete/sarcophagus.hpp @@ -43,6 +43,7 @@ class Sarcophagus : public Interactable { void update(float delta_time) override; void notifyTilePainted(int col, int row) override; + void onRemoved() override; [[nodiscard]] auto getState() const -> State { return state_; } [[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); } diff --git a/source/game/entities/concrete/whip_attack.cpp b/source/game/entities/concrete/whip_attack.cpp index 90f49ed..3860816 100644 --- a/source/game/entities/concrete/whip_attack.cpp +++ b/source/game/entities/concrete/whip_attack.cpp @@ -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& already_hit) { - if (actor == nullptr || actor->isDead()) { return; } - if (already_hit.contains(static_cast(actor))) { return; } - auto* mummy = dynamic_cast(actor); - if (mummy == nullptr || !mummy->isWhippable()) { return; } - const auto BOX = mummy->getCollisionRect(); + if (entity == nullptr || entity->isDead()) { return; } + if (already_hit.contains(static_cast(entity))) { return; } + const auto BOX = entity->getCollisionRect(); if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; } - mummy->enterHurt(whipDirToMummyDir(dir)); - already_hit.insert(static_cast(actor)); + if (entity->onWhipHit(attack)) { + already_hit.insert(static_cast(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) { diff --git a/source/game/entities/entity.cpp b/source/game/entities/entity.cpp index 092451a..d8586ca 100644 --- a/source/game/entities/entity.cpp +++ b/source/game/entities/entity.cpp @@ -28,6 +28,31 @@ void Entity::notifyTilePainted(int /*col*/, int /*row*/) { // No-op per defecte; només `Sarcophagus` l'usa. } +auto Entity::onWhipHit(WhipAttack* /*attack*/) -> bool { + // No-op per defecte; les entitats no-whippables (Torch, Hole, Sarc, + // Door, RollingRock...) retornen `false`. `Mummy` n'és l'override + // efectiu. El caller usa el retorn per marcar `already_hit_` — + // entitats que no han rebut dany NO bloquegen futurs frames del whip. + return false; +} + +void Entity::onJustSpawned() { + // No-op per defecte. `DynamicHazard` override per disparar el seu + // `triggerSpawnExit` (anima la fase EMERGING just després de néixer). +} + +void Entity::onRemoved() { + // No-op per defecte. `Sarcophagus` override per decrementar el + // comptador `total_sarcs_` del manager. +} + +auto Entity::shouldReceiveContact() const -> bool { + // Default: l'entitat rep contacte si és sòlida (`collides_`) o si és + // un trigger d'àrea (`triggers_on_overlap_`). `DynamicHazard` afegeix + // el filtre de SPAWN — mentre està emergint, ignora contactes. + return collides_ || triggers_on_overlap_; +} + auto Entity::getCollisionRect() const -> SDL_FRect { if (!collision_box_.has_value()) { return pixel_rect_; } return { diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index 88808a3..3a949d7 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -19,6 +19,7 @@ class Player; class EventBus; class Surface; class EntityManager; +class WhipAttack; // Entitat genèrica del món. Actua com a dada en comú per a tots els actors // del joc (decoratius, interactables, perills mòbils i estàtics): manté @@ -86,6 +87,38 @@ class Entity { // (per marcar perímetre); per defecte no-op. virtual void notifyTilePainted(int col, int row); + // L'entitat ha rebut un fueteig amb solapament. Es passa el punter al + // `WhipAttack` perquè la subclasse pugui llegir-ne direcció o atributs + // (mateix patró que `onPlayerContact(Player*)`). Retorna `true` si + // efectivament ha aplicat dany — el caller en marca el set + // `already_hit_` per evitar segons impactes dins del mateix swing. + // Default: no fa res, retorna `false`. Override a `Mummy` (whippable) + // i a futurs enemics whippables. `RollingRock` no override → roca + // immune al látigo per defecte. + virtual auto onWhipHit(WhipAttack* attack) -> bool; + + // Hook just després de néixer al món. El manager el crida quan + // `addSpawn` / `spawnActor` / `spawnPendingActors` instancien una + // entitat. La base no fa res; `DynamicHazard` override per disparar + // el seu `triggerSpawnExit` (anima la fase EMERGING). Substitueix el + // `dynamic_cast(observer)->triggerSpawnExit()` que el + // manager feia abans — ara el dispatch és polimòrfic. + virtual void onJustSpawned(); + + // Hook just abans que el manager esborri l'entitat (via + // `removeEntity`, típicament al editor). Permet a subclasses + // notificar al manager o publicar events per mantenir invariants + // (p.ex. `Sarcophagus` decrementa `total_sarcs_`). Default no-op. + virtual void onRemoved(); + + // Decisió polimòrfica de si aquesta entitat ha de rebre el callback + // `onPlayerContact` aquest frame. La base permet contacte si `collides_` + // o `triggers_on_overlap_` estan posats. Subclasses poden afegir + // condicions transitòries (p.ex. `DynamicHazard` retorna `false` mentre + // està en SPAWN). Centralitza el filtre que abans vivia al manager + // amb dynamic_casts/isSpawning explícits. + [[nodiscard]] virtual auto shouldReceiveContact() const -> bool; + [[nodiscard]] auto getName() const -> const std::string& { return name_; } [[nodiscard]] auto getTilePos() const -> TilePos { return tile_; } [[nodiscard]] auto getPixelRect() const -> SDL_FRect { return pixel_rect_; } diff --git a/source/game/entities/entity_manager.cpp b/source/game/entities/entity_manager.cpp index 7e838e2..3a148a1 100644 --- a/source/game/entities/entity_manager.cpp +++ b/source/game/entities/entity_manager.cpp @@ -102,7 +102,11 @@ void EntityManager::setDoor(Door* door) { auto EntityManager::removeEntity(Entity* observer) -> bool { if (observer == nullptr) { return false; } const bool WAS_DOOR = (door_ == observer); - const bool WAS_SARC = (observer->getName() == "sarcophagus"); + // `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()) { @@ -113,7 +117,6 @@ auto EntityManager::removeEntity(Entity* observer) -> bool { return false; } 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. diff --git a/source/game/entities/entity_manager.hpp b/source/game/entities/entity_manager.hpp index affc0b5..1eeab28 100644 --- a/source/game/entities/entity_manager.hpp +++ b/source/game/entities/entity_manager.hpp @@ -173,6 +173,15 @@ class EntityManager { // sarcs. No-op si la porta ja està oberta o si el nivell no en té. void forceOpenDoor(); + // Hook que `Sarcophagus::onRemoved` crida per decrementar el + // comptador `total_sarcs_`. Públic perquè el sarcòfag és la única + // entitat que necessita aquest manteniment, però l'API es manté + // genèrica: si en el futur Door volgués notificar quelcom semblant, + // afegim el seu propi callback. + void onSarcophagusRemoved() { + if (total_sarcs_ > 0) { --total_sarcs_; } + } + private: EventBus bus_; // Dos sacs lògics: diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index d3b2bf8..f1a0ee9 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -100,20 +100,17 @@ void EntityManager::checkPlayerCollisions(const std::vector& players) { } }; - // Sac de mapa: filtre per col·lisió / trigger. Tanquem el gap latent - // de FloorTrap/WallTrap (que tenien `collides_=false` i mai no es - // disparaven) i obrim la porta a hazards no-físics com `Hole`. + // 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_) { - if (!entity->collides() && !entity->triggersOnOverlap()) { continue; } + if (!entity->shouldReceiveContact()) { continue; } apply_contacts(entity.get()); } - // Sac d'actors (DynamicHazards): tots fan dany quan són actius. - // Saltem els que estan en SPAWN: visibles però sense interacció - // durant la fase inicial de ROOM_INTRO i durant futurs spawns en - // runtime. Sense dynamic_cast: el tipus ja és DynamicHazard. for (const auto& actor : actors_) { - if (actor->isSpawning()) { continue; } - if (!actor->collides() && !actor->triggersOnOverlap()) { continue; } + if (!actor->shouldReceiveContact()) { continue; } apply_contacts(actor.get()); } } @@ -500,17 +497,13 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) { void EntityManager::spawnPendingActors() { // Itera els pending i els instancia via `addSpawn`. El dispatch d'`add()` - // els enruta a `actors_`. El `triggerSpawnExit()` posterior arma el - // comptador de SPAWN — la mòmia neix ja en EMERGING, sense esperes. + // els enruta a `actors_`. `onJustSpawned()` és polimòrfic: la base + // no fa res, DynamicHazard l'override per disparar `triggerSpawnExit()` + // i entrar a EMERGING. Sense dynamic_cast: la decisió viu dins de + // cada entitat. for (const auto& pa : pending_actors_) { Entity* observer = addSpawn(pa.spawn_node); - if (observer == nullptr) { continue; } - // Per categoria YAML (`type: dynamic_hazard`) sabem que és - // DynamicHazard — el dynamic_cast aquí val zero i evita - // assumptions implícites sobre la jerarquia. - if (auto* dh = dynamic_cast(observer); dh != nullptr) { - dh->triggerSpawnExit(); - } + if (observer != nullptr) { observer->onJustSpawned(); } } pending_actors_.clear(); } @@ -530,7 +523,9 @@ auto EntityManager::spawnActor(const std::string& type, int col, int row) -> Dyn } Entity* observer = addSpawn(spawn); if (observer == nullptr) { return nullptr; } - auto* dh = dynamic_cast(observer); - if (dh != nullptr) { dh->triggerSpawnExit(); } - return dh; + // `onJustSpawned` polimòrfic: per a DynamicHazards arma EMERGING; per + // a un type erroni (no dynamic_hazard) queda no-op i retornem nullptr + // mitjançant el cast final, deixant la signatura `DynamicHazard*` neta. + observer->onJustSpawned(); + return dynamic_cast(observer); } diff --git a/source/game/entities/hazard.hpp b/source/game/entities/hazard.hpp index 641dd4c..b200b57 100644 --- a/source/game/entities/hazard.hpp +++ b/source/game/entities/hazard.hpp @@ -99,6 +99,20 @@ class DynamicHazard : public Hazard { [[nodiscard]] auto isSpawning() const -> bool { return spawn_state_ == SpawnState::SPAWNING; } [[nodiscard]] auto getSpawnDurationS() const -> float { return spawn_duration_s_; } + // Just després d'instanciar-se al món (via spawnPendingActors / + // spawnActor), l'actor entra automàticament en la fase EMERGING: + // arma el comptador i comença l'animació de sortida. Substitueix el + // dynamic_cast que el manager feia abans per cridar `triggerSpawnExit`. + void onJustSpawned() override { triggerSpawnExit(); } + + // Mentre està en SPAWN (HIDDEN/EMERGING), l'actor NO rep contacte + // ni amb el jugador ni amb attacks: visible però "no juga". Quan + // passa a ACTIVE, recupera el comportament estàndard d'Entity. + [[nodiscard]] auto shouldReceiveContact() const -> bool override { + if (isSpawning()) { return false; } + return Entity::shouldReceiveContact(); + } + // Política de moviment per frame. La base desplaça el pixel_rect_ // proporcionalment a velocitat i direcció; les subclasses amb IA la // sobreescriuen per triar nova direcció a les interseccions o rebotar. From a29ea641abc077518e3b367a95e4bf01e547e9af Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 11 May 2026 13:14:53 +0200 Subject: [PATCH 2/2] =?UTF-8?q?refactor(em):=20un=20sol=20vector=20`entiti?= =?UTF-8?q?es=5F`=20=E2=80=94=20polimorfisme=20per=20la=20=20discriminaci?= =?UTF-8?q?=C3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` 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`. --- source/game/entities/concrete/whip_attack.cpp | 16 ++-- source/game/entities/entity_manager.cpp | 81 +++++-------------- source/game/entities/entity_manager.hpp | 34 ++++---- .../entities/entity_manager_loadlevel.cpp | 34 ++++---- source/game/scenes/game_scene.cpp | 14 +--- 5 files changed, 61 insertions(+), 118 deletions(-) diff --git a/source/game/entities/concrete/whip_attack.cpp b/source/game/entities/concrete/whip_attack.cpp index 3860816..aa03d57 100644 --- a/source/game/entities/concrete/whip_attack.cpp +++ b/source/game/entities/concrete/whip_attack.cpp @@ -329,14 +329,14 @@ namespace { void WhipAttack::applyDamage() { if (entity_manager_ != nullptr) { - // 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()) { - hitEntityIfApplicable(actor, damage_rect_, this, already_hit_); + // Iterem totes les entitats. La discriminació la fa cada una via + // `onWhipHit`: Mummy entra HURT si és whippable, la resta retorna + // false (no-op). El cost d'iterar antorxes/sarcs/forats és una + // crida virtual buida cada una — menyspreable comparat amb la + // claredat del callsite. Si demà un sarc vol trencar-se al + // fueteig, només cal override de `onWhipHit` allà. + for (auto* entity : entity_manager_->getEntities()) { + hitEntityIfApplicable(entity, damage_rect_, this, already_hit_); } } if (players_ != nullptr) { diff --git a/source/game/entities/entity_manager.cpp b/source/game/entities/entity_manager.cpp index 3a148a1..2bdce84 100644 --- a/source/game/entities/entity_manager.cpp +++ b/source/game/entities/entity_manager.cpp @@ -74,19 +74,7 @@ auto EntityManager::add(std::unique_ptr entity) -> Entity* { 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)); - } + entities_.push_back(std::move(entity)); // cppcheck-suppress returnDanglingLifetime return raw; } @@ -107,15 +95,9 @@ auto EntityManager::removeEntity(Entity* observer) -> bool { // 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; - } + 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; } // No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot // disparar-se cap esdeveniment derivat. Quan el GameScene faça el @@ -128,8 +110,7 @@ auto EntityManager::addSarcophagus(std::unique_ptr sarc) -> Sarcoph 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)); + entities_.push_back(std::move(sarc)); ++total_sarcs_; // cppcheck-suppress returnDanglingLifetime return raw; @@ -137,34 +118,32 @@ auto EntityManager::addSarcophagus(std::unique_ptr sarc) -> Sarcoph 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çò + // 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 : map_entities_) { e->update(delta_time); } - for (auto& a : actors_) { a->update(delta_time); } + 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(map_entities_, [](const auto& e) { return e->isDead(); }); - std::erase_if(actors_, [](const auto& a) { return a->isDead(); }); + std::erase_if(entities_, [](const auto& e) { return e->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); } + // Fan-out polimòrfic a totes les entitats: cada una decideix què fer + // (`Sarcophagus` actualitza el seu perímetre, la resta queden en + // no-op). Cost d'iterar 40 elements amb crida virtual buida ~ 1 µs; + // qui paga el cost guanya simplicitat al callsite. + for (auto& e : entities_) { e->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_) { + // Cerca lineal: només `Hole` (i futurs decals trigger) compleixen la + // doble condició `triggersOnOverlap` + `isFloorDecal`. Costos típics + // (poques desenes d'entitats) menyspreables, una crida per frame des + // dels consumidors actius (Player a l'aterratge, Mòmia a la decisió + // de direcció). + for (const auto& entity : entities_) { if (!entity->triggersOnOverlap()) { continue; } if (!entity->isFloorDecal()) { continue; } const auto BOX = entity->getCollisionRect(); @@ -175,27 +154,9 @@ auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Ent } 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(); }); + out.reserve(entities_.size()); + std::ranges::transform(entities_, std::back_inserter(out), [](const auto& e) { return e.get(); }); return out; } diff --git a/source/game/entities/entity_manager.hpp b/source/game/entities/entity_manager.hpp index 1eeab28..6432762 100644 --- a/source/game/entities/entity_manager.hpp +++ b/source/game/entities/entity_manager.hpp @@ -9,12 +9,12 @@ #include "external/fkyaml_node.hpp" #include "game/entities/event_bus.hpp" -#include "game/entities/hazard.hpp" class Entity; class Sarcophagus; class Door; class Player; +class DynamicHazard; // Contenidor i coordinador d'entitats d'un nivell. Responsabilitats: // 1. `add` registra una entitat, la connecta al bus i manté propietat. @@ -74,14 +74,12 @@ class EntityManager { static void renderEntity(const Entity& entity); // Accés als punters observacionals a les entitats per a consumidors - // externs que necessiten construir una llista ordenada (z-order). - // `getEntities()` concatena els dos sacs interns (mapa + actors) per - // a consumidors retro-compat (editor, z-sort debug). `getMapEntities` - // i `getActors` exposen cada sac per separat — útil per a iteracions - // que ja saben quin tipus volen i estalvien filtres O(n). + // externs que necessiten iterar (z-order, aplicar dany, etc.). El + // polimorfisme dins de cada entitat fa la discriminació: l'iterador + // recorre tot i les entitats que no hagin de respondre tornen no-op + // (p.ex. Torch::onWhipHit retorna false sense efecte). Sense sacs + // separats — el `if (entity_is_X)` viu dins de l'entitat, no del caller. [[nodiscard]] auto getEntities() const -> std::vector; - [[nodiscard]] auto getMapEntities() const -> std::vector; - [[nodiscard]] auto getActors() const -> std::vector; // Comprova contacte del(s) jugador(s) amb entitats `collides()=true`. // Si la caixa base del jugador solapa amb la caixa de col·lisió de @@ -160,7 +158,7 @@ class EntityManager { void setExpectedPlayerCount(int n); [[nodiscard]] auto getBus() -> EventBus& { return bus_; } - [[nodiscard]] auto entityCount() const -> std::size_t { return map_entities_.size() + actors_.size(); } + [[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); } [[nodiscard]] auto pendingActorCount() const -> std::size_t { return pending_actors_.size(); } [[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; } [[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; } @@ -184,17 +182,13 @@ class EntityManager { private: EventBus bus_; - // Dos sacs lògics: - // `map_entities_` — decoració, static_hazards (Hole, FloorTrap, - // WallTrap) i interactables (Sarcophagus, Door). - // Cargades al loadFromLevel i persistents durant - // tota la sala. - // `actors_` — DynamicHazards (Mummy, RollingRock, futurs - // enemics). Separats per eliminar dynamic_casts - // i preparar el spawn diferit (commit posterior). - // `add()` enruta automàticament segons sigui o no DynamicHazard. - std::vector> map_entities_; - std::vector> actors_; + // Un sol contenidor lògic per a totes les entitats instanciades. La + // discriminació de tipus la fan els virtuals (`onPlayerContact`, + // `onWhipHit`, `shouldReceiveContact`, `notifyTilePainted`...) — el + // manager itera i cada entitat decideix què fa. Hi conviuen + // decoracions, static_hazards, interactables i dynamic_hazards + // (actors). + std::vector> entities_; // Cua d'actors declarats al YAML del mapa però encara no instanciats: // `loadFromLevel` hi empeny tots els DynamicHazards. `spawnPendingActors` // o `spawnActor` consumeixen aquesta cua (o creen un actor on-demand) diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index f1a0ee9..cda17b6 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -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& 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() { diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 1ad5b06..35eee74 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -1793,18 +1793,12 @@ void GameScene::render() { }; std::vector order; order.reserve(entity_manager_.entityCount() + players_.size()); - // Mergem els dos sacs (map + actors) i els jugadors en un sol - // vector temporal — el `stable_sort` per `feet_y` posterior fa el - // z-order interleaving entre els tres orígens sense importar el - // sac d'on venen. - const auto MAP = entity_manager_.getMapEntities(); - std::ranges::transform(MAP, std::back_inserter(order), [](Entity* e) { + // Mergem entitats i jugadors en un sol vector temporal — el + // `stable_sort` per `feet_y` posterior fa el z-order interleaving. + const auto ENTITIES = entity_manager_.getEntities(); + std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) { return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr}; }); - const auto ACTORS = entity_manager_.getActors(); - std::ranges::transform(ACTORS, std::back_inserter(order), [](DynamicHazard* a) -> Renderable { - return Renderable{.feet_y = a->getFeetY(), .entity = a, .player = nullptr}; - }); // Tots els jugadors entren al z-sort. Els fora de joc fan no-op a // `render()`; els actius (inclosos AIRBORNE durant l'spawn aeri) // es pinten amb el seu feet_y normal. La sombra que afecta la