diff --git a/source/game/entities/concrete/mummy.cpp b/source/game/entities/concrete/mummy.cpp index 9047e33..08a056c 100644 --- a/source/game/entities/concrete/mummy.cpp +++ b/source/game/entities/concrete/mummy.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -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 { + const float NEXT = prev + delta; + if (delta > 0.0F) { + for (int k = 0; k <= max_idx; ++k) { + const float TARGET = base + (static_cast(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(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 { + const auto BOX = getCollisionRect(); + if (dx != 0.0F) { + return findLaneCross(BOX.x, dx, static_cast(Map::playfield_x), static_cast(Map::pitch_x), Map::grid_cols); + } + return findLaneCross(BOX.y, dy, static_cast(Map::playfield_y), static_cast(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 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); diff --git a/source/game/entities/concrete/mummy.hpp b/source/game/entities/concrete/mummy.hpp index e5c99d1..937f763 100644 --- a/source/game/entities/concrete/mummy.hpp +++ b/source/game/entities/concrete/mummy.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "external/fkyaml_node.hpp" @@ -12,11 +13,6 @@ // (`chooseDirection`) que la base implementa com a aleatori uniforme // sobre les direccions admissibles. Futurs derivats poden sesgar cap al // jugador sense tocar la lògica de moviment. -// -// Andamiatge: el concepte d'"intersecció" i la validació de direcció -// requereixen conèixer el mapa. De moment l'exposem com un conjunt de -// direccions admissibles que l'EntityManager injecta; la implementació -// real arribarà quan cablegem l'entitat al game loop. class Mummy : public DynamicHazard { public: enum class Direction : std::uint8_t { NONE, @@ -32,11 +28,15 @@ class Mummy : public DynamicHazard { // aleatoria; si `allowed` està buit, retorna `NONE`. virtual auto chooseDirection(const std::vector& allowed) -> Direction; - // Política de moviment "corridor_wanderer": camina recte fins que el - // pròxim tram la bloquejaria (mur del playfield o sarc). Quan troba un - // obstacle, recull les direccions perpendiculars vàlides (evitant - // U-turn llevat que no hi hagi alternativa) i crida `chooseDirection` - // per decidir-ne una. No consumeix moviment el frame de gir. + // Política "corridor_wanderer": + // (a) A cada creuament de corredor (edge-detect de travessar la línia + // central d'un corredor perpendicular) amarra la caixa al centre + // exacte del creuament i decideix: seguir recta o girar a una + // perpendicular lliure. Mai mitja volta. + // (b) Si el pròxim tram la bloqueja (mur del playfield o sarc), gira + // cap a una perpendicular vàlida i, només si no en queda cap, fa + // mitja volta. + // En ambdós casos el frame de gir no consumeix avenç extra. void step(float delta_time) override; // Contacte amb el jugador: publica `player_hit` amb el dany. @@ -72,6 +72,24 @@ class Mummy : public DynamicHazard { // Aplica la direcció triada (wrapper del switch en una sola crida). void applyChoice(Direction dir); + // Si aquest frame travessa la línia central d'un corredor perpendicular, + // retorna el delta (sobre l'eix actiu) per amarrar la caixa a aquesta + // línia. `nullopt` si no en creua cap. Els corredors perpendiculars + // estan a `base + k·pitch` (k ∈ [0..max_idx]); la comparació és + // estricta a un costat per no re-disparar quan la caixa ja hi està. + [[nodiscard]] auto detectLaneCrossing(float dx, float dy) const -> std::optional; + + // Decisió a un creuament: pool = perpendiculars lliures + direcció + // actual si continua essent lliure. Mai mitja volta. Si el pool sortís + // buit (cas degenerat, a la pràctica no s'hi arriba), manté la + // direcció actual; el frame següent, si està bloquejada, la branca de + // bloqueig ja la reorientarà. + void decideAtIntersection(float step_px); + + // Direcció actual com a enum, reconstruïda a partir de `dir_x_/dir_y_`. + // S'usa al pool de creuament per afegir la recta com a candidata. + [[nodiscard]] auto currentDirection() const -> Direction; + // Animacions carregades del YAML, una per cada facing. Per a horitzontal // només en cal una (`walk`) i el flip ja diferencia esquerra/dreta. AnimationComponent anim_walk_down_;