feat(whip): separa draw_rect i damage_rect; sprites 80×32 / 16×96; feet_y delegat a l'owner; recover_* per al cooldown
No files matched your search
@@ -1,15 +1,26 @@
|
|||||||
name: whip_attack
|
name: whip_attack
|
||||||
|
|
||||||
# Quatre sprites distints, un per cardinal. El sistema d'animació actual
|
# Quatre sprites distints, un per cardinal. El sistema d'animació actual
|
||||||
# només permet UNA mida de frame per surface, i les hitboxes horitzontals
|
# només permet UNA mida de frame per surface, i els sprites del látigo
|
||||||
# (E/W: 48×16) i verticals (N/S: 16×48) tenen aspect ratios distints; per
|
# tenen aspect ratios distints segons direcció. Per això són 4 fitxers
|
||||||
# això són 4 fitxers en lloc d'una sola sheet compartida amb flips.
|
# en lloc d'una sola sheet compartida amb flips.
|
||||||
# Tots els .gif viuen al mateix directori que aquest YAML.
|
#
|
||||||
|
# Mides (= `draw_rect` del WhipAttack, que NO es retalla per
|
||||||
|
# bloquejadors):
|
||||||
|
# E/W → 80×32 px per frame: 16 px de wind-up darrere del jugador
|
||||||
|
# + 16 px de solapament + 48 px d'atac
|
||||||
|
# frontal. Cobreix tota l'altura del jugador.
|
||||||
|
# N/S → 16×96 px per frame: 48 px d'atac + 32 px solapant amb el jugador
|
||||||
|
# + 16 px de wind-up "darrere".
|
||||||
|
#
|
||||||
|
# La caja de DANY és independent i més petita (48×16 / 16×48), pegada
|
||||||
|
# al borde del jugador i retallada per murs/sarcs. La calcula
|
||||||
|
# `WhipAttack::configure` a partir del `owner_box` del payload.
|
||||||
sprites:
|
sprites:
|
||||||
east: { path: whip_east.gif, size: [48, 16] }
|
east: { path: whip_east.gif, size: [80, 32] }
|
||||||
west: { path: whip_west.gif, size: [48, 16] }
|
west: { path: whip_west.gif, size: [80, 32] }
|
||||||
north: { path: whip_north.gif, size: [16, 48] }
|
north: { path: whip_north.gif, size: [16, 96] }
|
||||||
south: { path: whip_south.gif, size: [16, 48] }
|
south: { path: whip_south.gif, size: [16, 96] }
|
||||||
|
|
||||||
# Animació compartida entre les 4 direccions: la seqüència de frames i les
|
# Animació compartida entre les 4 direccions: la seqüència de frames i les
|
||||||
# durades són idèntiques; només canvia quina surface/mida es clipa.
|
# durades són idèntiques; només canvia quina surface/mida es clipa.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 313 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 369 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 313 B |
@@ -68,8 +68,12 @@ animations:
|
|||||||
frames: [8, 9, 10, 11]
|
frames: [8, 9, 10, 11]
|
||||||
frame_ms: 100
|
frame_ms: 100
|
||||||
|
|
||||||
# Placeholder: igual que sam.yaml, reaprofitem `walk_*` fins que
|
# Placeholder mentre no arribe l'art real (el GIF de morcus encara no té
|
||||||
# arribe l'art específic del braç llançant el látigo.
|
# frames d'atac específics — reaprofitem `walk_*`). El bloc està declarat
|
||||||
|
# perquè `Player::animationName` el busque sense fallback i la mecànica
|
||||||
|
# de l'atac estiga lligada amb morcus tan bon punt s'afegisquen els
|
||||||
|
# frames; ara mateix visualment el sprite caminarà mentre llança el
|
||||||
|
# látigo (acceptat per al placeholder).
|
||||||
attack_down:
|
attack_down:
|
||||||
frames: [0, 1, 2, 3]
|
frames: [0, 1, 2, 3]
|
||||||
frame_ms: 50
|
frame_ms: 50
|
||||||
@@ -82,6 +86,18 @@ animations:
|
|||||||
frames: [8, 9, 10, 11]
|
frames: [8, 9, 10, 11]
|
||||||
frame_ms: 50
|
frame_ms: 50
|
||||||
|
|
||||||
|
# Cooldown post-swing (recovery): igual que a sam.yaml, placeholder als
|
||||||
|
# frames de `stand_*`. Quan arriben els frames reals només cal canviar
|
||||||
|
# els índexs ací.
|
||||||
|
recover_down:
|
||||||
|
frames: [0]
|
||||||
|
|
||||||
|
recover_up:
|
||||||
|
frames: [4]
|
||||||
|
|
||||||
|
recover:
|
||||||
|
frames: [8]
|
||||||
|
|
||||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -73,17 +73,31 @@ animations:
|
|||||||
# (TODO: dibuixar-ne d'específics quan arribe l'art). El sprite del
|
# (TODO: dibuixar-ne d'específics quan arribe l'art). El sprite del
|
||||||
# látigo viu al `WhipAttack` i es renderitza superposat per z-order.
|
# látigo viu al `WhipAttack` i es renderitza superposat per z-order.
|
||||||
attack_down:
|
attack_down:
|
||||||
frames: [0, 1, 2, 3]
|
frames: [28, 29, 30, 31]
|
||||||
frame_ms: 50
|
frame_ms: 50
|
||||||
|
|
||||||
attack_up:
|
attack_up:
|
||||||
frames: [4, 5, 6, 7]
|
frames: [24, 25, 26, 27]
|
||||||
frame_ms: 50
|
frame_ms: 50
|
||||||
|
|
||||||
attack:
|
attack:
|
||||||
frames: [8, 9, 10, 11]
|
frames: [20, 21, 22, 23]
|
||||||
frame_ms: 50
|
frame_ms: 50
|
||||||
|
|
||||||
|
# Animacions de cooldown post-swing (recovery): mentre dura
|
||||||
|
# `whip_cooldown_ms` el jugador continua bloquejat però l'animació ja
|
||||||
|
# no és la d'atac. Placeholder: reaprofiten els frames de `stand_*`
|
||||||
|
# (la silueta no canvia respecte d'ells); quan arriben els frames
|
||||||
|
# reals només cal canviar els índexs ací, sense tocar el codi.
|
||||||
|
recover_down:
|
||||||
|
frames: [1]
|
||||||
|
|
||||||
|
recover_up:
|
||||||
|
frames: [5]
|
||||||
|
|
||||||
|
recover:
|
||||||
|
frames: [9]
|
||||||
|
|
||||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||||
|
|||||||
@@ -89,18 +89,81 @@ WhipAttack::WhipAttack(const fkyaml::node& yaml) {
|
|||||||
anim_ = anim_swing_;
|
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;
|
direction_ = dir;
|
||||||
range_px_ = range_px;
|
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);
|
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);
|
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(); }
|
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) {
|
void WhipAttack::setWorldContext(EntityManager* manager, const std::vector<Player*>* players) {
|
||||||
entity_manager_ = manager;
|
entity_manager_ = manager;
|
||||||
players_ = players;
|
players_ = players;
|
||||||
|
|||||||
@@ -18,16 +18,27 @@ class Player;
|
|||||||
// Atac de látigo: spawnejat pel jugador en entrar a `ATTACKING`. Viu la
|
// Atac de látigo: spawnejat pel jugador en entrar a `ATTACKING`. Viu la
|
||||||
// durada de la seua animació (`swing`) i s'autoelimina al final.
|
// durada de la seua animació (`swing`) i s'autoelimina al final.
|
||||||
//
|
//
|
||||||
// Sprites: 4 fitxers GIF distints, un per cardinal. La mida del frame
|
// Sprites: 4 fitxers GIF distints, un per cardinal (sense flips). La
|
||||||
// canvia entre direccions (E/W: 48×16, N/S: 16×48), cosa que el sistema
|
// mida del frame canvia entre direccions (E/W: 80×32, N/S: 16×96),
|
||||||
// d'animació actual no permet expressar amb una sola surface (una entitat
|
// cosa que el sistema d'animació actual no permet expressar amb una
|
||||||
// = una graella). Per això el YAML llista les 4 entrades sota `sprites:`
|
// sola surface. Per això el YAML llista les 4 entrades sota `sprites:`
|
||||||
// i `configure()` tria la que toca segons direcció.
|
// i `configure()` tria la que toca segons direcció.
|
||||||
//
|
//
|
||||||
// Hitbox de dany: el rectangle de dany és el `pixel_rect_` retallat al
|
// Dos rectangles distints, intencionadament diferenciats:
|
||||||
// primer bloquejador en la línia d'atac (mur extern del playfield o
|
// - `pixel_rect_` (heretat d'`Entity`) = `draw_rect`. Cobreix tot el
|
||||||
// sarcòfag). Aquesta lògica viu a la fase de damage application; en
|
// sprite del látigo: 16 px de wind-up darrere del jugador, 16 px
|
||||||
// aquest skeleton només viu la part visual i de cicle de vida.
|
// 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 {
|
class WhipAttack : public Attack {
|
||||||
public:
|
public:
|
||||||
// Direcció cardinal del swing. Coincideix valors amb
|
// Direcció cardinal del swing. Coincideix valors amb
|
||||||
@@ -53,12 +64,16 @@ class WhipAttack : public Attack {
|
|||||||
explicit WhipAttack(const fkyaml::node& yaml);
|
explicit WhipAttack(const fkyaml::node& yaml);
|
||||||
|
|
||||||
// Configura l'estat runtime de la instància. Cal cridar-lo abans del
|
// Configura l'estat runtime de la instància. Cal cridar-lo abans del
|
||||||
// primer `update`. Selecciona la surface i la mida del pixel_rect_
|
// primer `update`. A partir del `owner_box` (pixel_rect del jugador,
|
||||||
// segons direcció, i posa la cantonada superior-esquerra de la
|
// típicament 16×32) i la direcció d'atac, deriva:
|
||||||
// hitbox a (spawn_x, spawn_y). També calcula la `damage_rect_`
|
// - `pixel_rect_` (= draw_rect): cantonada i mida del sprite del
|
||||||
// retallada contra murs i sarcs (mai sortiria del playfield ni
|
// látigo segons la taula d'offsets (E: -16, 0, 80×32; etc.).
|
||||||
// travessaria un sarc).
|
// - `damage_rect_`: caixa de dany 48×16 (o 16×48) pegada al
|
||||||
void configure(Direction dir, float spawn_x, float spawn_y, float range_px);
|
// 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
|
// Injecta el context que necessita la fase de dany: punter a
|
||||||
// l'`EntityManager` (per iterar Hazards) i a la llista de jugadors
|
// l'`EntityManager` (per iterar Hazards) i a la llista de jugadors
|
||||||
@@ -90,6 +105,14 @@ class WhipAttack : public Attack {
|
|||||||
// consumeix la pròpia `applyDamage`.
|
// consumeix la pròpia `applyDamage`.
|
||||||
[[nodiscard]] auto getDamageRect() const -> SDL_FRect { return damage_rect_; }
|
[[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
|
// Clau al `Resource::Cache` per a la surface de la direcció actual
|
||||||
// (la que ha triat `configure()`). El caller (típicament `GameScene`)
|
// (la que ha triat `configure()`). El caller (típicament `GameScene`)
|
||||||
// resol la surface i la passa via `setSurface()`. Aquesta separació
|
// resol la surface i la passa via `setSurface()`. Aquesta separació
|
||||||
@@ -123,6 +146,12 @@ class WhipAttack : public Attack {
|
|||||||
float range_px_{0.0F};
|
float range_px_{0.0F};
|
||||||
SDL_FRect damage_rect_{0.0F, 0.0F, 0.0F, 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};
|
EntityManager* entity_manager_{nullptr};
|
||||||
const std::vector<Player*>* players_{nullptr};
|
const std::vector<Player*>* players_{nullptr};
|
||||||
|
|
||||||
|
|||||||
@@ -140,8 +140,10 @@ class Entity {
|
|||||||
|
|
||||||
// Y dels peus: la vora inferior de la caixa de col·lisió. La usarà el
|
// 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
|
// Z-order per ordenar entitats i jugador de menys a més profunditat
|
||||||
// (REND-01, quan s'implemente).
|
// (REND-01, quan s'implemente). Virtual perquè entitats efímeres
|
||||||
[[nodiscard]] auto getFeetY() const -> float;
|
// (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
|
// El loader li assigna una caixa explícita llegida del YAML; sense
|
||||||
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
|
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -57,15 +59,18 @@ struct WorldPos {
|
|||||||
// - `owner` : Player que ha llançat l'atac (no rep dany propi).
|
// - `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
|
// - `direction` : Player::Cardinal castat a int (1=N, 2=S, 3=E, 4=W) per
|
||||||
// evitar dependència cíclica al header.
|
// evitar dependència cíclica al header.
|
||||||
// - `spawn_x/y` : cantonada superior-esquerra de la hitbox màxima
|
// - `owner_box` : pixel_rect del jugador (típicament 16×32 a AEE). El
|
||||||
// (sense retallar) en píxels.
|
// WhipAttack en deriva tant la `draw_rect_` (sprite del
|
||||||
// - `range_px` : longitud de la hitbox abans de retallar contra murs i
|
// látigo, més gran que el jugador per cobrir wind-up i
|
||||||
// sarcòfags. Llegida del YAML del jugador.
|
// 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 {
|
struct WhipSpawnPayload {
|
||||||
Player* owner{nullptr};
|
Player* owner{nullptr};
|
||||||
int direction{0};
|
int direction{0};
|
||||||
float spawn_x{0.0F};
|
SDL_FRect owner_box{0.0F, 0.0F, 0.0F, 0.0F};
|
||||||
float spawn_y{0.0F};
|
|
||||||
float range_px{0.0F};
|
float range_px{0.0F};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -660,47 +660,36 @@ void Player::enterAttacking(Cardinal facing) {
|
|||||||
// p.ex. just després d'un canvi d'input).
|
// p.ex. just després d'un canvi d'input).
|
||||||
applyCardinal(facing);
|
applyCardinal(facing);
|
||||||
|
|
||||||
// Geometria de spawn de la hitbox: alineada al rectangle de
|
if (facing == Cardinal::NONE) { return; }
|
||||||
// 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
|
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
|
||||||
// `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;
|
|
||||||
int dir_int = 0;
|
int dir_int = 0;
|
||||||
switch (facing) {
|
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:
|
case Cardinal::NORTH:
|
||||||
spawn_x = BOX.x;
|
|
||||||
spawn_y = BOX.y - RANGE_PX;
|
|
||||||
dir_int = 1;
|
dir_int = 1;
|
||||||
break;
|
break;
|
||||||
case Cardinal::SOUTH:
|
case Cardinal::SOUTH:
|
||||||
spawn_x = BOX.x;
|
|
||||||
spawn_y = BOX.y + BOX.h;
|
|
||||||
dir_int = 2;
|
dir_int = 2;
|
||||||
break;
|
break;
|
||||||
|
case Cardinal::EAST:
|
||||||
|
dir_int = 3;
|
||||||
|
break;
|
||||||
|
case Cardinal::WEST:
|
||||||
|
dir_int = 4;
|
||||||
|
break;
|
||||||
case Cardinal::NONE:
|
case Cardinal::NONE:
|
||||||
return;
|
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) {
|
if (bus_ != nullptr) {
|
||||||
|
const float RANGE_PX = whip_config.range_tiles * static_cast<float>(Map::tile_size);
|
||||||
bus_->publish("whip_spawn", WhipSpawnPayload{
|
bus_->publish("whip_spawn", WhipSpawnPayload{
|
||||||
.owner = this,
|
.owner = this,
|
||||||
.direction = dir_int,
|
.direction = dir_int,
|
||||||
.spawn_x = spawn_x,
|
.owner_box = getPosition(),
|
||||||
.spawn_y = spawn_y,
|
|
||||||
.range_px = RANGE_PX,
|
.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
|
// ATTACKING té dues sub-fases: durant `swing_ms` el sprite és
|
||||||
// `attack_*` (animació del braç llançant el látigo); durant
|
// `attack_*` (animació del braç llançant el látigo); durant
|
||||||
// `cooldown_ms` posterior tornem al `stand_*` de la direcció. Fem
|
// `cooldown_ms` posterior canviem a `recover_*` per a frames
|
||||||
// la decisió comparant el remaining_ms_ amb el cooldown configurat.
|
// 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) {
|
if (state_ == State::ATTACKING) {
|
||||||
const bool IN_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
|
const bool IN_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
|
||||||
if (IN_SWING) {
|
if (IN_SWING) {
|
||||||
@@ -1158,13 +1151,13 @@ auto Player::animationName(bool moving) const -> const char* {
|
|||||||
}
|
}
|
||||||
switch (facing_) {
|
switch (facing_) {
|
||||||
case Facing::UP:
|
case Facing::UP:
|
||||||
return "stand_up";
|
return "recover_up";
|
||||||
case Facing::DOWN:
|
case Facing::DOWN:
|
||||||
return "stand_down";
|
return "recover_down";
|
||||||
case Facing::SIDE:
|
case Facing::SIDE:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return "stand";
|
return "recover";
|
||||||
}
|
}
|
||||||
switch (facing_) {
|
switch (facing_) {
|
||||||
case Facing::UP:
|
case Facing::UP:
|
||||||
|
|||||||
@@ -904,8 +904,7 @@ void GameScene::spawnWhip(const WhipSpawnPayload& payload) {
|
|||||||
whip->setOwner(payload.owner);
|
whip->setOwner(payload.owner);
|
||||||
whip->configure(
|
whip->configure(
|
||||||
static_cast<WhipAttack::Direction>(payload.direction),
|
static_cast<WhipAttack::Direction>(payload.direction),
|
||||||
payload.spawn_x,
|
payload.owner_box,
|
||||||
payload.spawn_y,
|
|
||||||
payload.range_px);
|
payload.range_px);
|
||||||
whip->setSurface(Resource::Cache::get().getSurface(whip->getSurfaceKey()));
|
whip->setSurface(Resource::Cache::get().getSurface(whip->getSurfaceKey()));
|
||||||
// Donem accés a Hazards i jugadors per a la fase de dany. Sense això
|
// Donem accés a Hazards i jugadors per a la fase de dany. Sense això
|
||||||
|
|||||||
@@ -32,100 +32,179 @@ namespace {
|
|||||||
|
|
||||||
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
|
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
|
||||||
|
|
||||||
|
// Caja de jugador "estàndard" AEE: 16×32 amb cantonada (x, y).
|
||||||
|
// El damage_rect del whip s'alinearà al feet box (els últims 16 px),
|
||||||
|
// i el draw_rect respectarà els offsets de la taula del disseny.
|
||||||
|
auto playerBox(float x, float y) -> SDL_FRect {
|
||||||
|
return SDL_FRect{.x = x, .y = y, .w = 16.0F, .h = 32.0F};
|
||||||
|
}
|
||||||
|
|
||||||
// Spawneja un WhipAttack ben configurat per al test, sense surface.
|
// Spawneja un WhipAttack ben configurat per al test, sense surface.
|
||||||
// Els tests no exerciten render, així que `surface_=nullptr` és OK.
|
// Els tests no exerciten render, així que `surface_=nullptr` és OK.
|
||||||
auto makeWhip(WhipAttack::Direction dir, float spawn_x, float spawn_y, float range_px)
|
auto makeWhip(WhipAttack::Direction dir, const SDL_FRect& owner_box, float range_px)
|
||||||
-> std::unique_ptr<WhipAttack> {
|
-> std::unique_ptr<WhipAttack> {
|
||||||
auto yaml = EntityLoader::loadTemplate("whip_attack");
|
auto yaml = EntityLoader::loadTemplate("whip_attack");
|
||||||
auto whip = std::make_unique<WhipAttack>(yaml);
|
auto whip = std::make_unique<WhipAttack>(yaml);
|
||||||
whip->configure(dir, spawn_x, spawn_y, range_px);
|
whip->configure(dir, owner_box, range_px);
|
||||||
return whip;
|
return whip;
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWhipPixelRectPerDirection() {
|
void testWhipDrawRectPerDirection() {
|
||||||
std::cout << "[W1] WhipAttack pixel_rect per direcció\n";
|
std::cout << "[W1] WhipAttack draw_rect per direcció\n";
|
||||||
Map::loadFrom("data/maps/pyramid.yaml");
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
|
||||||
// Tots els spawns ben dins del playfield perquè el retall no
|
// Owner ben dins del playfield perquè el draw_rect (no es retalla)
|
||||||
// afecte la mida visual del sprite (pixel_rect = mida del frame
|
// tinga la mida nominal completa.
|
||||||
// sencer).
|
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||||
const float SPAWN_X = static_cast<float>(Map::playfield_x + 64);
|
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 64);
|
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||||
|
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||||
|
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||||
|
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||||
|
const auto BOX = playerBox(OX, OY);
|
||||||
const float RANGE = 48.0F;
|
const float RANGE = 48.0F;
|
||||||
|
|
||||||
const auto WE = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, RANGE);
|
const auto WE = makeWhip(WhipAttack::Direction::EAST, BOX, RANGE);
|
||||||
REQUIRE(WE->getPixelRect().w == 48.0F);
|
REQUIRE(WE->getPixelRect().w == 80.0F);
|
||||||
REQUIRE(WE->getPixelRect().h == 16.0F);
|
REQUIRE(WE->getPixelRect().h == 32.0F);
|
||||||
|
|
||||||
const auto WW = makeWhip(WhipAttack::Direction::WEST, SPAWN_X, SPAWN_Y, RANGE);
|
const auto WW = makeWhip(WhipAttack::Direction::WEST, BOX, RANGE);
|
||||||
REQUIRE(WW->getPixelRect().w == 48.0F);
|
REQUIRE(WW->getPixelRect().w == 80.0F);
|
||||||
REQUIRE(WW->getPixelRect().h == 16.0F);
|
REQUIRE(WW->getPixelRect().h == 32.0F);
|
||||||
|
|
||||||
const auto WN = makeWhip(WhipAttack::Direction::NORTH, SPAWN_X, SPAWN_Y, RANGE);
|
const auto WN = makeWhip(WhipAttack::Direction::NORTH, BOX, RANGE);
|
||||||
REQUIRE(WN->getPixelRect().w == 16.0F);
|
REQUIRE(WN->getPixelRect().w == 16.0F);
|
||||||
REQUIRE(WN->getPixelRect().h == 48.0F);
|
REQUIRE(WN->getPixelRect().h == 96.0F);
|
||||||
|
|
||||||
const auto WS = makeWhip(WhipAttack::Direction::SOUTH, SPAWN_X, SPAWN_Y, RANGE);
|
const auto WS = makeWhip(WhipAttack::Direction::SOUTH, BOX, RANGE);
|
||||||
REQUIRE(WS->getPixelRect().w == 16.0F);
|
REQUIRE(WS->getPixelRect().w == 16.0F);
|
||||||
REQUIRE(WS->getPixelRect().h == 48.0F);
|
REQUIRE(WS->getPixelRect().h == 96.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testWhipDrawRectOffsets() {
|
||||||
|
std::cout << "[W7] offsets del draw_rect per direcció (taula del disseny)\n";
|
||||||
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
|
||||||
|
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||||
|
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||||
|
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||||
|
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||||
|
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||||
|
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||||
|
const auto BOX = playerBox(OX, OY);
|
||||||
|
const float RANGE = 48.0F;
|
||||||
|
|
||||||
|
// E: (player.x - 16, player.y), 80×32.
|
||||||
|
const auto WE = makeWhip(WhipAttack::Direction::EAST, BOX, RANGE);
|
||||||
|
REQUIRE(WE->getPixelRect().x == OX - 16.0F);
|
||||||
|
REQUIRE(WE->getPixelRect().y == OY);
|
||||||
|
|
||||||
|
// W: (player.x - 48, player.y), 80×32.
|
||||||
|
const auto WW = makeWhip(WhipAttack::Direction::WEST, BOX, RANGE);
|
||||||
|
REQUIRE(WW->getPixelRect().x == OX - 48.0F);
|
||||||
|
REQUIRE(WW->getPixelRect().y == OY);
|
||||||
|
|
||||||
|
// N: (player.x, player.y - 48), 16×96.
|
||||||
|
const auto WN = makeWhip(WhipAttack::Direction::NORTH, BOX, RANGE);
|
||||||
|
REQUIRE(WN->getPixelRect().x == OX);
|
||||||
|
REQUIRE(WN->getPixelRect().y == OY - 48.0F);
|
||||||
|
|
||||||
|
// S: (player.x, player.y - 16), 16×96.
|
||||||
|
const auto WS = makeWhip(WhipAttack::Direction::SOUTH, BOX, RANGE);
|
||||||
|
REQUIRE(WS->getPixelRect().x == OX);
|
||||||
|
REQUIRE(WS->getPixelRect().y == OY - 16.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testWhipFeetYDelegation() {
|
||||||
|
std::cout << "[W8] feet_y delegat a la base de l'owner_box\n";
|
||||||
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
|
||||||
|
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||||
|
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||||
|
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||||
|
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||||
|
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||||
|
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||||
|
const auto BOX = playerBox(OX, OY);
|
||||||
|
const float EXPECTED_FEET_Y = OY + 32.0F; // base inferior del 16×32
|
||||||
|
|
||||||
|
// Independent de la direcció: el feet_y és el del jugador.
|
||||||
|
for (auto dir : {WhipAttack::Direction::EAST,
|
||||||
|
WhipAttack::Direction::WEST,
|
||||||
|
WhipAttack::Direction::NORTH,
|
||||||
|
WhipAttack::Direction::SOUTH}) {
|
||||||
|
const auto W = makeWhip(dir, BOX, 48.0F);
|
||||||
|
REQUIRE(W->getFeetY() == EXPECTED_FEET_Y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWhipHitboxClippedByWall() {
|
void testWhipHitboxClippedByWall() {
|
||||||
std::cout << "[W2] hitbox retallat per la paret del playfield\n";
|
std::cout << "[W2] damage_rect retallat per la paret del playfield\n";
|
||||||
Map::loadFrom("data/maps/pyramid.yaml");
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
|
||||||
// Llancem un E des d'una posició a 10 px de la paret dreta del
|
// Llancem un E des d'una posició a 10 px de la paret dreta del
|
||||||
// playfield, amb un range de 48 px → la hitbox efectiva ha de
|
// playfield, amb un range de 48 px → la hitbox efectiva ha de
|
||||||
// quedar acotada a 10 px (o menys, segons strict overlap).
|
// quedar acotada a < 48 i no més enllà del mur.
|
||||||
const float RIGHT_WALL = static_cast<float>(Map::playfield_x + Map::playfield_w);
|
const float RIGHT_WALL = static_cast<float>(Map::playfield_x + Map::playfield_w);
|
||||||
const float SPAWN_X = RIGHT_WALL - 10.0F;
|
const float OX = RIGHT_WALL - 16.0F - 10.0F; // l'owner mateix dins
|
||||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 64);
|
const float OY = static_cast<float>(Map::playfield_y + 96);
|
||||||
|
const auto BOX = playerBox(OX, OY);
|
||||||
const float RANGE = 48.0F;
|
const float RANGE = 48.0F;
|
||||||
|
|
||||||
const auto W = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, RANGE);
|
const auto W = makeWhip(WhipAttack::Direction::EAST, BOX, RANGE);
|
||||||
const auto DMG = W->getDamageRect();
|
const auto DMG = W->getDamageRect();
|
||||||
// Damage rect estrictament menor que el rang complet.
|
|
||||||
REQUIRE(DMG.w < RANGE);
|
REQUIRE(DMG.w < RANGE);
|
||||||
// I no més enllà de la paret.
|
|
||||||
REQUIRE(DMG.x + DMG.w <= RIGHT_WALL);
|
REQUIRE(DMG.x + DMG.w <= RIGHT_WALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWhipHitboxFreeAtFullRange() {
|
void testWhipHitboxFreeAtFullRange() {
|
||||||
std::cout << "[W3] hitbox sense bloquejador → longitud completa\n";
|
std::cout << "[W3] damage_rect sense bloquejador → longitud completa\n";
|
||||||
Map::loadFrom("data/maps/pyramid.yaml");
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
|
||||||
// Spawn molt a l'interior i range curt: cap bloquejador a
|
// Owner ben dins del playfield, range curt → 16 px lliures fins
|
||||||
// l'abast → la longitud efectiva ha de ser RANGE sencer.
|
// a topar amb res. La longitud efectiva = RANGE.
|
||||||
const float SPAWN_X = static_cast<float>(Map::playfield_x + 32);
|
const float OX = static_cast<float>(Map::playfield_x + 32);
|
||||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 96);
|
const float OY = static_cast<float>(Map::playfield_y + 96);
|
||||||
|
const auto BOX = playerBox(OX, OY);
|
||||||
const float RANGE = 16.0F;
|
const float RANGE = 16.0F;
|
||||||
|
|
||||||
const auto W = makeWhip(WhipAttack::Direction::SOUTH, SPAWN_X, SPAWN_Y, RANGE);
|
const auto W = makeWhip(WhipAttack::Direction::SOUTH, BOX, RANGE);
|
||||||
const auto DMG = W->getDamageRect();
|
const auto DMG = W->getDamageRect();
|
||||||
REQUIRE(DMG.h == RANGE);
|
REQUIRE(DMG.h == RANGE);
|
||||||
REQUIRE(DMG.w == 16.0F);
|
REQUIRE(DMG.w == 16.0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
void testWhipHitsMummyInRange() {
|
void testWhipHitsMummyInRange() {
|
||||||
std::cout << "[W4] mòmia en rang entra a HURT i es marca a alreadyHit\n";
|
std::cout << "[W4] mòmia en rang entra a HURT\n";
|
||||||
Map::loadFrom("data/maps/pyramid.yaml");
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
|
||||||
EntityManager manager;
|
EntityManager manager;
|
||||||
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
||||||
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
||||||
mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||||
// Posicionem la mòmia 24 px a la dreta del whip → dins de range_px.
|
|
||||||
const float SPAWN_X = static_cast<float>(Map::playfield_x + 64);
|
// Owner i mòmia al mateix corredor; mòmia 24 px a la dreta del
|
||||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 64);
|
// jugador → dins del damage_rect (48 px de rang).
|
||||||
mummy_owned->setPixelRect({.x = SPAWN_X + 24.0F, .y = SPAWN_Y - 12.0F, .w = 12.0F, .h = 24.0F});
|
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||||
|
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||||
|
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||||
|
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||||
|
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||||
|
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||||
|
const auto BOX = playerBox(OX, OY);
|
||||||
|
// Posicionem la mòmia perquè la seua collision box solape amb
|
||||||
|
// la zona on cau el damage_rect (els últims 16 px verticals
|
||||||
|
// del player, és a dir y ∈ [OY+16, OY+32]).
|
||||||
|
// Volem que la collision (offset_y=12, height=12) solape amb el
|
||||||
|
// damage_rect del whip (a y ∈ [OY+16, OY+32]). Posant pixel_rect.y
|
||||||
|
// = OY, la collision queda a [OY+12, OY+24], 8 px d'overlap.
|
||||||
|
mummy_owned->setPixelRect({.x = OX + 24.0F, .y = OY, .w = 12.0F, .h = 24.0F});
|
||||||
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
|
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
|
||||||
|
|
||||||
auto whip = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, 48.0F);
|
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 48.0F);
|
||||||
whip->setWorldContext(&manager, nullptr);
|
whip->setWorldContext(&manager, nullptr);
|
||||||
|
|
||||||
// Avancem l'animació fins arribar a un frame actiu (el YAML
|
// 60 ms → frame 1 (active). active_frames: [1, 2].
|
||||||
// declara `active_frames: [1, 2]`; cada frame dura 50 ms).
|
|
||||||
// Amb 1 sol update de 0.06 s passem el frame 0 → 1.
|
|
||||||
whip->update(0.06F);
|
whip->update(0.06F);
|
||||||
|
|
||||||
REQUIRE(mummy_raw->getState() == Mummy::State::HURT);
|
REQUIRE(mummy_raw->getState() == Mummy::State::HURT);
|
||||||
@@ -139,13 +218,19 @@ namespace {
|
|||||||
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
||||||
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
||||||
mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||||
const float SPAWN_X = static_cast<float>(Map::playfield_x + 32);
|
|
||||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 64);
|
// Mateixa col·locació corredor 2-3 que W4: el damage_rect cap
|
||||||
// Mòmia 96 px a la dreta → fora del rang de 32.
|
// íntegre al sarc-buit; només l'eix x distingirà "dins" de "fora".
|
||||||
mummy_owned->setPixelRect({.x = SPAWN_X + 96.0F, .y = SPAWN_Y - 12.0F, .w = 12.0F, .h = 24.0F});
|
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||||
|
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||||
|
const auto BOX = playerBox(OX, OY);
|
||||||
|
// Mòmia 96 px a la dreta de l'owner → fora d'un range de 32.
|
||||||
|
// Vertically aligned with the damage band perquè el test només
|
||||||
|
// depenga de l'eix horitzontal (no d'una falla d'alineament Y).
|
||||||
|
mummy_owned->setPixelRect({.x = OX + 96.0F, .y = OY, .w = 12.0F, .h = 24.0F});
|
||||||
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
|
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
|
||||||
|
|
||||||
auto whip = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, 32.0F);
|
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 32.0F);
|
||||||
whip->setWorldContext(&manager, nullptr);
|
whip->setWorldContext(&manager, nullptr);
|
||||||
whip->update(0.06F);
|
whip->update(0.06F);
|
||||||
|
|
||||||
@@ -160,30 +245,37 @@ namespace {
|
|||||||
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
||||||
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
||||||
mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||||
const float SPAWN_X = static_cast<float>(Map::playfield_x + 64);
|
|
||||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 64);
|
// Posicionem el jugador amb la collision (bottom 16×16) al
|
||||||
mummy_owned->setPixelRect({.x = SPAWN_X + 24.0F, .y = SPAWN_Y - 12.0F, .w = 12.0F, .h = 24.0F});
|
// corredor entre files 2 i 3 de sarcs (y∈[144,160]) i a la
|
||||||
|
// columna entre cols 0 i 1 (x∈[88,104]) — així el damage_rect
|
||||||
|
// cap al sarc-buit corredor i no es retalla per sarcs.
|
||||||
|
const float OX = static_cast<float>(Map::playfield_x + 64);
|
||||||
|
const float OY = static_cast<float>(Map::playfield_y + 80);
|
||||||
|
const auto BOX = playerBox(OX, OY);
|
||||||
|
// Volem que la collision (offset_y=12, height=12) solape amb el
|
||||||
|
// damage_rect del whip (a y ∈ [OY+16, OY+32]). Posant pixel_rect.y
|
||||||
|
// = OY, la collision queda a [OY+12, OY+24], 8 px d'overlap.
|
||||||
|
mummy_owned->setPixelRect({.x = OX + 24.0F, .y = OY, .w = 12.0F, .h = 24.0F});
|
||||||
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
|
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
|
||||||
|
|
||||||
auto whip = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, 48.0F);
|
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 48.0F);
|
||||||
whip->setWorldContext(&manager, nullptr);
|
whip->setWorldContext(&manager, nullptr);
|
||||||
|
|
||||||
// Primera passada: mòmia a HURT (el whip entra al frame 1
|
// Primera passada (frame 1, actiu) → mòmia a HURT.
|
||||||
// = actiu en el primer step de 0.06 s).
|
|
||||||
whip->update(0.06F);
|
whip->update(0.06F);
|
||||||
REQUIRE(mummy_raw->getState() == Mummy::State::HURT);
|
REQUIRE(mummy_raw->getState() == Mummy::State::HURT);
|
||||||
const float HURT_INITIAL = mummy_raw->getHurtRemainingS();
|
const float HURT_INITIAL = mummy_raw->getHurtRemainingS();
|
||||||
|
|
||||||
// Avancem la mòmia (descompta el seu timer) i tornem a tickar
|
// Avancem la mòmia (descompta el seu timer) i tornem a tickar el
|
||||||
// el whip mentre l'animació segueix activa: si `already_hit_`
|
// whip mentre l'animació segueix activa: si `already_hit_`
|
||||||
// funciona, el segon hit es bloqueja i el timer de la mòmia
|
// funciona, el segon hit es bloqueja i el timer de la mòmia NO
|
||||||
// NO es reseteja al valor inicial.
|
// es reseteja al valor inicial.
|
||||||
mummy_raw->update(0.05F);
|
mummy_raw->update(0.05F);
|
||||||
const float HURT_AFTER_MUMMY_TICK = mummy_raw->getHurtRemainingS();
|
const float HURT_AFTER_MUMMY_TICK = mummy_raw->getHurtRemainingS();
|
||||||
REQUIRE(HURT_AFTER_MUMMY_TICK < HURT_INITIAL);
|
REQUIRE(HURT_AFTER_MUMMY_TICK < HURT_INITIAL);
|
||||||
|
|
||||||
whip->update(0.05F); // frame 2, encara actiu
|
whip->update(0.05F); // frame 2, encara actiu
|
||||||
// Si re-hit, enterHurt restauraria el timer; alreadyHit ho impedeix.
|
|
||||||
REQUIRE(mummy_raw->getHurtRemainingS() == HURT_AFTER_MUMMY_TICK);
|
REQUIRE(mummy_raw->getHurtRemainingS() == HURT_AFTER_MUMMY_TICK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +284,9 @@ namespace {
|
|||||||
auto main() -> int {
|
auto main() -> int {
|
||||||
std::cout << "== smoke tests: WhipAttack ==\n";
|
std::cout << "== smoke tests: WhipAttack ==\n";
|
||||||
|
|
||||||
testWhipPixelRectPerDirection();
|
testWhipDrawRectPerDirection();
|
||||||
|
testWhipDrawRectOffsets();
|
||||||
|
testWhipFeetYDelegation();
|
||||||
testWhipHitboxClippedByWall();
|
testWhipHitboxClippedByWall();
|
||||||
testWhipHitboxFreeAtFullRange();
|
testWhipHitboxFreeAtFullRange();
|
||||||
testWhipHitsMummyInRange();
|
testWhipHitsMummyInRange();
|
||||||
|
|||||||