import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import test from "node:test"; import { auditWowOriginalManifest, normalizeWowEffectPath, packageWowOriginalEffects } from "./package-wow-original.mjs"; import { decodeWotlkM2Animation, decodeWotlkM2Mesh } from "./wotlk-m2-mesh.mjs"; test("normalizes client model paths without allowing traversal", () => { assert.equal(normalizeWowEffectPath("Spells\\Fireball.mdx", ".m2"), "spells/fireball.m2"); for (const name of ["../secret.m2", "spells/../secret.m2", "/spells/test.m2", "C:/secret.m2"]) { assert.throws(() => normalizeWowEffectPath(name), /Invalid WoW source path/); } }); test("all ten WoW classes package every referenced visual phase", () => { const manifest = JSON.parse(fs.readFileSync(new URL("../../public/assets/game/combat-effects/manifest.json", import.meta.url))); const coverage = manifest.coverage.originalVisuals; assert.equal(coverage.scope, "wow335-all-classes"); assert.equal(Object.keys(coverage.classes).length, 10); assert.equal(coverage.packagedModels, coverage.models); assert.deepEqual(coverage.failures, []); for (const [classId, entry] of Object.entries(coverage.classes)) { assert.ok(entry.withOriginalVisuals > 0, classId); assert.equal(entry.packagedPhaseAssets, entry.referencedPhaseAssets, classId); } assert.deepEqual(auditWowOriginalManifest(manifest).missing, []); }); test("reports unavailable source models without falsely marking them ready", () => { const original = { entries: [ { key: "wow335:133", source: "wow335", spellId: 133, phases: { impact: [{ model: "Spells/Missing.mdx", effectGraphUrl: "/old.json" }] } }, { key: "runewaker:1", source: "runewaker", spellId: 1 }, ] }; const result = packageWowOriginalEffects(original, { source: { read: () => null }, catalog: { classes: { mage: [{ ranks: [{ spellId: 133 }] }] } }, }); assert.equal(result.report.failures.length, 1); assert.equal(result.manifest.entries[0].phases.impact[0].effectGraphUrl, undefined); assert.deepEqual(result.manifest.entries[1], original.entries[1]); assert.equal(original.entries[0].phases.impact[0].effectGraphUrl, "/old.json"); }); const sourceRoot = path.resolve("../LadiksMPQEditor/common-2.mpq/spells"); test("decodes Death Knight dual textures with a single layer transparency track", { skip: !fs.existsSync(path.join(sourceRoot, "deathknight_bloodstrike.m2")), }, () => { const m2 = fs.readFileSync(path.join(sourceRoot, "deathknight_bloodstrike.m2")); const skin = fs.readFileSync(path.join(sourceRoot, "deathknight_bloodstrike00.skin")); const mesh = decodeWotlkM2Mesh(m2, skin); assert.equal(mesh.skin.textureUnits[0].textureIndices.length, 2); assert.equal(mesh.skin.textureUnits[0].textureWeightIndices.length, 1); const animation = decodeWotlkM2Animation(m2); assert.ok(animation.bones.length > 0); assert.ok(animation.bones.flatMap((b) => b.rotation.sequences.flatMap((s) => s.values)) .every((q) => q.every(Number.isFinite))); });