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.