test(whip): smokes de hitbox, dany a mòmia i alreadyHit; tests de Mummy HURT

This commit is contained in:
JailDesigner committed 2026-04-28 16:20:22 +02:00
1 parent df60709e6d
commit 121558b1fd
4 files changed
+326

No files matched your search

+208
View File
@@ -0,0 +1,208 @@
// 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__)
// 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)
-> 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);
return whip;
}
void testWhipPixelRectPerDirection() {
std::cout << "[W1] WhipAttack pixel_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);
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 WW = makeWhip(WhipAttack::Direction::WEST, SPAWN_X, SPAWN_Y, RANGE);
REQUIRE(WW->getPixelRect().w == 48.0F);
REQUIRE(WW->getPixelRect().h == 16.0F);
const auto WN = makeWhip(WhipAttack::Direction::NORTH, SPAWN_X, SPAWN_Y, RANGE);
REQUIRE(WN->getPixelRect().w == 16.0F);
REQUIRE(WN->getPixelRect().h == 48.0F);
const auto WS = makeWhip(WhipAttack::Direction::SOUTH, SPAWN_X, SPAWN_Y, RANGE);
REQUIRE(WS->getPixelRect().w == 16.0F);
REQUIRE(WS->getPixelRect().h == 48.0F);
}
void testWhipHitboxClippedByWall() {
std::cout << "[W2] hitbox 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).
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 RANGE = 48.0F;
const auto W = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, 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";
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);
const float RANGE = 16.0F;
const auto W = makeWhip(WhipAttack::Direction::SOUTH, SPAWN_X, SPAWN_Y, 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";
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});
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
auto whip = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, 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.
whip->update(0.06F);
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/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});
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});
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
auto whip = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, 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/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});
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});
auto* mummy_raw = static_cast<Mummy*>(manager.add(std::move(mummy_owned)));
auto whip = makeWhip(WhipAttack::Direction::EAST, SPAWN_X, SPAWN_Y, 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).
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.
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);
}
} // namespace
auto main() -> int {
std::cout << "== smoke tests: WhipAttack ==\n";
testWhipPixelRectPerDirection();
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;
}