Files
aee-arcade/tests/test_whip_attack.cpp
T

321 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Smoke tests del WhipAttack: hitbox per direcció, retall contra murs i
// sarcs, aplicació de dany a mòmies whippable, alreadyHit. Friendly
// fire (Player↔Player) NO es cobreix aquí — Player viu enllaçat amb
// stubs sense estat real (veure whip_attack_player_stubs.cpp).
//
// Convencions iguals a test_entities_smoke: framework-less, REQUIRE
// macro, comptador `g_failures`. Execució: `./test_whip_attack` des de
// build/, working_dir = arrel del repo.
#include <SDL3/SDL.h>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <vector>
#include "game/entities/concrete/mummy.hpp"
#include "game/entities/concrete/whip_attack.hpp"
#include "game/entities/entity_loader.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/map/map.hpp"
namespace {
int g_failures = 0;
void check(bool cond, const char* msg, const char* file, int line) {
if (!cond) {
std::cerr << " FAIL: " << msg << " (" << file << ":" << line << ")\n";
++g_failures;
}
}
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
// Treu la mòmia de l'estat SPAWN inicial (Mummy::Mummy ara hi entra
// amb l'animació `raise` activa). Els tests del whip necessiten que la
// mòmia estigui ACTIVE per reaccionar al fueteig — sense això,
// `enterHurt` torna early per `isSpawning()`. Tickem amb dt gran fins
// sortir de SPAWNING (mateix path real, només accelerat).
void exitMummySpawn(Mummy& mummy) {
for (int i = 0; i < 32 && mummy.isSpawning(); ++i) {
mummy.update(0.5F);
}
}
// 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, 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, owner_box, range_px);
return whip;
}
void testWhipDrawRectPerDirection() {
std::cout << "[W1] WhipAttack draw_rect per direcció\n";
Map::loadFrom("data/maps/p1/r1.yaml");
// 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, BOX, RANGE);
REQUIRE(WE->getPixelRect().w == 80.0F);
REQUIRE(WE->getPixelRect().h == 32.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, BOX, RANGE);
REQUIRE(WN->getPixelRect().w == 16.0F);
REQUIRE(WN->getPixelRect().h == 96.0F);
const auto WS = makeWhip(WhipAttack::Direction::SOUTH, BOX, RANGE);
REQUIRE(WS->getPixelRect().w == 16.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/p1/r1.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/p1/r1.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] damage_rect retallat per la paret del playfield\n";
Map::loadFrom("data/maps/p1/r1.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 < 48 i no més enllà del mur.
const float RIGHT_WALL = static_cast<float>(Map::playfield_x + Map::playfield_w);
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, BOX, RANGE);
const auto DMG = W->getDamageRect();
REQUIRE(DMG.w < RANGE);
REQUIRE(DMG.x + DMG.w <= RIGHT_WALL);
}
void testWhipHitboxFreeAtFullRange() {
std::cout << "[W3] damage_rect sense bloquejador → longitud completa\n";
Map::loadFrom("data/maps/p1/r1.yaml");
// 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, 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\n";
Map::loadFrom("data/maps/p1/r1.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});
// 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)));
exitMummySpawn(*mummy_raw);
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 48.0F);
whip->setWorldContext(&manager, nullptr);
// frame_ms=80 al YAML → frame 0 viu [0, 80] ms, frame 1 [80, 160] ms.
// active_frames=[1, 2], així que cal arribar com a mínim a 80 ms
// perquè `isFrameActive()` retorne true i `applyDamage()` corra.
whip->update(0.09F);
REQUIRE(mummy_raw->getState() == Mummy::State::HURT);
}
void testWhipNoHitOutOfRange() {
std::cout << "[W5] mòmia fora del rang no rep dany\n";
Map::loadFrom("data/maps/p1/r1.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});
// 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, BOX, 32.0F);
whip->setWorldContext(&manager, nullptr);
whip->update(0.06F);
REQUIRE(mummy_raw->getState() == Mummy::State::ROAMING);
}
void testWhipSingleHitPerSwing() {
std::cout << "[W6] una mòmia rep un sol impacte per swing\n";
Map::loadFrom("data/maps/p1/r1.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 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)));
exitMummySpawn(*mummy_raw);
auto whip = makeWhip(WhipAttack::Direction::EAST, BOX, 48.0F);
whip->setWorldContext(&manager, nullptr);
// frame_ms=80 → frame 0 [0, 80] ms; cal ≥ 80 ms per entrar al
// frame 1 (primer dels active_frames=[1, 2]).
whip->update(0.09F);
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.
mummy_raw->update(0.05F);
const float HURT_AFTER_MUMMY_TICK = mummy_raw->getHurtRemainingS();
REQUIRE(HURT_AFTER_MUMMY_TICK < HURT_INITIAL);
// Segon tick del whip: acumulat 0.09 + 0.05 = 0.14 s, encara
// dins del frame 1 ([80, 160] ms). already_hit_ ha de bloquejar.
whip->update(0.05F);
REQUIRE(mummy_raw->getHurtRemainingS() == HURT_AFTER_MUMMY_TICK);
}
} // namespace
auto main() -> int {
std::cout << "== smoke tests: WhipAttack ==\n";
testWhipDrawRectPerDirection();
testWhipDrawRectOffsets();
testWhipFeetYDelegation();
testWhipHitboxClippedByWall();
testWhipHitboxFreeAtFullRange();
testWhipHitsMummyInRange();
testWhipNoHitOutOfRange();
testWhipSingleHitPerSwing();
if (g_failures == 0) {
std::cout << "ALL OK\n";
return EXIT_SUCCESS;
}
std::cerr << g_failures << " failures\n";
return EXIT_FAILURE;
}