163 lines
7.4 KiB
C++
163 lines
7.4 KiB
C++
#pragma once
|
||
|
||
#include <SDL3/SDL.h>
|
||
|
||
#include <cstdint>
|
||
#include <string>
|
||
#include <unordered_set>
|
||
#include <vector>
|
||
|
||
#include "external/fkyaml_node.hpp"
|
||
#include "game/entities/animation_component.hpp"
|
||
#include "game/entities/attack.hpp"
|
||
|
||
class Entity;
|
||
class EntityManager;
|
||
class Player;
|
||
|
||
// Atac de látigo: spawnejat pel jugador en entrar a `ATTACKING`. Viu la
|
||
// durada de la seua animació (`swing`) i s'autoelimina al final.
|
||
//
|
||
// Sprites: 4 fitxers GIF distints, un per cardinal (sense flips). La
|
||
// mida del frame canvia entre direccions (E/W: 80×32, N/S: 16×96),
|
||
// cosa que el sistema d'animació actual no permet expressar amb una
|
||
// sola surface. Per això el YAML llista les 4 entrades sota `sprites:`
|
||
// i `configure()` tria la que toca segons direcció.
|
||
//
|
||
// Dos rectangles distints, intencionadament diferenciats:
|
||
// - `pixel_rect_` (heretat d'`Entity`) = `draw_rect`. Cobreix tot el
|
||
// sprite del látigo: 16 px de wind-up darrere del jugador, 16 px
|
||
// de solapament amb el jugador, i 48 px d'atac frontal (per E/W;
|
||
// proporcions equivalents per N/S amb 32 px de solapament). NO es
|
||
// retalla per bloquejadors — només render visual.
|
||
// - `damage_rect_` = caixa de dany al nivell dels peus del jugador
|
||
// (centrada sobre la base 16×16). 48×16 (E/W) o 16×48 (N/S),
|
||
// pegada al borde del jugador en la direcció d'atac. SÍ es retalla
|
||
// pel primer bloquejador (mur extern del playfield o sarcòfag).
|
||
//
|
||
// Z-order: `getFeetY()` delega al `owner_` per garantir que el látigo
|
||
// quede al mateix bucket d'y-sort que el jugador que l'ha llançat. Si
|
||
// no, l'altura física del sprite (esp. en N/S, que té 96 px) crearia
|
||
// solapaments raros amb mòmies a la trajectòria.
|
||
class WhipAttack : public Attack {
|
||
public:
|
||
// Direcció cardinal del swing. Coincideix valors amb
|
||
// `Player::Cardinal` (1=N, 2=S, 3=E, 4=W) per facilitar la
|
||
// conversió a la frontera de l'event `whip_spawn`.
|
||
enum class Direction : std::uint8_t {
|
||
NORTH = 1,
|
||
SOUTH = 2,
|
||
EAST = 3,
|
||
WEST = 4,
|
||
};
|
||
|
||
// Definició estàtica d'una direcció: clau al `Resource::Cache` i mida
|
||
// del frame en píxels. Llegida del YAML una vegada al ctor; usada per
|
||
// configure() per omplir surface_ i pixel_rect_. Pública perquè el
|
||
// helper d'anàlisi del YAML viu en un namespace anònim al .cpp.
|
||
struct DirSprite {
|
||
std::string surface_key;
|
||
float frame_w{0.0F};
|
||
float frame_h{0.0F};
|
||
};
|
||
|
||
explicit WhipAttack(const fkyaml::node& yaml);
|
||
|
||
// Configura l'estat runtime de la instància. Cal cridar-lo abans del
|
||
// primer `update`. A partir del `owner_box` (pixel_rect del jugador,
|
||
// típicament 16×32) i la direcció d'atac, deriva:
|
||
// - `pixel_rect_` (= draw_rect): cantonada i mida del sprite del
|
||
// látigo segons la taula d'offsets (E: -16, 0, 80×32; etc.).
|
||
// - `damage_rect_`: caixa de dany 48×16 (o 16×48) pegada al
|
||
// borde del jugador, retallada contra murs i sarcs.
|
||
// Convenció implícita: el jugador té collision rect 16×16 als
|
||
// últims 16 px del seu pixel_rect (els "peus"); aquesta funció
|
||
// n'usa la geometria per al damage_rect i el feet_y delegat.
|
||
void configure(Direction dir, const SDL_FRect& owner_box, float range_px);
|
||
|
||
// Injecta el context que necessita la fase de dany: punter a
|
||
// l'`EntityManager` (per iterar Hazards) i a la llista de jugadors
|
||
// (per al friendly fire). Si no es crida, els frames actius no
|
||
// apliquen dany — el whip queda merament visual. Cridar abans del
|
||
// primer `update` (típicament just després de `configure`).
|
||
void setWorldContext(EntityManager* manager, const std::vector<Player*>* players);
|
||
|
||
// Avança animació; durant frames actius, aplica dany; quan
|
||
// l'animació es completa, marca dead.
|
||
void update(float delta_time) override;
|
||
|
||
[[nodiscard]] auto getDirection() const -> Direction { return direction_; }
|
||
|
||
// True si el frame actual de l'animació és un dels `active_frames`
|
||
// declarats al YAML (els frames durant els quals la hitbox aplica
|
||
// dany). Wind-up i recovery retornen false.
|
||
[[nodiscard]] auto isFrameActive() const -> bool;
|
||
|
||
// Llistat dels frames actius (lectura del YAML, immutable per
|
||
// instància). Exposat per als smoke tests.
|
||
[[nodiscard]] auto getActiveFrames() const -> const std::vector<int>& { return active_frames_; }
|
||
|
||
// Llargada de la hitbox abans de retallar contra murs/sarcs (en píxels).
|
||
[[nodiscard]] auto getRangePx() const -> float { return range_px_; }
|
||
|
||
// Hitbox efectiva de dany (ja retallada pel primer bloquejador en la
|
||
// línia d'atac). Exposada per als smoke tests; en producció només la
|
||
// consumeix la pròpia `applyDamage`.
|
||
[[nodiscard]] auto getDamageRect() const -> SDL_FRect { return damage_rect_; }
|
||
|
||
// Y dels peus delegada al `owner_box` capturat a `configure()`: el
|
||
// látigo es z-ordena al mateix bucket que el jugador que l'ha
|
||
// llançat (= base inferior del seu pixel_rect, que coincideix amb
|
||
// el feet_y del jugador a la convenció estàndard del player AEE).
|
||
// Snapshot, no consulta dinàmica: el jugador queda quiet durant
|
||
// ATTACKING, així que snapshot ≡ live per a la vida del whip.
|
||
[[nodiscard]] auto getFeetY() const -> float override;
|
||
|
||
// Clau al `Resource::Cache` per a la surface de la direcció actual
|
||
// (la que ha triat `configure()`). El caller (típicament `GameScene`)
|
||
// resol la surface i la passa via `setSurface()`. Aquesta separació
|
||
// manté el codi de l'entitat sense dependència directa al
|
||
// `Resource::Cache`, igual que la resta d'entitats del joc.
|
||
[[nodiscard]] auto getSurfaceKey() const -> const std::string&;
|
||
|
||
private:
|
||
[[nodiscard]] auto spriteFor(Direction dir) const -> const DirSprite&;
|
||
|
||
// Calcula la `damage_rect_` final: retalla la longitud de l'atac
|
||
// contra el primer bloquejador (mur extern del playfield o sarc) en
|
||
// la línia d'atac. Implementació iterativa píxel a píxel — el rang
|
||
// típic és ~48 px, així que el cost és menyspreable.
|
||
void computeDamageRect(Direction dir, float spawn_x, float spawn_y, float range_px);
|
||
|
||
// Aplica dany aquest frame: itera Hazards (Mummy whippable) i
|
||
// jugadors (excepte `owner_`) que solapen amb `damage_rect_` i
|
||
// estan en estat "atacable", crida `enterHurt` amb la direcció
|
||
// adequada i els marca a `already_hit_` per garantir un sol
|
||
// impacte per swing per objectiu.
|
||
void applyDamage();
|
||
|
||
Direction direction_{Direction::EAST};
|
||
AnimationComponent anim_swing_;
|
||
std::vector<int> active_frames_;
|
||
// Set d'objectius ja colpejats aquest swing: usem `void*` perquè
|
||
// hi guardem tant `Entity*` (Mummy) com `Player*` (que NO hereta
|
||
// d'Entity) sense `reinterpret_cast` semànticament dubtós.
|
||
std::unordered_set<void*> already_hit_;
|
||
float range_px_{0.0F};
|
||
SDL_FRect damage_rect_{0.0F, 0.0F, 0.0F, 0.0F};
|
||
|
||
// Snapshot del feet_y de l'owner pres a `configure()` (= base
|
||
// inferior del seu pixel_rect, que coincideix amb el `getFeetY()`
|
||
// del Player en la convenció estàndard d'AEE). El retornem des
|
||
// del nostre `getFeetY()` per delegar el bucket d'y-sort al jugador.
|
||
float owner_feet_y_{0.0F};
|
||
|
||
EntityManager* entity_manager_{nullptr};
|
||
const std::vector<Player*>* players_{nullptr};
|
||
|
||
DirSprite sprite_north_;
|
||
DirSprite sprite_south_;
|
||
DirSprite sprite_east_;
|
||
DirSprite sprite_west_;
|
||
};
|