fix(mummy): evita soft-lock per drift sub-pixel a HURT vora del mur

This commit is contained in:
JailDesigner committed 2026-05-08 12:07:18 +02:00
1 parent 0d58da536c
commit e9f99a0542
2 files changed
+128 -5

No files matched your search

+32 -5
View File
@@ -222,9 +222,24 @@ void Mummy::step(float delta_time) {
const float DX = dir_x_ * speed_ * delta_time; const float DX = dir_x_ * speed_ * delta_time;
const float DY = dir_y_ * speed_ * delta_time; const float DY = dir_y_ * speed_ * delta_time;
// Si no té direcció (p.ex. YAML sense `initial_direction`), el frame és // Sense direcció: pot ser un setup degenerat (YAML sense
// no-op fins que algú la injecte amb `setDirection`. // `initial_direction`) o el residu d'un HURT que va sortir amb
if (DX == 0.0F && DY == 0.0F) { return; } // `chooseHurtDirection() == NONE` (totes les direccions provades
// bloquejades). En aquest segon cas la mòmia quedaria eternament quieta
// — `setDirection(0,0)` a `enterHurt` no toca `anim_`, així que es
// veuria animant-se sense moure's. Re-decidim com a una intersecció:
// si trobem alguna perpendicular vàlida, la prenem; si no, mitja volta.
// Si tampoc, mantenim (0,0) i tornem a provar el proper frame —
// pràcticament impossible amb la geometria actual, però no enganxa.
if (DX == 0.0F && DY == 0.0F) {
auto pools = collectDirectionPools(speed_ * delta_time, /*avoid_blocked=*/true);
if (!pools.perp.empty()) {
applyChoice(chooseDirection(pools.perp));
} else if (!pools.uturn.empty()) {
applyChoice(chooseDirection(pools.uturn));
}
return;
}
// Branca creuament: si aquest frame travessa la línia central d'un // Branca creuament: si aquest frame travessa la línia central d'un
// corredor perpendicular, amarra la caixa al centre del creuament i // corredor perpendicular, amarra la caixa al centre del creuament i
@@ -612,8 +627,20 @@ void Mummy::hurtStep(float delta_time) {
return; return;
} }
// Topa amb mur o sarc: surt del HURT immediatament. La IA normal // Topa amb mur del playfield: amarra la BOX a la vora exacta abans de
// re-decideix al pròxim step (no fem U-turn ni re-orientació aquí). // transitar a ROAMING. Sense aquest snap, el drift sub-pixel acumulat a
// la cursa (DX = -hurt_speed·dt rarament és enter al joc real, on dt ve
// de SDL_GetTicks) deixaria la BOX a un offset de 0.x px dins de
// l'última cel·la. Com que la BOX és 16×16 i els passadissos també ho
// són, qualsevol drift fa que la cantonada sobresurti cap als sarcs
// adjacents — i el `chooseHurtDirection` del proper fueteig classifica
// les perpendiculars com a bloquejades per micro-overlap, retornant
// NONE i deixant la mòmia eternament quieta. Sarc-collision durant HURT
// és un cas molt menys habitual (la cursa va sempre per corredor recte);
// si arriba a passar, l'snap al playfield no l'arregla però la branca
// de recovery a `step()` el cobreix.
if (!Map::insidePlayfield(CANDIDATE)) { snapToPlayfieldEdge(DX, DY); }
state_ = State::ROAMING; state_ = State::ROAMING;
hurt_remaining_s_ = 0.0F; hurt_remaining_s_ = 0.0F;
} }
+96
View File
@@ -496,6 +496,99 @@ namespace {
REQUIRE(mummy.getState() == Mummy::State::ROAMING); REQUIRE(mummy.getState() == Mummy::State::ROAMING);
} }
void testMummyHurtSnapsAtPlayfieldWall() {
std::cout << "[T18] mummy HURT amarra a la vora del playfield en topar\n";
// Regressió: amb dt no-exacte (joc real → SDL_GetTicks ms) el drift
// sub-pixel de la cursa HURT deixava la BOX a 0.x px dins de l'última
// cel·la quan topava amb el mur. Com que la BOX 16×16 cabe exacta al
// corredor 16×16, qualsevol drift fa que la cantonada perpendicular
// sobresurti cap als sarcs adjacents — i el `chooseHurtDirection` del
// proper fueteig classifica les perpendiculars com a bloquejades per
// micro-overlap, retornant NONE i deixant la mòmia eternament quieta
// amb l'animació de córrer activa. El fix és snap a la vora exacta
// dins de `hurtStep` abans de transitar a ROAMING.
Map::loadFrom("data/maps/pyramid.yaml");
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
Mummy mummy(tmpl);
// Caixa real del joc (YAML): collision 16×16 als peus del sprite 16×32.
mummy.setCollisionBox({.offset_x = 0, .offset_y = 16, .width = 16, .height = 16});
// Comencem prou a prop del mur perquè la cursa hi arribi abans que
// expiri el timer (60 px/s · 0.6 s = 36 px de runway).
mummy.setPixelRect({.x = 40.0F, .y = 160.0F, .w = 16.0F, .h = 32.0F});
REQUIRE(mummy.getCollisionRect().x == 40.0F);
mummy.enterHurt(Mummy::Direction::WEST);
REQUIRE(mummy.getState() == Mummy::State::HURT);
REQUIRE(mummy.getDirX() == -1.0F);
// Avancem amb dt no-divisor de 1 (joc real) fins que topa el mur.
for (int i = 0; i < 200 && mummy.getState() == Mummy::State::HURT; ++i) {
mummy.update(0.0173F);
}
REQUIRE(mummy.getState() == Mummy::State::ROAMING);
// El fix garanteix snap exacte a playfield_x = 24.
REQUIRE(mummy.getCollisionRect().x == static_cast<float>(Map::playfield_x));
}
void testMummyRoamingRecoversFromZeroDir() {
std::cout << "[T19] mummy ROAMING amb dir=(0,0) re-decideix la propera step()\n";
// Xarxa de seguretat: si per qualsevol via la mòmia entra a ROAMING
// amb dir_x_/dir_y_ a zero, el `step()` anterior feia early return i
// es quedava quieta per sempre. Ara re-decideix com a una intersecció
// si troba alguna direcció vàlida.
Map::loadFrom("data/maps/pyramid.yaml");
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
Mummy mummy(tmpl);
mummy.setCollisionBox({.offset_x = 0, .offset_y = 16, .width = 16, .height = 16});
// Posicionem-la a un creuament aliniat (interior, lluny de murs i
// sarcs problemàtics): vertical x=88 ∩ horitzontal y=80.
mummy.setPixelRect({.x = 88.0F, .y = 64.0F, .w = 16.0F, .h = 32.0F});
// Forcem dir=(0,0) sense passar per HURT — simula l'estat patològic.
mummy.setDirection(0.0F, 0.0F);
REQUIRE(mummy.getDirX() == 0.0F);
REQUIRE(mummy.getDirY() == 0.0F);
// Un sol step amb dt típic ha d'assignar alguna direcció no-zero.
mummy.step(0.0167F);
const bool RECOVERED = mummy.getDirX() != 0.0F || mummy.getDirY() != 0.0F;
REQUIRE(RECOVERED);
}
void testMummyHurtCornerNoSoftLock() {
std::cout << "[T20] mummy HURT contra cantonada + re-whip no fa soft-lock\n";
// Integració: cursa HURT cap a una cantonada del playfield, després
// fueteig immediat al mateix lloc. Amb el bug, la mòmia quedava amb
// dir=(0,0) i no es movia per molts fueteigs que rebés. Amb el fix
// (snap a hurtStep + recovery a step), o bé `chooseHurtDirection`
// troba una direcció lliure des de la posició aliniada, o bé el
// proper step la rescata. En cap cas pot quedar permanentment quieta.
Map::loadFrom("data/maps/pyramid.yaml");
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
Mummy mummy(tmpl);
mummy.setCollisionBox({.offset_x = 0, .offset_y = 16, .width = 16, .height = 16});
// Mateix runway que T18.
mummy.setPixelRect({.x = 40.0F, .y = 160.0F, .w = 16.0F, .h = 32.0F});
mummy.enterHurt(Mummy::Direction::WEST);
for (int i = 0; i < 200 && mummy.getState() == Mummy::State::HURT; ++i) {
mummy.update(0.0173F);
}
// Fueteig immediat: enterHurt al moment exacte de quedar al mur.
mummy.enterHurt(Mummy::Direction::WEST);
// Després d'una desena de frames, la mòmia s'ha de moure ja sigui via
// la fugida HURT (perpendicular trobada) o via recovery a ROAMING.
const auto BEFORE = mummy.getPixelRect();
for (int i = 0; i < 60; ++i) { mummy.update(0.0173F); }
const auto AFTER = mummy.getPixelRect();
const float DX_TOTAL = AFTER.x - BEFORE.x;
const float DY_TOTAL = AFTER.y - BEFORE.y;
REQUIRE((DX_TOTAL != 0.0F) || (DY_TOTAL != 0.0F));
}
void testMummyWhippableFlag() { void testMummyWhippableFlag() {
std::cout << "[T17] mummy YAML llig `whippable` correctament\n"; std::cout << "[T17] mummy YAML llig `whippable` correctament\n";
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy"); fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
@@ -584,6 +677,9 @@ auto main() -> int {
testMummyStep(); testMummyStep();
testMummyHurtState(); testMummyHurtState();
testMummyHurtAgainstWall(); testMummyHurtAgainstWall();
testMummyHurtSnapsAtPlayfieldWall();
testMummyRoamingRecoversFromZeroDir();
testMummyHurtCornerNoSoftLock();
testMummyWhippableFlag(); testMummyWhippableFlag();
testTorchAnimation(); testTorchAnimation();
testExitedAggregator(); testExitedAggregator();