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 }); } });