fix(mummy): evita soft-lock per drift sub-pixel a HURT vora del mur
This commit is contained in:
1 parent
0d58da536c
commit
e9f99a0542
2 files changed
+128
-5
No files matched your search
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user