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:
1 parent
121558b1fd
commit
8f66961c7b
15 files changed
+353
-127
No files matched your search
+152
-58
@@ -32,100 +32,179 @@ namespace {
|
||||
|
||||
#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.
|
||||
// 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> {
|
||||
auto yaml = EntityLoader::loadTemplate("whip_attack");
|
||||
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;
|
||||
}
|
||||
|
||||
void testWhipPixelRectPerDirection() {
|
||||
std::cout << "[W1] WhipAttack pixel_rect per direcció\n";
|
||||
void testWhipDrawRectPerDirection() {
|
||||
std::cout << "[W1] WhipAttack draw_rect per direcció\n";
|
||||
Map::loadFrom("data/maps/pyramid.yaml");
|
||||
|
||||
// Tots els spawns ben dins del playfield perquè el retall no
|
||||
// afecte la mida visual del sprite (pixel_rect = mida del frame
|
||||
// sencer).
|
||||
const float SPAWN_X = static_cast<float>(Map::playfield_x + 64);
|
||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 64);
|
||||
// Owner ben dins del playfield perquè el draw_rect (no es retalla)
|
||||
// tinga la mida nominal completa.
|
||||
// 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;
|
||||
|
||||
const auto WE = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, RANGE);
|
||||
REQUIRE(WE->getPixelRect().w == 48.0F);
|
||||
REQUIRE(WE->getPixelRect().h == 16.0F);
|
||||
const auto WE = makeWhip(WhipAttack::Direction::EAST, BOX, RANGE);
|
||||
REQUIRE(WE->getPixelRect().w == 80.0F);
|
||||
REQUIRE(WE->getPixelRect().h == 32.0F);
|
||||
|
||||
const auto WW = makeWhip(WhipAttack::Direction::WEST, SPAWN_X, SPAWN_Y, RANGE);
|
||||
REQUIRE(WW->getPixelRect().w == 48.0F);
|
||||
REQUIRE(WW->getPixelRect().h == 16.0F);
|
||||
const auto WW = makeWhip(WhipAttack::Direction::WEST, BOX, RANGE);
|
||||
REQUIRE(WW->getPixelRect().w == 80.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().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().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() {
|
||||
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");
|
||||
|
||||
// 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
|
||||
// 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 SPAWN_X = RIGHT_WALL - 10.0F;
|
||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 64);
|
||||
const float OX = RIGHT_WALL - 16.0F - 10.0F; // l'owner mateix dins
|
||||
const float OY = static_cast<float>(Map::playfield_y + 96);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
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();
|
||||
// Damage rect estrictament menor que el rang complet.
|
||||
REQUIRE(DMG.w < RANGE);
|
||||
// I no més enllà de la paret.
|
||||
REQUIRE(DMG.x + DMG.w <= RIGHT_WALL);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// Spawn molt a l'interior i range curt: cap bloquejador a
|
||||
// l'abast → la longitud efectiva ha de ser RANGE sencer.
|
||||
const float SPAWN_X = static_cast<float>(Map::playfield_x + 32);
|
||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 96);
|
||||
// Owner ben dins del playfield, range curt → 16 px lliures fins
|
||||
// a topar amb res. La longitud efectiva = RANGE.
|
||||
const float OX = static_cast<float>(Map::playfield_x + 32);
|
||||
const float OY = static_cast<float>(Map::playfield_y + 96);
|
||||
const auto BOX = playerBox(OX, OY);
|
||||
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();
|
||||
REQUIRE(DMG.h == RANGE);
|
||||
REQUIRE(DMG.w == 16.0F);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
EntityManager manager;
|
||||
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
||||
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
||||
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);
|
||||
const float SPAWN_Y = static_cast<float>(Map::playfield_y + 64);
|
||||
mummy_owned->setPixelRect({.x = SPAWN_X + 24.0F, .y = SPAWN_Y - 12.0F, .w = 12.0F, .h = 24.0F});
|
||||
|
||||
// Owner i mòmia al mateix corredor; mòmia 24 px a la dreta del
|
||||
// jugador → dins del damage_rect (48 px de rang).
|
||||
// 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 whip = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, 48.0F);
|
||||
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 48.0F);
|
||||
whip->setWorldContext(&manager, nullptr);
|
||||
|
||||
// Avancem l'animació fins arribar a un frame actiu (el YAML
|
||||
// declara `active_frames: [1, 2]`; cada frame dura 50 ms).
|
||||
// Amb 1 sol update de 0.06 s passem el frame 0 → 1.
|
||||
// 60 ms → frame 1 (active). active_frames: [1, 2].
|
||||
whip->update(0.06F);
|
||||
|
||||
REQUIRE(mummy_raw->getState() == Mummy::State::HURT);
|
||||
@@ -139,13 +218,19 @@ namespace {
|
||||
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
||||
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
||||
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);
|
||||
// Mòmia 96 px a la dreta → fora del rang de 32.
|
||||
mummy_owned->setPixelRect({.x = SPAWN_X + 96.0F, .y = SPAWN_Y - 12.0F, .w = 12.0F, .h = 24.0F});
|
||||
|
||||
// Mateixa col·locació corredor 2-3 que W4: el damage_rect cap
|
||||
// íntegre al sarc-buit; només l'eix x distingirà "dins" de "fora".
|
||||
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 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->update(0.06F);
|
||||
|
||||
@@ -160,30 +245,37 @@ namespace {
|
||||
auto mummy_yaml = EntityLoader::loadTemplate("mummy");
|
||||
auto mummy_owned = std::make_unique<Mummy>(mummy_yaml);
|
||||
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);
|
||||
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);
|
||||
// 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 whip = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, 48.0F);
|
||||
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 48.0F);
|
||||
whip->setWorldContext(&manager, nullptr);
|
||||
|
||||
// Primera passada: mòmia a HURT (el whip entra al frame 1
|
||||
// = actiu en el primer step de 0.06 s).
|
||||
// Primera passada (frame 1, actiu) → mòmia a HURT.
|
||||
whip->update(0.06F);
|
||||
REQUIRE(mummy_raw->getState() == Mummy::State::HURT);
|
||||
const float HURT_INITIAL = mummy_raw->getHurtRemainingS();
|
||||
|
||||
// Avancem la mòmia (descompta el seu timer) i tornem a tickar
|
||||
// el whip mentre l'animació segueix activa: si `already_hit_`
|
||||
// funciona, el segon hit es bloqueja i el timer de la mòmia
|
||||
// NO es reseteja al valor inicial.
|
||||
// Avancem la mòmia (descompta el seu timer) i tornem a tickar el
|
||||
// whip mentre l'animació segueix activa: si `already_hit_`
|
||||
// funciona, el segon hit es bloqueja i el timer de la mòmia NO
|
||||
// es reseteja al valor inicial.
|
||||
mummy_raw->update(0.05F);
|
||||
const float HURT_AFTER_MUMMY_TICK = mummy_raw->getHurtRemainingS();
|
||||
REQUIRE(HURT_AFTER_MUMMY_TICK < HURT_INITIAL);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -192,7 +284,9 @@ namespace {
|
||||
auto main() -> int {
|
||||
std::cout << "== smoke tests: WhipAttack ==\n";
|
||||
|
||||
testWhipPixelRectPerDirection();
|
||||
testWhipDrawRectPerDirection();
|
||||
testWhipDrawRectOffsets();
|
||||
testWhipFeetYDelegation();
|
||||
testWhipHitboxClippedByWall();
|
||||
testWhipHitboxFreeAtFullRange();
|
||||
testWhipHitsMummyInRange();
|
||||
|
||||
Reference in new issue
Block a user