303 lines
12 KiB
C++
303 lines
12 KiB
C++
#include "game/entities/concrete/mummy.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <optional>
|
||
#include <random>
|
||
#include <string>
|
||
#include <utility>
|
||
|
||
#include "game/entities/entity_factory.hpp"
|
||
#include "game/entities/event_bus.hpp"
|
||
#include "game/map/map.hpp"
|
||
|
||
namespace {
|
||
|
||
// RNG local al mòdul: compartit per totes les mòmies (no cal reproducció
|
||
// determinista a l'andamiatge). Es reinicialitza amb una llavor fixa
|
||
// per facilitar la depuració — si algun dia volem aleatoritat real,
|
||
// llegir-la de std::random_device.
|
||
auto rng() -> std::mt19937& {
|
||
static std::mt19937 k_rng_{1337};
|
||
return k_rng_;
|
||
}
|
||
|
||
// Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix
|
||
// patró que Door/Sarcophagus). Si la clau no apareix, retorna una
|
||
// animació d'1 frame loopant, neutra — la momia es podrà moure però
|
||
// visualment es quedarà estàtica.
|
||
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||
std::vector<int> frames = {0};
|
||
float fps = 8.0F;
|
||
bool loop = true;
|
||
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
|
||
const auto& anim = yaml["animations"][key];
|
||
if (anim.contains("frames")) {
|
||
frames.clear();
|
||
for (const auto& f : anim["frames"]) {
|
||
// cppcheck-suppress useStlAlgorithm
|
||
frames.push_back(f.get_value<int>());
|
||
}
|
||
if (frames.empty()) { frames.push_back(0); }
|
||
}
|
||
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
|
||
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
||
}
|
||
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)
|
||
: anim_walk_down_(buildAnimation(yaml, "walk_down")),
|
||
anim_walk_up_(buildAnimation(yaml, "walk_up")),
|
||
anim_walk_lateral_(buildAnimation(yaml, "walk")) {
|
||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
||
collides_ = true;
|
||
interactable_ = false;
|
||
|
||
// Velocitat i dany des del bloc `stats` (si existeix).
|
||
if (yaml.contains("stats")) {
|
||
const auto& s = yaml["stats"];
|
||
if (s.contains("speed")) { setSpeed(s["speed"].get_value<float>()); }
|
||
if (s.contains("damage")) { setDamage(s["damage"].get_value<int>()); }
|
||
}
|
||
|
||
// Direcció inicial llegida del bloc `ai:`. Per defecte dreta, per
|
||
// coherència amb el que fa RollingRock. Valors acceptats: right, left,
|
||
// up, down. Qualsevol altre s'interpreta com a "sense direcció" (la
|
||
// momia no es mou fins que step() no triï una direcció).
|
||
std::string initial = "right";
|
||
if (yaml.contains("ai") && yaml["ai"].contains("initial_direction")) {
|
||
initial = yaml["ai"]["initial_direction"].get_value<std::string>();
|
||
}
|
||
if (initial == "right") {
|
||
changeDirection(1.0F, 0.0F);
|
||
} else if (initial == "left") {
|
||
changeDirection(-1.0F, 0.0F);
|
||
} else if (initial == "up") {
|
||
changeDirection(0.0F, -1.0F);
|
||
} else if (initial == "down") {
|
||
changeDirection(0.0F, 1.0F);
|
||
}
|
||
}
|
||
|
||
void Mummy::changeDirection(float dx, float dy) {
|
||
setDirection(dx, dy);
|
||
// Prevalença lateral: si hi ha component horitzontal s'usa l'anim
|
||
// lateral + flip segons el signe. Només direccions purament verticals
|
||
// disparen walk_up/walk_down. Quan la direcció queda (0,0) (cul-de-sac
|
||
// amb NONE) no toquem l'animació: la momia es quedarà al frame actual
|
||
// fins que step() li torni a assignar direcció.
|
||
if (dx != 0.0F) {
|
||
anim_ = anim_walk_lateral_;
|
||
flip_ = (dx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
||
} else if (dy < 0.0F) {
|
||
anim_ = anim_walk_up_;
|
||
flip_ = SDL_FLIP_NONE;
|
||
} else if (dy > 0.0F) {
|
||
anim_ = anim_walk_down_;
|
||
flip_ = SDL_FLIP_NONE;
|
||
}
|
||
if (anim_.has_value()) { anim_->reset(); }
|
||
}
|
||
|
||
auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction {
|
||
if (allowed.empty()) { return Direction::NONE; }
|
||
std::uniform_int_distribution<std::size_t> dist(0, allowed.size() - 1);
|
||
return allowed[dist(rng())];
|
||
}
|
||
|
||
auto Mummy::candidateBox(float dx, float dy) const -> SDL_FRect {
|
||
auto box = getCollisionRect();
|
||
box.x += dx;
|
||
box.y += dy;
|
||
return box;
|
||
}
|
||
|
||
void Mummy::snapToPlayfieldEdge(float dx, float dy) {
|
||
// La caja 16×16 té exactament l'amplada del corredor; si ens aturàvem
|
||
// a una posició subpíxel dins de l'última cel·la, qualsevol gir
|
||
// perpendicular col·lidiria amb el sarc adjacent al limit. Per això
|
||
// reduïm el dx/dy fins a la vora exacta del playfield en l'eix actiu
|
||
// (l'altre sempre és 0 perquè la momia només es mou en 1D per frame).
|
||
const auto BOX = getCollisionRect();
|
||
float snap_dx = 0.0F;
|
||
float snap_dy = 0.0F;
|
||
if (dx > 0.0F) {
|
||
snap_dx = std::max(0.0F, static_cast<float>(Map::playfield_x + Map::playfield_w) - BOX.x - BOX.w);
|
||
} else if (dx < 0.0F) {
|
||
snap_dx = std::min(0.0F, static_cast<float>(Map::playfield_x) - BOX.x);
|
||
}
|
||
if (dy > 0.0F) {
|
||
snap_dy = std::max(0.0F, static_cast<float>(Map::playfield_y + Map::playfield_h) - BOX.y - BOX.h);
|
||
} else if (dy < 0.0F) {
|
||
snap_dy = std::min(0.0F, static_cast<float>(Map::playfield_y) - BOX.y);
|
||
}
|
||
if (!Map::collides(candidateBox(snap_dx, snap_dy))) {
|
||
pixel_rect_.x += snap_dx;
|
||
pixel_rect_.y += snap_dy;
|
||
}
|
||
}
|
||
|
||
void Mummy::step(float delta_time) {
|
||
// Desplaçament nominal en aquest frame segons la direcció actual.
|
||
const float DX = dir_x_ * speed_ * delta_time;
|
||
const float DY = dir_y_ * speed_ * delta_time;
|
||
|
||
// Si no té direcció (p.ex. YAML sense `initial_direction`), el frame és
|
||
// no-op fins que algú la injecte amb `setDirection`.
|
||
if (DX == 0.0F && DY == 0.0F) { return; }
|
||
|
||
// 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)) {
|
||
pixel_rect_.x += DX;
|
||
pixel_rect_.y += DY;
|
||
return;
|
||
}
|
||
|
||
// Bloqueig per la vora del playfield: amarrem la caja a la vora exacta.
|
||
if (!Map::insidePlayfield(CANDIDATE)) { snapToPlayfieldEdge(DX, DY); }
|
||
|
||
// Cal girar. Preferim perpendiculars; només fem U-turn si no hi ha cap
|
||
// altra opció (comportament típic d'Oh Mummy!: no es recula sense motiu).
|
||
const auto POOLS = collectDirectionPools(speed_ * delta_time);
|
||
const auto& pool = !POOLS.perp.empty() ? POOLS.perp : POOLS.uturn;
|
||
applyChoice(chooseDirection(pool));
|
||
// 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;
|
||
float dy;
|
||
Direction dir;
|
||
};
|
||
const std::array<Candidate, 4> ALL{{
|
||
{.dx = step, .dy = 0.0F, .dir = Direction::RIGHT},
|
||
{.dx = -step, .dy = 0.0F, .dir = Direction::LEFT},
|
||
{.dx = 0.0F, .dy = -step, .dir = Direction::UP},
|
||
{.dx = 0.0F, .dy = step, .dir = Direction::DOWN},
|
||
}};
|
||
DirectionPools pools;
|
||
for (const auto& c : ALL) {
|
||
const bool IS_CURRENT = (c.dx * dir_x_ > 0.0F) || (c.dy * dir_y_ > 0.0F);
|
||
// 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);
|
||
if (IS_UTURN) {
|
||
pools.uturn.push_back(c.dir);
|
||
} else {
|
||
pools.perp.push_back(c.dir);
|
||
}
|
||
}
|
||
return pools;
|
||
}
|
||
|
||
void Mummy::applyChoice(Direction dir) {
|
||
switch (dir) {
|
||
case Direction::UP:
|
||
changeDirection(0.0F, -1.0F);
|
||
break;
|
||
case Direction::DOWN:
|
||
changeDirection(0.0F, 1.0F);
|
||
break;
|
||
case Direction::LEFT:
|
||
changeDirection(-1.0F, 0.0F);
|
||
break;
|
||
case Direction::RIGHT:
|
||
changeDirection(1.0F, 0.0F);
|
||
break;
|
||
case Direction::NONE:
|
||
changeDirection(0.0F, 0.0F);
|
||
break;
|
||
}
|
||
}
|
||
|
||
void Mummy::onPlayerContact(Player* /*player*/) {
|
||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
||
}
|
||
|
||
REGISTER_ENTITY("mummy", Mummy)
|
||
REGISTER_ENTITY_AS("momia", Mummy, Momia)
|