import { readdir, readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; globalThis.self = globalThis; globalThis.createImageBitmap = async () => ({ width: 1, height: 1, close() {} }); globalThis.ProgressEvent ??= class ProgressEvent { constructor(type, init = {}) { this.type = type; Object.assign(this, init); } }; const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); const assetDirectory = path.resolve( scriptDirectory, "../public/assets/creatures/wailing-caverns", ); const files = (await readdir(assetDirectory)) .filter((file) => file.endsWith("-animated.glb")) .sort(); if (files.length !== 16) throw new Error(`Expected 16 animated creature GLBs; found ${files.length}.`); let nativeCombatModels = 0; for (const file of files) { const bytes = await readFile(path.join(assetDirectory, file)); const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); const gltf = await new Promise((resolve, reject) => { new GLTFLoader().parse(arrayBuffer, "", resolve, reject); }); const attack = gltf.animations.find((clip) => /^Attack(?!.*Ready)/i.test(clip.name)); const death = gltf.animations.find((clip) => /^(Death|Drown)\b/i.test(clip.name)); const run = gltf.animations.find((clip) => /^Run\b/i.test(clip.name)); const wound = gltf.animations.find((clip) => /^(?:StandWound|CombatWound|CombatCritical)\b/i.test(clip.name)); if (!attack || !death || !run || !wound) { throw new Error(`${file}: missing a native Run, Attack, Wound, or Death clip.`); } nativeCombatModels += 1; for (const clip of gltf.animations) { // WoW includes metadata-only static states such as Lasher Submerged. // Preserve those names; animated clips must still have positive duration. if (clip.tracks.length > 0 && (!Number.isFinite(clip.duration) || clip.duration < 0)) { throw new Error(`${file}: invalid duration on animation "${clip.name}".`); } for (const track of clip.tracks) { if (!Array.from(track.times).every(Number.isFinite) || !Array.from(track.values).every(Number.isFinite)) { throw new Error(`${file}: non-finite values in "${clip.name}".`); } } } console.log(`${file}: ${gltf.animations.map((clip) => clip.name).join(", ")}`); } if (nativeCombatModels !== files.length) { throw new Error(`Expected native combat clips on all ${files.length} models; found ${nativeCombatModels}.`); } console.log(`Validated ${files.length} GLBs with complete native locomotion/combat libraries.`);