forked from phenom/healer-man
265 lines
11 KiB
JavaScript
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 });
|
|
}
|
|
});
|