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:
1 parent
5312abc761
commit
9d256b7f2d
2 files changed
+108
-12
No files matched your search
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -45,6 +46,29 @@ namespace {
|
|||||||
return {frames, fps, loop};
|
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
|
} // namespace
|
||||||
|
|
||||||
Mummy::Mummy(const fkyaml::node& yaml)
|
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`.
|
// no-op fins que algú la injecte amb `setDirection`.
|
||||||
if (DX == 0.0F && DY == 0.0F) { return; }
|
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.
|
// i sense trepitjar cap sarc), s'aplica el moviment i s'acaba el frame.
|
||||||
const auto CANDIDATE = candidateBox(DX, DY);
|
const auto CANDIDATE = candidateBox(DX, DY);
|
||||||
if (Map::insidePlayfield(CANDIDATE) && !Map::collides(CANDIDATE)) {
|
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.
|
// 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 {
|
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||||
struct Candidate {
|
struct Candidate {
|
||||||
float dx;
|
float dx;
|
||||||
@@ -183,7 +259,9 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
|||||||
DirectionPools pools;
|
DirectionPools pools;
|
||||||
for (const auto& c : ALL) {
|
for (const auto& c : ALL) {
|
||||||
const bool IS_CURRENT = (c.dx * dir_x_ > 0.0F) || (c.dy * dir_y_ > 0.0F);
|
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);
|
const auto TEST = candidateBox(c.dx, c.dy);
|
||||||
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
|
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { 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);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
@@ -12,11 +13,6 @@
|
|||||||
// (`chooseDirection`) que la base implementa com a aleatori uniforme
|
// (`chooseDirection`) que la base implementa com a aleatori uniforme
|
||||||
// sobre les direccions admissibles. Futurs derivats poden sesgar cap al
|
// sobre les direccions admissibles. Futurs derivats poden sesgar cap al
|
||||||
// jugador sense tocar la lògica de moviment.
|
// 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 {
|
class Mummy : public DynamicHazard {
|
||||||
public:
|
public:
|
||||||
enum class Direction : std::uint8_t { NONE,
|
enum class Direction : std::uint8_t { NONE,
|
||||||
@@ -32,11 +28,15 @@ class Mummy : public DynamicHazard {
|
|||||||
// aleatoria; si `allowed` està buit, retorna `NONE`.
|
// aleatoria; si `allowed` està buit, retorna `NONE`.
|
||||||
virtual auto chooseDirection(const std::vector<Direction>& allowed) -> Direction;
|
virtual auto chooseDirection(const std::vector<Direction>& allowed) -> Direction;
|
||||||
|
|
||||||
// Política de moviment "corridor_wanderer": camina recte fins que el
|
// Política "corridor_wanderer":
|
||||||
// pròxim tram la bloquejaria (mur del playfield o sarc). Quan troba un
|
// (a) A cada creuament de corredor (edge-detect de travessar la línia
|
||||||
// obstacle, recull les direccions perpendiculars vàlides (evitant
|
// central d'un corredor perpendicular) amarra la caixa al centre
|
||||||
// U-turn llevat que no hi hagi alternativa) i crida `chooseDirection`
|
// exacte del creuament i decideix: seguir recta o girar a una
|
||||||
// per decidir-ne una. No consumeix moviment el frame de gir.
|
// 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;
|
void step(float delta_time) override;
|
||||||
|
|
||||||
// Contacte amb el jugador: publica `player_hit` amb el dany.
|
// 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).
|
// Aplica la direcció triada (wrapper del switch en una sola crida).
|
||||||
void applyChoice(Direction dir);
|
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<float>;
|
||||||
|
|
||||||
|
// 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
|
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
|
||||||
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
||||||
AnimationComponent anim_walk_down_;
|
AnimationComponent anim_walk_down_;
|
||||||
|
|||||||
Reference in new issue
Block a user