#pragma once #include #include #include #include #include #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* 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& { 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 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 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}; // `entity_manager_` viu a Entity (protected): l'utilitzem d'allà. const std::vector* players_{nullptr}; DirSprite sprite_north_; DirSprite sprite_south_; DirSprite sprite_east_; DirSprite sprite_west_; };