diff --git a/source/game/entities/concrete/mummy.cpp b/source/game/entities/concrete/mummy.cpp index b13cd29..5612428 100644 --- a/source/game/entities/concrete/mummy.cpp +++ b/source/game/entities/concrete/mummy.cpp @@ -8,6 +8,7 @@ #include #include "game/entities/entity_factory.hpp" +#include "game/entities/entity_manager.hpp" #include "game/entities/event_bus.hpp" #include "game/entities/player.hpp" #include "game/map/map.hpp" @@ -235,8 +236,15 @@ void Mummy::step(float delta_time) { // Cal girar. Preferim perpendiculars; només fem U-turn si no hi ha cap // altra opció (comportament típic d'Oh Mummy!: no es recula sense motiu). - const auto POOLS = collectDirectionPools(speed_ * delta_time); - const auto& pool = !POOLS.perp.empty() ? POOLS.perp : POOLS.uturn; + // Primer intentem amb el filtre de decals (Hole) actiu; si totes les + // alternatives apunten a un decal trigger, fallback al pool sense filtre + // — millor caure al forat que quedar-se paralitzada en una sala mal + // dissenyada (la mòmia no mor pels forats). + auto pools = collectDirectionPools(speed_ * delta_time, /*avoid_decals=*/true); + if (pools.perp.empty() && pools.uturn.empty()) { + pools = collectDirectionPools(speed_ * delta_time, /*avoid_decals=*/false); + } + const auto& pool = !pools.perp.empty() ? pools.perp : pools.uturn; applyChoice(chooseDirection(pool)); // No avancem aquest frame: la direcció nova s'aplicarà al pròxim step. } @@ -260,24 +268,38 @@ auto Mummy::currentDirection() const -> Direction { void Mummy::decideAtIntersection(float step_px) { // Pool base: perpendiculars lliures (collectDirectionPools ja exclou la // direcció actual i la mitja volta va al seu propi calaix, que ací - // ignorem — mai fem U-turn a un creuament). - auto pools = collectDirectionPools(step_px); + // ignorem — mai fem U-turn a un creuament). Primera passa amb filtre + // de decals (Hole) actiu. + auto pools = collectDirectionPools(step_px, /*avoid_decals=*/true); std::vector pool = std::move(pools.perp); // Afegim la recta si segueix essent lliure. A un creuament "net" ho és // sempre (la mòmia hi ha arribat per aquest corredor), però als creua- // ments de la vora del playfield la recta pot estar al límit i no - // permetre un pas més — en aquest cas la descartem. + // permetre un pas més — en aquest cas la descartem. També la descartem + // si apunta a un decal trigger: aquesta és la regla "evita forats" en + // mode "seguir recte". const auto STRAIGHT_BOX = candidateBox(dir_x_ * step_px, dir_y_ * step_px); - if (Map::insidePlayfield(STRAIGHT_BOX) && !Map::collides(STRAIGHT_BOX)) { + const bool STRAIGHT_OPEN = Map::insidePlayfield(STRAIGHT_BOX) && !Map::collides(STRAIGHT_BOX); + if (STRAIGHT_OPEN && !isDecalAheadInDir(dir_x_, dir_y_)) { pool.push_back(currentDirection()); } + // Fallback: si totes les opcions filtrades porten a decal o estan + // bloquejades, repetim el pool sense filtre i tornem a afegir la recta + // si és lliure (encara que vaja a forat). Així la mòmia mai es queda + // paralitzada en un creuament rodejat de forats. + if (pool.empty()) { + auto pools_unfiltered = collectDirectionPools(step_px, /*avoid_decals=*/false); + pool = std::move(pools_unfiltered.perp); + if (STRAIGHT_OPEN) { pool.push_back(currentDirection()); } + } + if (pool.empty()) { return; } applyChoice(chooseDirection(pool)); } -auto Mummy::collectDirectionPools(float step) const -> DirectionPools { +auto Mummy::collectDirectionPools(float step, bool avoid_decals) const -> DirectionPools { struct Candidate { float dx; float dy; @@ -297,6 +319,11 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools { if (IS_CURRENT) { continue; } const auto TEST = candidateBox(c.dx, c.dy); if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; } + // Filtre de decals (Hole i futurs decals trigger): comprova el + // tile cardinal contigu; si conté un trigger, descartem la + // direcció. El caller pot demanar `avoid_decals=false` com a + // fallback quan totes les filtrades estan bloquejades. + if (avoid_decals && isDecalAheadInDir(c.dx, c.dy)) { continue; } const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F); if (IS_UTURN) { pools.uturn.push_back(c.dir); @@ -307,6 +334,31 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools { return pools; } +auto Mummy::isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool { + // Sense manager injectat (smoke tests sense `EntityManager::add`), el + // filtre és no-op: la mòmia es comporta exactament com abans del feature. + if (entity_manager_ == nullptr) { return false; } + if (dx_signed == 0.0F && dy_signed == 0.0F) { return false; } + // El rect a consultar és el AABB del tile cardinal contigu (mida + // `Map::tile_size`), NO el AABB de la mòmia desplaçat un pas — així + // captura "si la mòmia estiguera centrada en aquell tile, hi pisaria + // un decal?" amb marge. + const auto BOX = getCollisionRect(); + const auto TS = static_cast(Map::tile_size); + const auto SIGN_OF = [](float v) -> float { + if (v > 0.0F) { return 1.0F; } + if (v < 0.0F) { return -1.0F; } + return 0.0F; + }; + const SDL_FRect NEXT{ + .x = BOX.x + (SIGN_OF(dx_signed) * TS), + .y = BOX.y + (SIGN_OF(dy_signed) * TS), + .w = TS, + .h = TS, + }; + return entity_manager_->findFloorTriggerAt(NEXT) != nullptr; +} + void Mummy::applyChoice(Direction dir) { switch (dir) { case Direction::NORTH: diff --git a/source/game/entities/concrete/mummy.hpp b/source/game/entities/concrete/mummy.hpp index 9e78db3..d408cca 100644 --- a/source/game/entities/concrete/mummy.hpp +++ b/source/game/entities/concrete/mummy.hpp @@ -85,12 +85,25 @@ class Mummy : public DynamicHazard { // Recull les direccions alternatives viables des de la posició actual: // `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou // perquè ja sabem que està bloquejada. Cada candidata es valida amb - // `Map::insidePlayfield` + `Map::collides` sobre la caja resultant. + // `Map::insidePlayfield` + `Map::collides` sobre la caja resultant. Si + // `avoid_decals` és true i hi ha `EntityManager` injectat, també es + // descarten les direccions que apunten a un tile amb un decal trigger + // (Hole o futurs pinchos plans/charcos). El caller és responsable de + // re-cridar amb `avoid_decals=false` com a fallback si totes les + // direccions filtrades estan bloquejades. struct DirectionPools { std::vector perp; std::vector uturn; }; - [[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools; + [[nodiscard]] auto collectDirectionPools(float step, bool avoid_decals = true) const -> DirectionPools; + + // True si el tile cardinal contigu en la direcció (signe de dx, signe + // de dy) conté un decal trigger (`is_floor_decal_ && triggers_on_overlap_`) + // segons `EntityManager::findFloorTriggerAt`. Captura `Hole` i qualsevol + // futur decal trigger sense hardcodejar tipus. Si `entity_manager_` no + // està injectat (smoke tests sense manager), retorna false silenciosament + // — la mòmia es comporta com abans del feature. + [[nodiscard]] auto isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool; // Canvi de direcció + selecció de l'animació corresponent (un cardinal // per branch). Tots els camins que trien nova direcció (ctor amb diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index 7f2c937..88808a3 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -12,9 +12,13 @@ // `source/game/entities/player.hpp` i només es referencia per referència // dins `onPlayerContact`; `EventBus` és opcional a cada entitat i viu al // seu header propi. `Surface` és el backend de render paletitzat del motor. +// `EntityManager` només es referencia per punter (injectat opcionalment +// per a entitats que necessiten consultar el món d'entitats — p.ex. la +// mòmia per saber si el pròxim tile té un decal trigger tipus Hole). class Player; class EventBus; class Surface; +class EntityManager; // 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é @@ -122,6 +126,14 @@ class Entity { // estàtiques de moment) el poden ignorar. void setEventBus(EventBus* bus) { bus_ = bus; } + // Injecció opcional del manager d'entitats (mateix patró que + // `setEventBus`: el manager s'auto-injecta a `add`/`setDoor`/ + // `addSarcophagus`). El consumeixen entitats que volen consultar el + // món d'entitats — avui la mòmia, per saber si el tile següent + // conté un decal trigger (Hole) i evitar caure-hi. Entitats que no + // l'usen el poden ignorar. + void setEntityManager(EntityManager* manager) { entity_manager_ = manager; } + // Assigna la caixa de render en píxels. La invoca `EntityManager` durant // `loadFromLevel` un cop sap la posició definitiva de l'entitat al mapa. void setPixelRect(SDL_FRect rect) { pixel_rect_ = rect; } @@ -199,4 +211,5 @@ class Entity { bool dead_{false}; SDL_FlipMode flip_{SDL_FLIP_NONE}; EventBus* bus_{nullptr}; + EntityManager* entity_manager_{nullptr}; }; diff --git a/source/game/entities/entity_manager.cpp b/source/game/entities/entity_manager.cpp index 319b061..0a679c6 100644 --- a/source/game/entities/entity_manager.cpp +++ b/source/game/entities/entity_manager.cpp @@ -72,6 +72,7 @@ void EntityManager::forceOpenDoor() { auto EntityManager::add(std::unique_ptr entity) -> Entity* { if (entity == nullptr) { return nullptr; } entity->setEventBus(&bus_); + entity->setEntityManager(this); Entity* raw = entity.get(); entities_.push_back(std::move(entity)); // cppcheck-suppress returnDanglingLifetime @@ -80,12 +81,16 @@ auto EntityManager::add(std::unique_ptr entity) -> Entity* { void EntityManager::setDoor(Door* door) { door_ = door; - if (door_ != nullptr) { door_->setEventBus(&bus_); } + if (door_ != nullptr) { + door_->setEventBus(&bus_); + door_->setEntityManager(this); + } } auto EntityManager::addSarcophagus(std::unique_ptr sarc) -> Sarcophagus* { if (sarc == nullptr) { return nullptr; } sarc->setEventBus(&bus_); + sarc->setEntityManager(this); Sarcophagus* raw = sarc.get(); entities_.push_back(std::move(sarc)); ++total_sarcs_; @@ -110,6 +115,24 @@ void EntityManager::notifyPlayerTilePainted(int col, int row) { for (auto& e : entities_) { e->notifyTilePainted(col, row); } } +auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* { + // Cerca lineal sobre `entities_`: cost menyspreable amb poques + // desenes d'entitats per sala i una crida per frame des dels + // consumidors actius (Player a l'aterratge, Mòmia a la decisió de + // direcció). Si esdevé calent, es podria indexar per cel·la del + // tilemap. La doble condició filtra exclusivament decals de sòl que + // volen rebre triggers — avui Hole; en el futur pinchos plans, + // plaques de pressió, charcos. + for (const auto& entity : 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 { std::vector out; out.reserve(entities_.size()); diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index 9078964..7d25c40 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -58,23 +58,6 @@ void EntityManager::renderAll() { for (const auto& e : entities_) { renderEntity(*e); } } -auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* { - // Cerca lineal sobre `entities_`: cost menyspreable amb poques - // desenes d'entitats per sala i una sola crida per frame i per - // jugador (a l'aterratge). Si esdevé calent, es podria indexar - // per cel·la del tilemap. La doble condició filtra exclusivament - // decals de sòl que volen rebre triggers — avui Hole; en el - // futur pinchos plans, plaques de pressió, charcos. - for (const auto& entity : 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; -} - void EntityManager::checkPlayerCollisions(const std::vector& players) { if (players.empty()) { return; } for (const auto& entity : entities_) {