From 5db3bb0bd07d54bc74b691ec45c5190dca26e9b2 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 27 Apr 2026 17:07:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(mummy):=20estat=20HURT=20amb=20fugida=20en?= =?UTF-8?q?=20l=C3=ADnia=20recta=20i=20recuperaci=C3=B3=20al=20timer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/entities/mummy/mummy.yaml | 10 ++ source/game/entities/concrete/mummy.cpp | 130 ++++++++++++++++++++++++ source/game/entities/concrete/mummy.hpp | 47 +++++++++ 3 files changed, 187 insertions(+) diff --git a/data/entities/mummy/mummy.yaml b/data/entities/mummy/mummy.yaml index 41f6bf3..c3743e7 100644 --- a/data/entities/mummy/mummy.yaml +++ b/data/entities/mummy/mummy.yaml @@ -38,3 +38,13 @@ ai: stats: speed: 30 damage: 1 + +# Estat HURT: post-fuetada, fugint en línia recta. Si la direcció +# preferida (la del whip) està bloquejada, prova perpendiculars; si totes +# ho estan, queda quieta tot el `duration_ms` i després torna a ROAMING. +# `speed` opcional: si no apareix, s'usa `stats.speed`. El `whippable` +# habilita la reacció al látigo (false ⇒ el whip la ignora). +hurt: + duration_ms: 600 + speed: 60 + whippable: true diff --git a/source/game/entities/concrete/mummy.cpp b/source/game/entities/concrete/mummy.cpp index 1edef50..43977d5 100644 --- a/source/game/entities/concrete/mummy.cpp +++ b/source/game/entities/concrete/mummy.cpp @@ -104,6 +104,22 @@ Mummy::Mummy(const fkyaml::node& yaml) } else if (initial == "down") { changeDirection(0.0F, 1.0F); } + + // Configuració de l'estat HURT: opcional. Si el bloc no apareix, la + // mòmia no entrarà mai a HURT efectiu (defensiu); el WhipAttack la + // pot ignorar amb `isWhippable() == false` si volem que no reacció. + if (yaml.contains("hurt")) { + const auto& h = yaml["hurt"]; + if (h.contains("duration_ms")) { + hurt_config_.duration_s = static_cast(h["duration_ms"].get_value()) / 1000.0F; + } + if (h.contains("speed")) { + hurt_config_.speed_px_per_s = h["speed"].get_value(); + } + if (h.contains("whippable")) { + whippable_ = h["whippable"].get_value(); + } + } } void Mummy::changeDirection(float dx, float dy) { @@ -305,4 +321,118 @@ void Mummy::onPlayerContact(Player* player) { } } +void Mummy::enterHurt(Direction flee_dir) { + state_ = State::HURT; + hurt_remaining_s_ = hurt_config_.duration_s; + hurt_speed_ = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_; + + const Direction CHOSEN = chooseHurtDirection(flee_dir); + if (CHOSEN == Direction::NONE) { + // Cap direcció lliure: la mòmia queda quieta però en HURT durant + // tot el timer. Posem dir a 0 perquè `hurtStep` no es mou. + setDirection(0.0F, 0.0F); + } else { + applyChoice(CHOSEN); + } +} + +auto Mummy::chooseHurtDirection(Direction preferred) const -> Direction { + // Avenç ~1 frame a 60 FPS amb la velocitat de HURT. Tan curt com + // perquè un canvi sub-tile no decideixe; suficient perquè un mur + // adjacent (o sarc tocant la caja) es detecte. + const float TEST_SPEED = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_; + const float STEP = TEST_SPEED * (1.0F / 60.0F); + + auto is_open = [this, STEP](Direction d) -> bool { + float dx = 0.0F; + float dy = 0.0F; + switch (d) { + case Direction::UP: + dy = -STEP; + break; + case Direction::DOWN: + dy = STEP; + break; + case Direction::LEFT: + dx = -STEP; + break; + case Direction::RIGHT: + dx = STEP; + break; + case Direction::NONE: + return false; + } + const auto BOX = candidateBox(dx, dy); + return Map::insidePlayfield(BOX) && !Map::collides(BOX); + }; + + if (preferred != Direction::NONE && is_open(preferred)) { return preferred; } + + // Perpendiculars segons la direcció preferida (ordre arbitrari però + // consistent: "esquerra del whip" primer). + Direction left = Direction::NONE; + Direction right = Direction::NONE; + switch (preferred) { + case Direction::UP: + left = Direction::LEFT; + right = Direction::RIGHT; + break; + case Direction::DOWN: + left = Direction::RIGHT; + right = Direction::LEFT; + break; + case Direction::LEFT: + left = Direction::DOWN; + right = Direction::UP; + break; + case Direction::RIGHT: + left = Direction::UP; + right = Direction::DOWN; + break; + case Direction::NONE: + return Direction::NONE; + } + if (is_open(left)) { return left; } + if (is_open(right)) { return right; } + + return Direction::NONE; +} + +void Mummy::hurtStep(float delta_time) { + // Si està quieta (totes les direccions bloquejades a `enterHurt`), no + // avança ni surt: l'únic camí de sortida és el timer. + if (dir_x_ == 0.0F && dir_y_ == 0.0F) { return; } + + const float DX = dir_x_ * hurt_speed_ * delta_time; + const float DY = dir_y_ * hurt_speed_ * delta_time; + const auto CANDIDATE = candidateBox(DX, DY); + if (Map::insidePlayfield(CANDIDATE) && !Map::collides(CANDIDATE)) { + pixel_rect_.x += DX; + pixel_rect_.y += DY; + return; + } + + // Topa amb mur o sarc: surt del HURT immediatament. La IA normal + // re-decideix al pròxim step (no fem U-turn ni re-orientació aquí). + state_ = State::ROAMING; + hurt_remaining_s_ = 0.0F; +} + +void Mummy::update(float delta_time) { + if (state_ == State::HURT) { + // Avancem l'animació inline (no podem cridar `Entity::update` ni + // `DynamicHazard::update`: el primer salta una capa virtual i el + // segon dispararia `step()` amb la lògica d'IA). Volem només el + // tick d'animació + el moviment recta del HURT. + if (anim_.has_value()) { anim_->tick(delta_time); } + hurtStep(delta_time); + hurt_remaining_s_ -= delta_time; + if (hurt_remaining_s_ <= 0.0F && state_ == State::HURT) { + state_ = State::ROAMING; + } + return; + } + DynamicHazard::update(delta_time); +} + REGISTER_ENTITY("mummy", Mummy) diff --git a/source/game/entities/concrete/mummy.hpp b/source/game/entities/concrete/mummy.hpp index 937f763..e7ab31a 100644 --- a/source/game/entities/concrete/mummy.hpp +++ b/source/game/entities/concrete/mummy.hpp @@ -21,6 +21,21 @@ class Mummy : public DynamicHazard { LEFT, RIGHT }; + // Estat d'alt nivell. ROAMING = IA normal (corridor_wanderer); HURT = + // post-fuetada, fugint en línia recta durant `hurt_config_.duration_s`. + // Mentre dura el HURT no decideix a creuaments ni canvia de direcció; + // si topa amb mur o sarc, surt del HURT abans de l'expiració del timer. + enum class State : std::uint8_t { ROAMING, + HURT }; + + // Configuració de l'estat HURT, llegida del bloc `hurt:` del YAML. + // Si `duration_s == 0` la mòmia mai entra a HURT efectiu (defensiu). + // `speed_px_per_s == 0` ⇒ s'usa la velocitat normal de la mòmia. + struct HurtConfig { + float duration_s{0.0F}; + float speed_px_per_s{0.0F}; + }; + explicit Mummy(const fkyaml::node& yaml); // Hook d'IA. Donat un conjunt de direccions admissibles, decideix la @@ -39,9 +54,22 @@ class Mummy : public DynamicHazard { // En ambdós casos el frame de gir no consumeix avenç extra. void step(float delta_time) override; + // Update sencer: afegim la branca HURT (avanç del timer + step recta). + // En estat ROAMING delega a `step()` com sempre via la base. + void update(float delta_time) override; + // Contacte amb el jugador: publica `player_hit` amb el dany. void onPlayerContact(Player* player) override; + // Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`. + // Si està bloquejada, prova perpendiculars; si totes estan bloquejades, + // la mòmia queda quieta però en HURT durant tot el timer. + void enterHurt(Direction flee_dir); + + [[nodiscard]] auto getState() const -> State { return state_; } + [[nodiscard]] auto isWhippable() const -> bool { return whippable_; } + [[nodiscard]] auto getHurtRemainingS() const -> float { return hurt_remaining_s_; } + private: // Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy). [[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect; @@ -90,9 +118,28 @@ class Mummy : public DynamicHazard { // S'usa al pool de creuament per afegir la recta com a candidata. [[nodiscard]] auto currentDirection() const -> Direction; + // Avança recta a `hurt_speed_ * delta_time`. Si la caixa candidata + // sortiria del playfield o trepitjaria un sarc, marca la sortida del + // HURT abans de l'expiració del timer (la mòmia no rebota). + void hurtStep(float delta_time); + + // Tria la direcció definitiva del HURT donada la preferida (la del + // whip). Si la preferida està bloquejada, prova perpendiculars; si + // totes ho estan, retorna `NONE` (la mòmia queda quieta en HURT). + [[nodiscard]] auto chooseHurtDirection(Direction preferred) 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_; AnimationComponent anim_walk_up_; AnimationComponent anim_walk_lateral_; + + // Estat HURT i la seua configuració. `hurt_speed_` és l'efectiva + // (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la + // velocitat normal de la mòmia). + State state_{State::ROAMING}; + HurtConfig hurt_config_; + bool whippable_{true}; + float hurt_remaining_s_{0.0F}; + float hurt_speed_{0.0F}; };