Files
healer-man/scripts/combat-effects/package-priest-original.test.mjs
T
2026-09-01 16:31:26 -04:00

265 lines
11 KiB
JavaScript

import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
auditPriestOriginalManifest,
packagePriestOriginalEffects,
priestSpellIdsFromCatalog,
} from "./package-priest-original.mjs";
test("collects every ranked Priest spell id", () => {
const ids = priestSpellIdsFromCatalog({
classes: {
priest: [
{ ranks: [{ spellId: 2050 }, { spellId: 2052 }] },
{ ranks: [{ spellId: 2061 }] },
],
},
});
assert.equal(ids.has(2050), true);
assert.equal(ids.has(2052), true);
assert.equal(ids.has(2061), true);
assert.equal(ids.has(585), true);
});
test("packages source-backed Priest graphs and attaches only ready assets", () => {
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "healer-man-priest-effects-"));
try {
const modelRoot = path.join(temporaryRoot, "models");
const textureRoot = path.join(temporaryRoot, "textures");
const outputRoot = path.join(temporaryRoot, "public-pack");
fs.mkdirSync(path.join(modelRoot, "Spells"), { recursive: true });
fs.mkdirSync(path.join(textureRoot, "Spells"), { recursive: true });
const modelBuffer = Buffer.alloc(64);
modelBuffer.writeUInt32LE(0, 60);
fs.writeFileSync(path.join(modelRoot, "Spells", "Fixture.m2"), modelBuffer);
fs.writeFileSync(path.join(textureRoot, "Spells", "Fixture.BLP"), Buffer.from("source-texture"));
const sourceManifest = {
schemaVersion: 1,
coverage: { requested: 1, authentic: 1, fallback: 0 },
entries: [{
key: "wow335:2061",
source: "wow335",
spellId: 2061,
phases: { impact: [{ model: "Spells/Fixture.mdx" }] },
}],
};
const decoded = {
durationMs: 900,
animations: [{ index: 0, id: 0, variationIndex: 0, durationMs: 900, moveSpeed: 0, flags: 0 }],
textures: [{ type: 0, flags: 0, filename: "Spells/Fixture.BLP" }],
ribbons: [],
particles: [{
id: 1,
flags: 0,
position: [0, 0, 0],
parentBone: 0,
textureIndex: 0,
geometryModel: "",
recursionModel: "",
blendMode: 4,
emitterType: 1,
particleColorIndex: 0,
particleType: 0,
headOrTail: 0,
textureTileRotation: 0,
textureRows: 1,
textureColumns: 1,
lifespanVariation: 0,
emissionRateVariation: 0,
scaleVariation: [0, 0],
tailLength: 0,
tracks: Object.fromEntries([
"emissionSpeed", "speedVariation", "verticalRange", "horizontalRange", "gravity",
"lifespan", "emissionRate", "emissionAreaLength", "emissionAreaWidth", "zSource", "enabled",
].map((name) => [name, {
interpolation: name === "emissionRate" ? 1 : 0,
globalSequence: -1,
firstSequence: { samples: name === "enabled" ? [{ timeMs: 0, value: 1 }] : [] },
}]).concat([
["headCell", { samples: [] }],
["tailCell", { samples: [] }],
])),
samples: {
emissionRate: [{ timeMs: 0, value: 12 }],
lifespan: [{ timeMs: 0, value: 0.8 }],
emissionSpeed: [{ timeMs: 0, value: 1 }],
color: [{ normalizedTime: 0, value: [255, 255, 255] }],
alpha: [{ normalizedTime: 0, value: 32767 }],
scale: [{ normalizedTime: 0, value: [0.2, 0.2] }],
},
}],
};
const { manifest, report } = packagePriestOriginalEffects(sourceManifest, {
catalog: { classes: { priest: [{ ranks: [{ spellId: 2061 }] }] } },
modelRoots: [modelRoot],
textureRoots: [textureRoot],
outputRoot,
publicPrefix: "/fixture/priest",
decodeM2: () => decoded,
convertTexture: (source, output) => fs.copyFileSync(source, output),
});
const asset = manifest.entries[0].phases.impact[0];
assert.equal(asset.effectKind, "wow-m2-particles");
assert.match(asset.effectGraphUrl, /^\/fixture\/priest\/graphs\/fixture-/);
assert.equal(report.packagedModels, 1);
assert.equal(report.readyParticleEmitters, 1);
const graphFile = path.join(outputRoot, "graphs", path.posix.basename(asset.effectGraphUrl));
const graph = JSON.parse(fs.readFileSync(graphFile, "utf8"));
assert.equal(graph.sourceModel, "Spells/Fixture.mdx");
assert.equal(graph.textures[0].url.startsWith("/fixture/priest/textures/"), true);
assert.equal(graph.interpolation, undefined);
assert.equal(graph.particles[0].interpolation.emissionRate, 1);
assert.deepEqual(graph.particles[0].samples.enabled, [{ timeMs: 0, value: 1 }]);
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
});
test("packages the Power Word: Shield aura as a three-layer static mesh graph", () => {
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "healer-man-priest-shield-"));
try {
const modelRoot = path.join(temporaryRoot, "models");
const textureRoot = path.join(temporaryRoot, "textures");
const outputRoot = path.join(temporaryRoot, "public-pack");
fs.mkdirSync(path.join(modelRoot, "Spells"), { recursive: true });
fs.mkdirSync(path.join(textureRoot, "Spells"), { recursive: true });
fs.writeFileSync(
path.join(modelRoot, "Spells", "HolyDivineShield_State_Base.m2"), Buffer.from("fixture-m2"),
);
fs.writeFileSync(
path.join(modelRoot, "Spells", "HolyDivineShield_State_Base00.skin"), Buffer.from("fixture-skin"),
);
for (const texture of ["ShieldA.BLP", "ShieldB.BLP", "ShieldC.BLP"]) {
fs.writeFileSync(path.join(textureRoot, "Spells", texture), Buffer.from(`source-${texture}`));
}
const scalarTrack = (values, interpolation = 1) => ({
interpolation,
globalSequence: -1,
sequences: [{ timestamps: values.map((_, index) => index * 500), values }],
});
const colorTrack = (color) => ({
interpolation: 0,
globalSequence: -1,
sequences: [{ timestamps: [0], values: [color] }],
});
const sourceManifest = {
schemaVersion: 1,
coverage: { requested: 1, authentic: 1, fallback: 0 },
entries: [{
key: "wow335:17",
source: "wow335",
spellId: 17,
phases: {
"aura-start": [{ model: "Spells/HolyDivineShield_State_Base.mdx" }],
},
}],
};
const decodedMesh = {
globalSequences: [],
animations: [
{ index: 0, id: 0, variationIndex: 0, durationMs: 700, moveSpeed: 0, flags: 33 },
{ index: 1, id: 158, variationIndex: 0, durationMs: 633, moveSpeed: 0, flags: 32 },
{ index: 2, id: 159, variationIndex: 0, durationMs: 1_100, moveSpeed: 0, flags: 33 },
],
bounds: { minimum: [-1, -1, 0], maximum: [1, 1, 0], radius: 1.5 },
vertices: [
{ position: [0, 0, 0], normal: [1, 0, 0], uv: [0, 0] },
{ position: [0, 1, 0], normal: [1, 0, 0], uv: [1, 0] },
{ position: [0, 0, 1], normal: [1, 0, 0], uv: [0, 1] },
],
indices: [0, 1, 2, 0, 1, 2, 0, 1, 2],
colors: [0, 1, 2].map((index) => ({
color: colorTrack([1, 0.9 - index * 0.1, 0.3]),
alpha: scalarTrack([32_767], 0),
})),
textures: ["ShieldA.BLP", "ShieldB.BLP", "ShieldC.BLP"].map((filename) => ({
type: 0, flags: 0, filename: `Spells/${filename}`,
})),
textureWeights: [0, 1, 2].map(() => scalarTrack([0, 6_553])),
materials: [0, 1, 2].map(() => ({ flags: 19, blendMode: 4 })),
skin: {
textureUnits: [0, 1, 2].map((index) => ({
submeshIndex: index,
priorityPlane: 0,
shaderId: 0,
colorIndex: index,
materialIndex: index,
materialLayer: 0,
textureIndices: [index],
textureCoordinateIndices: [0],
textureWeightIndices: [index],
textureTransformIndices: [-1],
drawRange: { indexStart: index * 3, indexCount: 3 },
})),
},
};
const { manifest, report } = packagePriestOriginalEffects(sourceManifest, {
catalog: { classes: { priest: [{ ranks: [{ spellId: 17 }] }] } },
modelRoots: [modelRoot],
textureRoots: [textureRoot],
outputRoot,
publicPrefix: "/fixture/priest",
decodeM2: () => { throw new Error("particle decoder should not handle the static shield"); },
decodeMesh: () => decodedMesh,
convertTexture: (source, output) => fs.copyFileSync(source, output),
});
const asset = manifest.entries[0].phases["aura-start"][0];
assert.equal(asset.effectKind, "wow-m2-static-mesh");
assert.equal(asset.scale, 0.7);
assert.match(asset.effectGraphUrl, /^\/fixture\/priest\/graphs\/holydivineshield-state-base-/);
assert.equal(report.readyStaticMeshes, 1);
assert.equal(report.readyParticleEmitters, 0);
const graphFile = path.join(outputRoot, "graphs", path.posix.basename(asset.effectGraphUrl));
const graph = JSON.parse(fs.readFileSync(graphFile, "utf8"));
assert.equal(graph.kind, "wow-m2-static-mesh");
assert.deepEqual(graph.geometry, {
positions: [0, 0, 0, 0, 1, 0, 0, 0, 1],
normals: [1, 0, 0, 1, 0, 0, 1, 0, 0],
uvs: [0, 0, 1, 0, 0, 1],
indices: [0, 1, 2, 0, 1, 2, 0, 1, 2],
});
assert.equal(graph.layers.length, 3);
assert.deepEqual(graph.animations.map(({ id, durationMs }) => ({ id, durationMs })), [
{ id: 0, durationMs: 700 },
{ id: 158, durationMs: 633 },
{ id: 159, durationMs: 1_100 },
]);
assert.equal(graph.colors.length, 3);
assert.equal(graph.textureWeights.length, 3);
assert.deepEqual(graph.textureWeights[0], {
interpolation: 1,
globalSequence: -1,
sequences: [{ timestamps: [0, 500], values: [0, 6_553] }],
});
assert.deepEqual(graph.layers.map((layer) => ({
textureIndex: layer.textureIndex,
blendMode: layer.blendMode,
drawRange: [layer.indexStart, layer.indexCount],
})), [
{ textureIndex: 0, blendMode: 4, drawRange: [0, 3] },
{ textureIndex: 1, blendMode: 4, drawRange: [3, 3] },
{ textureIndex: 2, blendMode: 4, drawRange: [6, 3] },
]);
assert.ok(graph.textures.every((texture) => texture.url.startsWith("/fixture/priest/textures/")));
const audit = auditPriestOriginalManifest(manifest, new Set([17]), new Set([17]));
assert.deepEqual(audit.missingPowerWordShieldAuraStarts, []);
const missingAudit = auditPriestOriginalManifest({
...manifest,
entries: manifest.entries.map((entry) => ({
...entry,
phases: { "aura-start": entry.phases["aura-start"].map((item) => ({ ...item, effectKind: "wrong" })) },
})),
}, new Set([17]), new Set([17]));
assert.deepEqual(missingAudit.missingPowerWordShieldAuraStarts, [17]);
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
});