#include "game/entities/concrete/mummy.hpp" #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}; } } // 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; } // Prova d'avançar. 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::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); if (IS_CURRENT) { continue; } // ja sabem que està bloquejada 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)