// 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 #include #include #include #include #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 { auto yaml = EntityLoader::loadTemplate("whip_attack"); auto whip = std::make_unique(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(Map::playfield_x + 64); const float SPAWN_Y = static_cast(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(Map::playfield_x + Map::playfield_w); const float SPAWN_X = RIGHT_WALL - 10.0F; const float SPAWN_Y = static_cast(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(Map::playfield_x + 32); const float SPAWN_Y = static_cast(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_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(Map::playfield_x + 64); const float SPAWN_Y = static_cast(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(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_yaml); mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12}); const float SPAWN_X = static_cast(Map::playfield_x + 32); const float SPAWN_Y = static_cast(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(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_yaml); mummy_owned->setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12}); const float SPAWN_X = static_cast(Map::playfield_x + 64); const float SPAWN_Y = static_cast(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(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; }