Files
aee-arcade/source/game/entities/concrete/mummy.hpp
T

209 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include <climits>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#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};
std::string sound_key; // Buit = sense so. El fitxer viu a Defaults::Paths::AUDIO_SOUNDS_DIR.
};
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<Direction>& 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;
// Contacte amb el fueteig: si la mòmia és whippable, entra a HURT amb
// la direcció derivada del whip. Retorna true si efectivament ha
// rebut dany — el WhipAttack ho usa per al `already_hit_`. Substitueix
// el `dynamic_cast<Mummy*>` que feia el whip abans per discriminar.
auto onWhipHit(WhipAttack* attack) -> bool 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<Direction> perp;
std::vector<Direction> 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<float>;
// 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_;
// Animació d'aparició (no-loop). Mentre `isSpawning()`, `update()`
// la tickeja sense moure la mòmia; quan `completed()`, la mòmia
// transita a ACTIVE via `endSpawn()` i conmuta a la walk_* que
// toca segons direcció.
AnimationComponent anim_raise_;
// 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};
};