#include "game/entities/concrete/mummy.hpp" #include #include #include #include #include #include #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 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()); } if (frames.empty()) { frames.push_back(0); } } if (anim.contains("fps")) { fps = anim["fps"].get_value(); } if (anim.contains("loop")) { loop = anim["loop"].get_value(); } } 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) : 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("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()); } if (s.contains("damage")) { setDamage(s["damage"].get_value()); } } // 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(); } 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& allowed) -> Direction { if (allowed.empty()) { return Direction::NONE; } std::uniform_int_distribution 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(Map::playfield_x + Map::playfield_w) - BOX.x - BOX.w); } else if (dx < 0.0F) { snap_dx = std::min(0.0F, static_cast(Map::playfield_x) - BOX.x); } if (dy > 0.0F) { snap_dy = std::max(0.0F, static_cast(Map::playfield_y + Map::playfield_h) - BOX.y - BOX.h); } else if (dy < 0.0F) { snap_dy = std::min(0.0F, static_cast(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 { 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; float dy; Direction dir; }; const std::array 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)