feat(whip): separa draw_rect i damage_rect; sprites 80×32 / 16×96; feet_y delegat a l'owner; recover_* per al cooldown

This commit is contained in:
JailDesigner committed 2026-04-28 18:28:19 +02:00
1 parent 121558b1fd
commit 8f66961c7b
15 files changed
+353 -127

No files matched your search

+65 -2
View File
@@ -89,18 +89,81 @@ WhipAttack::WhipAttack(const fkyaml::node& yaml) {
anim_ = anim_swing_;
}
void WhipAttack::configure(Direction dir, float spawn_x, float spawn_y, float range_px) {
void WhipAttack::configure(Direction dir, const SDL_FRect& owner_box, float range_px) {
direction_ = dir;
range_px_ = range_px;
// Draw rect (= pixel_rect_): cobreix tot el sprite del látigo segons
// la taula d'offsets del disseny:
// E: (player.x - 16, player.y ), 80×32
// W: (player.x - 48, player.y ), 80×32
// N: (player.x, player.y - 48 ), 16×96
// S: (player.x, player.y - 16 ), 16×96
const auto& chosen = spriteFor(dir);
setPixelRect({.x = spawn_x, .y = spawn_y, .w = chosen.frame_w, .h = chosen.frame_h});
SDL_FRect draw{0.0F, 0.0F, chosen.frame_w, chosen.frame_h};
switch (dir) {
case Direction::EAST:
draw.x = owner_box.x - 16.0F;
draw.y = owner_box.y;
break;
case Direction::WEST:
draw.x = owner_box.x - 48.0F;
draw.y = owner_box.y;
break;
case Direction::NORTH:
draw.x = owner_box.x;
draw.y = owner_box.y - 48.0F;
break;
case Direction::SOUTH:
draw.x = owner_box.x;
draw.y = owner_box.y - 16.0F;
break;
}
setPixelRect(draw);
// Damage rect: 48×16 (E/W) o 16×48 (N/S) al nivell dels peus del
// jugador (els últims 16 px del seu pixel_rect — convenció estàndard
// del player AEE), pegat al borde en la direcció d'atac.
// computeDamageRect rep la cantonada del rect MÀXIM (sense retallar)
// i recorre internament la línia per topar amb el primer bloquejador.
const float COLLISION_TOP_Y = owner_box.y + owner_box.h - 16.0F;
const float OWNER_RIGHT = owner_box.x + owner_box.w;
const float OWNER_BOTTOM = owner_box.y + owner_box.h;
float spawn_x = 0.0F;
float spawn_y = 0.0F;
switch (dir) {
case Direction::EAST:
spawn_x = OWNER_RIGHT;
spawn_y = COLLISION_TOP_Y;
break;
case Direction::WEST:
spawn_x = owner_box.x - range_px;
spawn_y = COLLISION_TOP_Y;
break;
case Direction::NORTH:
spawn_x = owner_box.x;
spawn_y = COLLISION_TOP_Y - range_px;
break;
case Direction::SOUTH:
spawn_x = owner_box.x;
spawn_y = OWNER_BOTTOM;
break;
}
computeDamageRect(dir, spawn_x, spawn_y, range_px);
// Snapshot del feet_y del jugador. La convenció estàndard del Player
// AEE té el seu feet_y a la base inferior del pixel_rect (collision
// 16×16 als últims 16 px del sprite 16×32), així que owner_box.y +
// owner_box.h reprodueix `Player::getFeetY()` exactament.
owner_feet_y_ = owner_box.y + owner_box.h;
if (anim_.has_value()) { anim_->reset(); }
}
auto WhipAttack::getFeetY() const -> float {
return owner_feet_y_;
}
void WhipAttack::setWorldContext(EntityManager* manager, const std::vector<Player*>* players) {
entity_manager_ = manager;
players_ = players;
+43 -14
View File
@@ -18,16 +18,27 @@ 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. La mida del frame
// canvia entre direccions (E/W: 48×16, N/S: 16×48), cosa que el sistema
// d'animació actual no permet expressar amb una sola surface (una entitat
// = una graella). Per això el YAML llista les 4 entrades sota `sprites:`
// 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ó.
//
// Hitbox de dany: el rectangle de dany és el `pixel_rect_` retallat al
// primer bloquejador en la línia d'atac (mur extern del playfield o
// sarcòfag). Aquesta lògica viu a la fase de damage application; en
// aquest skeleton només viu la part visual i de cicle de vida.
// 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
@@ -53,12 +64,16 @@ class WhipAttack : public Attack {
explicit WhipAttack(const fkyaml::node& yaml);
// Configura l'estat runtime de la instància. Cal cridar-lo abans del
// primer `update`. Selecciona la surface i la mida del pixel_rect_
// segons direcció, i posa la cantonada superior-esquerra de la
// hitbox a (spawn_x, spawn_y). També calcula la `damage_rect_`
// retallada contra murs i sarcs (mai sortiria del playfield ni
// travessaria un sarc).
void configure(Direction dir, float spawn_x, float spawn_y, float range_px);
// 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
@@ -90,6 +105,14 @@ class WhipAttack : public Attack {
// 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ó
@@ -123,6 +146,12 @@ class WhipAttack : public Attack {
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};
+4 -2
View File
@@ -140,8 +140,10 @@ class Entity {
// Y dels peus: la vora inferior de la caixa de col·lisió. La usarà el
// Z-order per ordenar entitats i jugador de menys a més profunditat
// (REND-01, quan s'implemente).
[[nodiscard]] auto getFeetY() const -> float;
// (REND-01, quan s'implemente). Virtual perquè entitats efímeres
// (com `WhipAttack`) poden delegar al seu owner per quedar al
// mateix bucket d'y-sort que el jugador que les ha llançat.
[[nodiscard]] virtual auto getFeetY() const -> float;
// El loader li assigna una caixa explícita llegida del YAML; sense
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
+11 -6
View File
@@ -1,5 +1,7 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstddef>
#include <cstdint>
#include <functional>
@@ -57,15 +59,18 @@ struct WorldPos {
// - `owner` : Player que ha llançat l'atac (no rep dany propi).
// - `direction` : Player::Cardinal castat a int (1=N, 2=S, 3=E, 4=W) per
// evitar dependència cíclica al header.
// - `spawn_x/y` : cantonada superior-esquerra de la hitbox màxima
// (sense retallar) en píxels.
// - `range_px` : longitud de la hitbox abans de retallar contra murs i
// sarcòfags. Llegida del YAML del jugador.
// - `owner_box` : pixel_rect del jugador (típicament 16×32 a AEE). El
// WhipAttack en deriva tant la `draw_rect_` (sprite del
// látigo, més gran que el jugador per cobrir wind-up i
// solapament) com la `damage_rect_` (caixa de dany al
// nivell dels peus, retallada per murs/sarcs).
// - `range_px` : longitud nominal de la hitbox de dany abans de
// retallar contra murs i sarcòfags. Llegida del YAML
// del jugador.
struct WhipSpawnPayload {
Player* owner{nullptr};
int direction{0};
float spawn_x{0.0F};
float spawn_y{0.0F};
SDL_FRect owner_box{0.0F, 0.0F, 0.0F, 0.0F};
float range_px{0.0F};
};
+23 -30
View File
@@ -660,47 +660,36 @@ void Player::enterAttacking(Cardinal facing) {
// p.ex. just després d'un canvi d'input).
applyCardinal(facing);
// Geometria de spawn de la hitbox: alineada al rectangle de
// col·lisió del jugador, no al sprite (que té perspectiva falsa amb
// el cap dalt). La hitbox és `range_px × tile_size` (E/W) o
// `tile_size × range_px` (N/S); el GameScene la consumeix per
// crear el WhipAttack amb la mida adequada per direcció.
const auto BOX = getBaseBox();
const float RANGE_PX = whip_config.range_tiles * static_cast<float>(Map::tile_size);
float spawn_x = 0.0F;
float spawn_y = 0.0F;
if (facing == Cardinal::NONE) { return; }
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
int dir_int = 0;
switch (facing) {
case Cardinal::EAST:
spawn_x = BOX.x + BOX.w;
spawn_y = BOX.y;
dir_int = 3;
break;
case Cardinal::WEST:
spawn_x = BOX.x - RANGE_PX;
spawn_y = BOX.y;
dir_int = 4;
break;
case Cardinal::NORTH:
spawn_x = BOX.x;
spawn_y = BOX.y - RANGE_PX;
dir_int = 1;
break;
case Cardinal::SOUTH:
spawn_x = BOX.x;
spawn_y = BOX.y + BOX.h;
dir_int = 2;
break;
case Cardinal::EAST:
dir_int = 3;
break;
case Cardinal::WEST:
dir_int = 4;
break;
case Cardinal::NONE:
return;
}
// Passem el pixel_rect (16×32 a sam/morcus) al payload; el WhipAttack
// en deriva tant la draw_rect (sprite del látigo, més gran) com la
// damage_rect (al nivell dels peus, retallada per bloquejadors).
if (bus_ != nullptr) {
const float RANGE_PX = whip_config.range_tiles * static_cast<float>(Map::tile_size);
bus_->publish("whip_spawn", WhipSpawnPayload{
.owner = this,
.direction = dir_int,
.spawn_x = spawn_x,
.spawn_y = spawn_y,
.owner_box = getPosition(),
.range_px = RANGE_PX,
});
}
@@ -1141,8 +1130,12 @@ auto Player::animationName(bool moving) const -> const char* {
}
// ATTACKING té dues sub-fases: durant `swing_ms` el sprite és
// `attack_*` (animació del braç llançant el látigo); durant
// `cooldown_ms` posterior tornem al `stand_*` de la direcció. Fem
// la decisió comparant el remaining_ms_ amb el cooldown configurat.
// `cooldown_ms` posterior canviem a `recover_*` per a frames
// pròpis de recuperació. Mentre l'art no estiga dibuixat, els
// YAMLs apunten `recover_*` als mateixos frames que `stand_*` —
// visualment és idèntic, però la separació deixa l'arquitectura
// preparada perquè en afegir frames reals només calga tocar el
// YAML, no el codi.
if (state_ == State::ATTACKING) {
const bool IN_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
if (IN_SWING) {
@@ -1158,13 +1151,13 @@ auto Player::animationName(bool moving) const -> const char* {
}
switch (facing_) {
case Facing::UP:
return "stand_up";
return "recover_up";
case Facing::DOWN:
return "stand_down";
return "recover_down";
case Facing::SIDE:
break;
}
return "stand";
return "recover";
}
switch (facing_) {
case Facing::UP: