feat(mummy): decisió a creuaments de corredor

La mòmia ara decideix direcció a cada creuament de corredor (edge-detect
de travessar la línia central d'un corredor perpendicular) i no només
quan topa amb un mur o sarc. A la detecció, amarra la caixa al centre
exacte del creuament per evitar drift sub-píxel i tria entre continuar
recta o girar a una perpendicular lliure. Mai mitja volta a un creuament
— la mitja volta queda reservada al cas de bloqueig sense alternatives.
This commit is contained in:
JailDesigner committed 2026-04-22 22:52:51 +02:00
1 parent 5312abc761
commit 9d256b7f2d
2 files changed
+108 -12

No files matched your search

+80 -2
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <array>
#include <optional>
#include <random>
#include <string>
#include <utility>
@@ -45,6 +46,29 @@ namespace {
return {frames, fps, loop};
}
// Busca si el segment [prev, prev+delta] travessa una lane del patró
// `base + k·pitch` (k ∈ [0..max_idx]). Retorna el delta que cal aplicar
// per arribar exactament a la primera línia travessada. Iterem en el
// sentit de la marxa per encertar la MÉS propera quan el frame creua
// més d'una lane (cas marginal a velocitats típiques, però pot passar
// amb dt grans). Comparació estricta a un costat: no dispara quan la
// caixa ja es troba alineada a la lane (hi està després del snap).
auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional<float> {
const float NEXT = prev + delta;
if (delta > 0.0F) {
for (int k = 0; k <= max_idx; ++k) {
const float TARGET = base + (static_cast<float>(k) * pitch);
if (prev < TARGET && NEXT >= TARGET) { return TARGET - prev; }
}
} else if (delta < 0.0F) {
for (int k = max_idx; k >= 0; --k) {
const float TARGET = base + (static_cast<float>(k) * pitch);
if (prev > TARGET && NEXT <= TARGET) { return TARGET - prev; }
}
}
return std::nullopt;
}
} // namespace
Mummy::Mummy(const fkyaml::node& yaml)
@@ -148,7 +172,23 @@ void Mummy::step(float delta_time) {
// no-op fins que algú la injecte amb `setDirection`.
if (DX == 0.0F && DY == 0.0F) { return; }
// Prova d'avançar. Si la caixa candidata és vàlida (dins del playfield
// Branca creuament: si aquest frame travessa la línia central d'un
// corredor perpendicular, amarra la caixa al centre del creuament i
// hi decideix. La mòmia consumeix només la porció de moviment fins al
// centre; el remanent queda per al pròxim frame, ja amb la (potser
// nova) direcció aplicada. Així la caixa 16×16 queda sempre alineada
// a la graella de corredors i els girs no xoquen per drift sub-píxel.
if (const auto SNAP = detectLaneCrossing(DX, DY); SNAP.has_value()) {
if (DX != 0.0F) {
pixel_rect_.x += *SNAP;
} else {
pixel_rect_.y += *SNAP;
}
decideAtIntersection(speed_ * delta_time);
return;
}
// Avenç normal: si la caixa candidata és vàlida (dins del playfield
// i sense trepitjar cap sarc), s'aplica el moviment i s'acaba el frame.
const auto CANDIDATE = candidateBox(DX, DY);
if (Map::insidePlayfield(CANDIDATE) && !Map::collides(CANDIDATE)) {
@@ -168,6 +208,42 @@ void Mummy::step(float delta_time) {
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
}
auto Mummy::detectLaneCrossing(float dx, float dy) const -> std::optional<float> {
const auto BOX = getCollisionRect();
if (dx != 0.0F) {
return findLaneCross(BOX.x, dx, static_cast<float>(Map::playfield_x), static_cast<float>(Map::pitch_x), Map::grid_cols);
}
return findLaneCross(BOX.y, dy, static_cast<float>(Map::playfield_y), static_cast<float>(Map::pitch_y), Map::grid_rows);
}
auto Mummy::currentDirection() const -> Direction {
if (dir_x_ > 0.0F) { return Direction::RIGHT; }
if (dir_x_ < 0.0F) { return Direction::LEFT; }
if (dir_y_ > 0.0F) { return Direction::DOWN; }
if (dir_y_ < 0.0F) { return Direction::UP; }
return Direction::NONE;
}
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);
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.
const auto STRAIGHT_BOX = candidateBox(dir_x_ * step_px, dir_y_ * step_px);
if (Map::insidePlayfield(STRAIGHT_BOX) && !Map::collides(STRAIGHT_BOX)) {
pool.push_back(currentDirection());
}
if (pool.empty()) { return; }
applyChoice(chooseDirection(pool));
}
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
struct Candidate {
float dx;
@@ -183,7 +259,9 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
DirectionPools pools;
for (const auto& c : ALL) {
const bool IS_CURRENT = (c.dx * dir_x_ > 0.0F) || (c.dy * dir_y_ > 0.0F);
if (IS_CURRENT) { continue; } // ja sabem que està bloquejada
// La direcció actual no forma part del pool: al bloqueig sabem que
// està bloquejada; al creuament, qui decideix l'afegeix a banda.
if (IS_CURRENT) { continue; }
const auto TEST = candidateBox(c.dx, c.dy);
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);