170 lines
8.2 KiB
C++
170 lines
8.2 KiB
C++
#pragma once
|
||
|
||
#include <cstdint>
|
||
#include <optional>
|
||
#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};
|
||
};
|
||
|
||
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;
|
||
|
||
// 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_decals` é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_decals=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_decals = true) const -> DirectionPools;
|
||
|
||
// True si el tile cardinal contigu en la direcció (signe de dx, signe
|
||
// de dy) conté un decal trigger (`is_floor_decal_ && triggers_on_overlap_`)
|
||
// segons `EntityManager::findFloorTriggerAt`. Captura `Hole` i qualsevol
|
||
// futur decal trigger sense hardcodejar tipus. Si `entity_manager_` no
|
||
// està injectat (smoke tests sense manager), retorna false silenciosament
|
||
// — la mòmia es comporta com abans del feature.
|
||
[[nodiscard]] auto isDecalAheadInDir(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;
|
||
|
||
// 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};
|
||
};
|