Merge feat/mummy-ai — generalització del filtre de cruces de la mòmia

This commit is contained in:
JailDesigner committed 2026-04-30 20:06:54 +02:00
commit 4e2ba827e3
2 files changed
+30 -21

No files matched your search

+16 -13
View File
@@ -261,7 +261,7 @@ void Mummy::step(float delta_time) {
// TODO(refactor): mateixa cascada a `decideAtIntersection` amb la // TODO(refactor): mateixa cascada a `decideAtIntersection` amb la
// variant "afegir recta a Nivell 1". Factoritzable // variant "afegir recta a Nivell 1". Factoritzable
// quan tinga sentit (3+ callsites). // quan tinga sentit (3+ callsites).
auto pools = collectDirectionPools(speed_ * delta_time, /*avoid_decals=*/true); auto pools = collectDirectionPools(speed_ * delta_time, /*avoid_blocked=*/true);
const std::vector<Direction>* chosen = nullptr; const std::vector<Direction>* chosen = nullptr;
if (!pools.perp.empty()) { if (!pools.perp.empty()) {
chosen = &pools.perp; chosen = &pools.perp;
@@ -270,7 +270,7 @@ void Mummy::step(float delta_time) {
} }
DirectionPools pools_unfiltered; DirectionPools pools_unfiltered;
if (chosen == nullptr) { if (chosen == nullptr) {
pools_unfiltered = collectDirectionPools(speed_ * delta_time, /*avoid_decals=*/false); pools_unfiltered = collectDirectionPools(speed_ * delta_time, /*avoid_blocked=*/false);
if (!pools_unfiltered.perp.empty()) { if (!pools_unfiltered.perp.empty()) {
chosen = &pools_unfiltered.perp; chosen = &pools_unfiltered.perp;
} else if (!pools_unfiltered.uturn.empty()) { } else if (!pools_unfiltered.uturn.empty()) {
@@ -350,13 +350,13 @@ void Mummy::decideAtIntersection(float step_px) {
// permetia mai en creuaments, però és preferible recular que caure al // permetia mai en creuaments, però és preferible recular que caure al
// forat quan totes les altres surten a decal. // forat quan totes les altres surten a decal.
// TODO(refactor): cascada paral·lela a la branca de bloqueig de `step`. // TODO(refactor): cascada paral·lela a la branca de bloqueig de `step`.
auto pools = collectDirectionPools(step_px, /*avoid_decals=*/true); auto pools = collectDirectionPools(step_px, /*avoid_blocked=*/true);
// Nivell 1: perpendiculars (filtrades) + recta si lliure i sense decal. // Nivell 1: perpendiculars (filtrades) + recta si lliure i sense decal.
std::vector<Direction> level1 = std::move(pools.perp); std::vector<Direction> level1 = std::move(pools.perp);
const auto STRAIGHT_BOX = candidateBox(dir_x_ * step_px, dir_y_ * step_px); const auto STRAIGHT_BOX = candidateBox(dir_x_ * step_px, dir_y_ * step_px);
const bool STRAIGHT_OPEN = 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_)) { if (STRAIGHT_OPEN && !isBlockedAheadInDir(dir_x_, dir_y_)) {
level1.push_back(currentDirection()); level1.push_back(currentDirection());
} }
@@ -376,7 +376,7 @@ void Mummy::decideAtIntersection(float step_px) {
DirectionPools pools_unfiltered; DirectionPools pools_unfiltered;
std::vector<Direction> level3; std::vector<Direction> level3;
if (chosen == nullptr) { if (chosen == nullptr) {
pools_unfiltered = collectDirectionPools(step_px, /*avoid_decals=*/false); pools_unfiltered = collectDirectionPools(step_px, /*avoid_blocked=*/false);
level3 = std::move(pools_unfiltered.perp); level3 = std::move(pools_unfiltered.perp);
if (STRAIGHT_OPEN) { level3.push_back(currentDirection()); } if (STRAIGHT_OPEN) { level3.push_back(currentDirection()); }
if (!level3.empty()) { if (!level3.empty()) {
@@ -390,7 +390,7 @@ void Mummy::decideAtIntersection(float step_px) {
applyChoice(chooseDirection(*chosen)); applyChoice(chooseDirection(*chosen));
} }
auto Mummy::collectDirectionPools(float step, bool avoid_decals) const -> DirectionPools { auto Mummy::collectDirectionPools(float step, bool avoid_blocked) const -> DirectionPools {
struct Candidate { struct Candidate {
float dx; float dx;
float dy; float dy;
@@ -412,9 +412,9 @@ auto Mummy::collectDirectionPools(float step, bool avoid_decals) const -> Direct
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; } if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
// Filtre de decals (Hole i futurs decals trigger): comprova el // Filtre de decals (Hole i futurs decals trigger): comprova el
// tile cardinal contigu; si conté un trigger, descartem la // tile cardinal contigu; si conté un trigger, descartem la
// direcció. El caller pot demanar `avoid_decals=false` com a // direcció. El caller pot demanar `avoid_blocked=false` com a
// fallback quan totes les filtrades estan bloquejades. // fallback quan totes les filtrades estan bloquejades.
if (avoid_decals && isDecalAheadInDir(c.dx, c.dy)) { continue; } if (avoid_blocked && isBlockedAheadInDir(c.dx, c.dy)) { continue; }
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F); const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
if (IS_UTURN) { if (IS_UTURN) {
pools.uturn.push_back(c.dir); pools.uturn.push_back(c.dir);
@@ -425,10 +425,7 @@ auto Mummy::collectDirectionPools(float step, bool avoid_decals) const -> Direct
return pools; return pools;
} }
auto Mummy::isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool { auto Mummy::isBlockedAheadInDir(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; } if (dx_signed == 0.0F && dy_signed == 0.0F) { return false; }
// El rect a consultar és el AABB del tile cardinal contigu (mida // El rect a consultar és el AABB del tile cardinal contigu (mida
// `Map::tile_size`), ancorat al tile actual de la mòmia. Calculem el // `Map::tile_size`), ancorat al tile actual de la mòmia. Calculem el
@@ -457,7 +454,13 @@ auto Mummy::isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool {
.w = TS, .w = TS,
.h = TS, .h = TS,
}; };
return entity_manager_->findFloorTriggerAt(NEXT) != nullptr; // Ordre per cost creixent: comparació de coordenades, després iteració
// sobre sarcs i porta, i finalment cerca a l'index d'entitats. Sense
// manager (smoke tests) saltem només l'últim filtre.
if (!Map::insidePlayfield(NEXT)) { return true; }
if (Map::collides(NEXT)) { return true; }
if (entity_manager_ != nullptr && entity_manager_->findFloorTriggerAt(NEXT) != nullptr) { return true; }
return false;
} }
void Mummy::applyChoice(Direction dir) { void Mummy::applyChoice(Direction dir) {
+14 -8
View File
@@ -87,24 +87,30 @@ class Mummy : public DynamicHazard {
// `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou // `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou
// perquè ja sabem que està bloquejada. Cada candidata es valida amb // perquè ja sabem que està bloquejada. Cada candidata es valida amb
// `Map::insidePlayfield` + `Map::collides` sobre la caja resultant. Si // `Map::insidePlayfield` + `Map::collides` sobre la caja resultant. Si
// `avoid_decals` és true i hi ha `EntityManager` injectat, també es // `avoid_blocked` és true i hi ha `EntityManager` injectat, també es
// descarten les direccions que apunten a un tile amb un decal trigger // descarten les direccions que apunten a un tile amb un decal trigger
// (Hole o futurs pinchos plans/charcos). El caller és responsable de // (Hole o futurs pinchos plans/charcos). El caller és responsable de
// re-cridar amb `avoid_decals=false` com a fallback si totes les // re-cridar amb `avoid_blocked=false` com a fallback si totes les
// direccions filtrades estan bloquejades. // direccions filtrades estan bloquejades.
struct DirectionPools { struct DirectionPools {
std::vector<Direction> perp; std::vector<Direction> perp;
std::vector<Direction> uturn; std::vector<Direction> uturn;
}; };
[[nodiscard]] auto collectDirectionPools(float step, bool avoid_decals = true) const -> DirectionPools; [[nodiscard]] auto collectDirectionPools(float step, bool avoid_blocked = true) const -> DirectionPools;
// True si el tile cardinal contigu en la direcció (signe de dx, signe // 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_`) // de dy) està bloquejat per qualsevol motiu: fora del playfield, ocupat
// per geometria estàtica (sarcs, door_corridor tancat) via `Map::collides`,
// o conté un decal trigger (`is_floor_decal_ && triggers_on_overlap_`)
// segons `EntityManager::findFloorTriggerAt`. Captura `Hole` i qualsevol // segons `EntityManager::findFloorTriggerAt`. Captura `Hole` i qualsevol
// futur decal trigger sense hardcodejar tipus. Si `entity_manager_` no // futur decal trigger sense hardcodejar tipus, i evita que la mòmia
// està injectat (smoke tests sense manager), retorna false silenciosament // triï a un creuament una direcció que el frame següent toparia (cas
// — la mòmia es comporta com abans del feature. // típic: a la intersecció anterior al límit nord del playfield, on el
[[nodiscard]] auto isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool; // pas immediat encara hi cap però el tile sencer següent ja està fora).
// Si `entity_manager_` no està injectat (smoke tests sense manager),
// el filtre de decals és silenciós però els altres dos (playfield i
// collides) segueixen actius.
[[nodiscard]] auto isBlockedAheadInDir(float dx_signed, float dy_signed) const -> bool;
// Canvi de direcció + selecció de l'animació corresponent (un cardinal // Canvi de direcció + selecció de l'animació corresponent (un cardinal
// per branch). Tots els camins que trien nova direcció (ctor amb // per branch). Tots els camins que trien nova direcció (ctor amb