feat(mummy): evita tiles amb decal trigger (Hole) en decidir direcció

This commit is contained in:
JailDesigner committed 2026-04-30 16:50:26 +02:00
1 parent 3e5217066f
commit 3bfca35f3a
5 files changed
+111 -27

No files matched your search

+59 -7
View File
@@ -8,6 +8,7 @@
#include <utility>
#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<Direction> 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<float>(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:
+15 -2
View File
@@ -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<Direction> perp;
std::vector<Direction> 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
+13
View File
@@ -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};
};
+24 -1
View File
@@ -72,6 +72,7 @@ void EntityManager::forceOpenDoor() {
auto EntityManager::add(std::unique_ptr<Entity> 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) -> 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<Sarcophagus> 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<Entity*> {
std::vector<Entity*> out;
out.reserve(entities_.size());
@@ -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<Player*>& players) {
if (players.empty()) { return; }
for (const auto& entity : entities_) {