test(whip): smokes de hitbox, dany a mòmia i alreadyHit; tests de Mummy HURT
This commit is contained in:
1 parent
df60709e6d
commit
121558b1fd
4 files changed
+326
No files matched your search
@@ -361,6 +361,29 @@ if(BUILD_TESTS)
|
|||||||
COMMAND test_entities_smoke
|
COMMAND test_entities_smoke
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
|
|
||||||
|
# Smoke del WhipAttack. Inclou tot l'stack d'entitats + whip_attack.cpp.
|
||||||
|
# `whip_attack_player_stubs.cpp` proveeix símbols mínims de Player per
|
||||||
|
# satisfer el linker sense arrossegar audio/gamepad/screen — els tests
|
||||||
|
# del whip s'enfoquen a hitbox + Mummy, no a friendly fire.
|
||||||
|
add_executable(test_whip_attack
|
||||||
|
tests/test_whip_attack.cpp
|
||||||
|
tests/whip_attack_player_stubs.cpp
|
||||||
|
source/core/resources/resource_helper.cpp
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
|
source/game/map/map.cpp
|
||||||
|
${ENTITY_SOURCES_BASE}
|
||||||
|
source/game/entities/concrete/whip_attack.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_whip_attack PRIVATE
|
||||||
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
"${CMAKE_BINARY_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(test_whip_attack PRIVATE SDL3::SDL3)
|
||||||
|
target_compile_options(test_whip_attack PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
add_test(NAME whip_attack
|
||||||
|
COMMAND test_whip_attack
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||||
|
|
||||||
# Smoke del flux d'escenes i del parser de debug.yaml. Stand-alone:
|
# Smoke del flux d'escenes i del parser de debug.yaml. Stand-alone:
|
||||||
# només enllaça els helpers purs (debug_config, placeholder_input,
|
# només enllaça els helpers purs (debug_config, placeholder_input,
|
||||||
# key_config) perquè la integració completa dels placeholders arrossegaria
|
# key_config) perquè la integració completa dels placeholders arrossegaria
|
||||||
|
|||||||
@@ -431,6 +431,77 @@ namespace {
|
|||||||
REQUIRE(mummy.getDirX() <= 0.0F);
|
REQUIRE(mummy.getDirX() <= 0.0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testMummyHurtState() {
|
||||||
|
std::cout << "[T15] mummy enterHurt → fugida + recuperació\n";
|
||||||
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
|
||||||
|
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
|
||||||
|
Mummy mummy(tmpl);
|
||||||
|
mummy.setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||||
|
// Mateixa col·locació que testMummyStep: corredor exterior
|
||||||
|
// superior, lliure de sarcs, prou marge per fugir cap a la dreta.
|
||||||
|
mummy.setPixelRect({.x = 60.0F, .y = 40.0F, .w = 12.0F, .h = 24.0F});
|
||||||
|
|
||||||
|
// Direcció preferida: dreta. Mòmia ha d'entrar en HURT i avançar
|
||||||
|
// a la dreta (la velocitat de hurt és més alta que la base).
|
||||||
|
mummy.enterHurt(Mummy::Direction::RIGHT);
|
||||||
|
REQUIRE(mummy.getState() == Mummy::State::HURT);
|
||||||
|
REQUIRE(mummy.getDirX() > 0.0F);
|
||||||
|
|
||||||
|
const auto BEFORE = mummy.getPixelRect();
|
||||||
|
mummy.update(0.05F); // 50 ms: hauria d'avançar
|
||||||
|
const auto AFTER = mummy.getPixelRect();
|
||||||
|
REQUIRE(AFTER.x > BEFORE.x);
|
||||||
|
REQUIRE(mummy.getState() == Mummy::State::HURT);
|
||||||
|
|
||||||
|
// Avancem fins esgotar el timer (`hurt.duration_ms` = 600 ms al
|
||||||
|
// YAML → 0.6 s; sumem 1 s amb un sol step gran per garantir-ho).
|
||||||
|
mummy.update(1.0F);
|
||||||
|
REQUIRE(mummy.getState() == Mummy::State::ROAMING);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testMummyHurtAgainstWall() {
|
||||||
|
std::cout << "[T16] mummy HURT contra mur → surt abans del timer\n";
|
||||||
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
|
||||||
|
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
|
||||||
|
Mummy mummy(tmpl);
|
||||||
|
mummy.setCollisionBox({.offset_x = 0, .offset_y = 12, .width = 12, .height = 12});
|
||||||
|
|
||||||
|
// Posicionem la mòmia tocant la paret dreta del playfield i la
|
||||||
|
// fem fugir cap a la dreta: el primer pas hauria de topar amb
|
||||||
|
// la paret i fer-la sortir de HURT.
|
||||||
|
const float RIGHT_WALL = static_cast<float>(Map::playfield_x + Map::playfield_w);
|
||||||
|
mummy.setPixelRect({.x = RIGHT_WALL - 12.0F, .y = 40.0F, .w = 12.0F, .h = 24.0F});
|
||||||
|
|
||||||
|
// La direcció preferida (RIGHT) està bloquejada al test
|
||||||
|
// d'`is_open`; entrarem amb una perpendicular o quieta.
|
||||||
|
// L'objecte de la prova és que `state_` torne a ROAMING quan
|
||||||
|
// el moviment topa: ho forcem fent que `enterHurt` triï igual
|
||||||
|
// RIGHT i topem en cridar `update`. Per aconseguir-ho posicionem
|
||||||
|
// la mòmia al límit i forcem la direcció via `setDirection`
|
||||||
|
// ABANS d'`enterHurt`. Però com `enterHurt` reescriu la
|
||||||
|
// direcció, el camí més directe és:
|
||||||
|
// 1) `enterHurt(RIGHT)` → si is_open(RIGHT) val false, intenta
|
||||||
|
// perpendiculars; si totes fallen, queda quieta.
|
||||||
|
// 2) Si està quieta, el HURT tampoc avança i només mor pel
|
||||||
|
// timer. Aquesta variant del test NO comprova la sortida
|
||||||
|
// per col·lisió; només verifica que entrar a HURT a la
|
||||||
|
// paret no peta.
|
||||||
|
mummy.enterHurt(Mummy::Direction::RIGHT);
|
||||||
|
REQUIRE(mummy.getState() == Mummy::State::HURT);
|
||||||
|
// Update llarg → expira el timer i torna a ROAMING.
|
||||||
|
mummy.update(1.0F);
|
||||||
|
REQUIRE(mummy.getState() == Mummy::State::ROAMING);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testMummyWhippableFlag() {
|
||||||
|
std::cout << "[T17] mummy YAML llig `whippable` correctament\n";
|
||||||
|
fkyaml::node tmpl = EntityLoader::loadTemplate("mummy");
|
||||||
|
Mummy mummy(tmpl);
|
||||||
|
REQUIRE(mummy.isWhippable());
|
||||||
|
}
|
||||||
|
|
||||||
void testTorchAnimation() {
|
void testTorchAnimation() {
|
||||||
std::cout << "[T10] torch animation wired from yaml\n";
|
std::cout << "[T10] torch animation wired from yaml\n";
|
||||||
// Regressió: el torch.yaml declarava `frames: [0]` i el render no
|
// Regressió: el torch.yaml declarava `frames: [0]` i el render no
|
||||||
@@ -510,6 +581,9 @@ auto main() -> int {
|
|||||||
testSarcophagusYaml();
|
testSarcophagusYaml();
|
||||||
testCollisionBox();
|
testCollisionBox();
|
||||||
testMummyStep();
|
testMummyStep();
|
||||||
|
testMummyHurtState();
|
||||||
|
testMummyHurtAgainstWall();
|
||||||
|
testMummyWhippableFlag();
|
||||||
testTorchAnimation();
|
testTorchAnimation();
|
||||||
testExitedAggregator();
|
testExitedAggregator();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Stubs de Player per al binari `test_whip_attack`.
|
||||||
|
//
|
||||||
|
// El path de friendly fire del `WhipAttack` referencia
|
||||||
|
// `Player::enterHurt(Cardinal)` i `Player::getBaseBox() const`. Linkar
|
||||||
|
// `player.cpp` arrossegaria tot l'stack del jugador (audio, gamepad,
|
||||||
|
// input, screen, resource_cache…), una explosió de dependències per a
|
||||||
|
// uns tests que no exerciten aqueix camí. Aquests stubs proporcionen
|
||||||
|
// els símbols com a no-op perquè el test del whip pugui linkar i centrar
|
||||||
|
// la cobertura en hitbox + Mummy.
|
||||||
|
//
|
||||||
|
// Si algun dia volem cobrir friendly fire, caldrà o bé un binari de test
|
||||||
|
// amb tot l'stack o invertir la dependència via callable injectable
|
||||||
|
// (refactor de `WhipAttack::applyDamage`).
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
|
|
||||||
|
void Player::enterHurt(Cardinal /*preferred*/) {
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Player::getBaseBox() const -> SDL_FRect {
|
||||||
|
return SDL_FRect{0.0F, 0.0F, 0.0F, 0.0F};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user