#pragma once #include #include #include #include #include "external/fkyaml_node.hpp" #include "game/entities/animation_component.hpp" #include "game/entities/hazard.hpp" // Mòmia: perill dinàmic que deambula pels passadís. A cada intersecció // tria una direcció vàlida; l'algoritme per triar-la és un hook virtual // (`chooseDirection`) que la base implementa com a aleatori uniforme // sobre les direccions admissibles. Futurs derivats poden sesgar cap al // jugador sense tocar la lògica de moviment. class Mummy : public DynamicHazard { public: enum class Direction : std::uint8_t { NONE, NORTH, SOUTH, WEST, EAST }; // 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 // següent direcció a prendre. La implementació de la base tria una // aleatoria; si `allowed` està buit, retorna `NONE`. virtual auto chooseDirection(const std::vector& allowed) -> Direction; // Política "corridor_wanderer": // (a) A cada creuament de corredor (edge-detect de travessar la línia // central d'un corredor perpendicular) amarra la caixa al centre // exacte del creuament i decideix: seguir recta o girar a una // 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; // 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_; } [[nodiscard]] auto killsOutright() const -> bool { return kills_outright_; } private: // Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy). [[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect; // Amarra la caja a la vora exacta del playfield quan el candidat hi // sortiria — sense això, amb la caja 16×16 iguala el corredor, la // posició subpíxel dins de l'última cel·la impediria girar a cap // perpendicular (col·lisionaria amb el sarc adjacent). Es crida abans // de la lògica de gir. void snapToPlayfieldEdge(float dx, float dy); // Recull les direccions alternatives viables des de la posició actual: // `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou // perquè ja sabem que està bloquejada. Cada candidata es valida amb // `Map::insidePlayfield` + `Map::collides` sobre la caja resultant. Si // `avoid_blocked` és true i hi ha `EntityManager` injectat, també es // descarten les direccions que apunten a un tile amb un decal trigger // (Hole o futurs pinchos plans/charcos). El caller és responsable de // re-cridar amb `avoid_blocked=false` com a fallback si totes les // direccions filtrades estan bloquejades. struct DirectionPools { std::vector perp; std::vector uturn; }; [[nodiscard]] auto collectDirectionPools(float step, bool avoid_blocked = true) const -> DirectionPools; // True si el tile cardinal contigu en la direcció (signe de dx, signe // de dy) està bloquejat per qualsevol motiu: fora del playfield, ocupat // per geometria estàtica (sarcs, door_corridor tancat) via `Map::collides`, // o conté un decal trigger (`is_floor_decal_ && triggers_on_overlap_`) // segons `EntityManager::findFloorTriggerAt`. Captura `Hole` i qualsevol // futur decal trigger sense hardcodejar tipus, i evita que la mòmia // triï a un creuament una direcció que el frame següent toparia (cas // típic: a la intersecció anterior al límit nord del playfield, on el // pas immediat encara hi cap però el tile sencer següent ja està fora). // Si `entity_manager_` no està injectat (smoke tests sense manager), // el filtre de decals és silenciós però els altres dos (playfield i // collides) segueixen actius. [[nodiscard]] auto isBlockedAheadInDir(float dx_signed, float dy_signed) const -> bool; // Canvi de direcció + selecció de l'animació corresponent (un cardinal // per branch). Tots els camins que trien nova direcció (ctor amb // `initial_direction`, gir després d'un bloqueig) passen per ací; el // flip viu al YAML (`walk_west: flip_h: true`) i es propaga des de // l'animació al `flip_` heretat. void changeDirection(float dx, float dy); // Aplica la direcció triada (wrapper del switch en una sola crida). 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; // 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; // 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; // Detecció reactiva al canvi de tile (substitueix el lookahead sub-tile // entre creuaments). Es crida al principi de cada `step()`. Si el tile // que ocupa la mòmia (calculat des del centre de la BOX) ha canviat // respecte a l'última crida, comprova si el tile nou conté un decal // trigger; si el conté, dispara mitja volta immediata. La primera crida // (last_tile_col_ == INT_MIN) només registra el tile actual sense // reaccionar — evita un falç positiu si la mòmia spawneja sobre un // decal o si el setup encara no és estable. void reactToCurrentTile(); // Animacions carregades del YAML, una per cada cardinal. La variant // `walk_west` reaprofita els frames de l'est però amb `flip_h: true` // declarat al YAML — el motor d'animació aplica el flip al sprite // automàticament; aquesta classe no toca mai `flip_` directament. AnimationComponent anim_walk_south_; AnimationComponent anim_walk_north_; AnimationComponent anim_walk_east_; AnimationComponent anim_walk_west_; // 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}; // Si true, el contacte amb el jugador dispara `Player::enterDying` // (mort d'un sol toc, bypass del flux `hurt`/knockback). Llegit del // bloc `stats.kills_outright` del YAML; default true per coherència // amb el comportament d'Oh Mummy! (les mòmies maten). bool kills_outright_{true}; // Tile que ocupava la mòmia a l'última crida a `reactToCurrentTile`. // Inicialitzats a `INT_MIN` perquè la primera crida (lazy init: no // podem fer-ho al constructor perquè `pixel_rect_` encara no té // posició; setPixelRect del manager arriba després) registra el tile // de spawn sense reaccionar. int last_tile_col_{INT_MIN}; int last_tile_row_{INT_MIN}; };