Kelsidavis-WoWee/tests/test_m2_structs.cpp
Paul e58f9b4b40 feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.

Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.

Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.

Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.

Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.

Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00

164 lines
4.5 KiB
C++

// M2 struct layout and field tests (header-only, no loader source)
#include <catch_amalgamated.hpp>
#include "pipeline/m2_loader.hpp"
#include <cstring>
using namespace wowee::pipeline;
TEST_CASE("M2Sequence fields are default-initialized", "[m2]") {
M2Sequence seq{};
REQUIRE(seq.id == 0);
REQUIRE(seq.duration == 0);
REQUIRE(seq.movingSpeed == 0.0f);
REQUIRE(seq.flags == 0);
REQUIRE(seq.blendTime == 0);
REQUIRE(seq.boundRadius == 0.0f);
}
TEST_CASE("M2AnimationTrack hasData", "[m2]") {
M2AnimationTrack track;
REQUIRE_FALSE(track.hasData());
track.sequences.push_back({});
REQUIRE(track.hasData());
}
TEST_CASE("M2AnimationTrack default interpolation", "[m2]") {
M2AnimationTrack track;
REQUIRE(track.interpolationType == 0);
REQUIRE(track.globalSequence == -1);
}
TEST_CASE("M2Bone parent defaults to root", "[m2]") {
M2Bone bone{};
bone.parentBone = -1;
REQUIRE(bone.parentBone == -1);
REQUIRE(bone.keyBoneId == 0);
}
TEST_CASE("M2Vertex layout", "[m2]") {
M2Vertex vert{};
vert.position = glm::vec3(1.0f, 2.0f, 3.0f);
vert.boneWeights[0] = 255;
vert.boneWeights[1] = 0;
vert.boneWeights[2] = 0;
vert.boneWeights[3] = 0;
vert.boneIndices[0] = 5;
vert.normal = glm::vec3(0.0f, 1.0f, 0.0f);
vert.texCoords[0] = glm::vec2(0.5f, 0.5f);
REQUIRE(vert.position.x == 1.0f);
REQUIRE(vert.boneWeights[0] == 255);
REQUIRE(vert.boneIndices[0] == 5);
REQUIRE(vert.normal.y == 1.0f);
REQUIRE(vert.texCoords[0].x == 0.5f);
}
TEST_CASE("M2Texture stores filename", "[m2]") {
M2Texture tex{};
tex.type = 1;
tex.filename = "Creature\\Hogger\\Hogger.blp";
REQUIRE(tex.filename == "Creature\\Hogger\\Hogger.blp");
}
TEST_CASE("M2Batch submesh fields", "[m2]") {
M2Batch batch{};
batch.skinSectionIndex = 3;
batch.textureCount = 2;
batch.indexStart = 100;
batch.indexCount = 300;
batch.vertexStart = 0;
batch.vertexCount = 150;
batch.submeshId = 0;
batch.submeshLevel = 0;
REQUIRE(batch.skinSectionIndex == 3);
REQUIRE(batch.textureCount == 2);
REQUIRE(batch.indexCount == 300);
REQUIRE(batch.vertexCount == 150);
}
TEST_CASE("M2Material blend modes", "[m2]") {
M2Material mat{};
mat.flags = 0;
mat.blendMode = 2; // Alpha blend
REQUIRE(mat.blendMode == 2);
mat.blendMode = 0; // Opaque
REQUIRE(mat.blendMode == 0);
}
TEST_CASE("M2Model isValid", "[m2]") {
M2Model model{};
REQUIRE_FALSE(model.isValid()); // no vertices or indices
model.vertices.push_back({});
REQUIRE_FALSE(model.isValid()); // vertices but no indices
model.indices.push_back(0);
REQUIRE(model.isValid()); // both present
}
TEST_CASE("M2Model bounding box", "[m2]") {
M2Model model{};
model.boundMin = glm::vec3(-1.0f, -2.0f, -3.0f);
model.boundMax = glm::vec3(1.0f, 2.0f, 3.0f);
model.boundRadius = 5.0f;
glm::vec3 center = (model.boundMin + model.boundMax) * 0.5f;
REQUIRE(center.x == Catch::Approx(0.0f));
REQUIRE(center.y == Catch::Approx(0.0f));
REQUIRE(center.z == Catch::Approx(0.0f));
}
TEST_CASE("M2ParticleEmitter defaults", "[m2]") {
M2ParticleEmitter emitter{};
emitter.textureRows = 1;
emitter.textureCols = 1;
emitter.enabled = true;
REQUIRE(emitter.textureRows == 1);
REQUIRE(emitter.textureCols == 1);
REQUIRE(emitter.enabled);
}
TEST_CASE("M2RibbonEmitter defaults", "[m2]") {
M2RibbonEmitter ribbon{};
REQUIRE(ribbon.edgesPerSecond == Catch::Approx(15.0f));
REQUIRE(ribbon.edgeLifetime == Catch::Approx(0.5f));
REQUIRE(ribbon.gravity == Catch::Approx(0.0f));
}
TEST_CASE("M2Attachment position", "[m2]") {
M2Attachment att{};
att.id = 1; // Right hand
att.bone = 42;
att.position = glm::vec3(0.1f, 0.2f, 0.3f);
REQUIRE(att.id == 1);
REQUIRE(att.bone == 42);
REQUIRE(att.position.z == Catch::Approx(0.3f));
}
TEST_CASE("M2Model collections", "[m2]") {
M2Model model{};
// Bones
model.bones.push_back({});
model.bones[0].parentBone = -1;
model.bones[0].pivot = glm::vec3(0, 0, 0);
// Sequences
model.sequences.push_back({});
model.sequences[0].id = 0; // Stand
model.sequences[0].duration = 1000;
// Textures
model.textures.push_back({});
model.textures[0].type = 0;
model.textures[0].filename = "test.blp";
REQUIRE(model.bones.size() == 1);
REQUIRE(model.sequences.size() == 1);
REQUIRE(model.textures.size() == 1);
REQUIRE(model.sequences[0].duration == 1000);
}