Files
healer-man/scripts/audit-combat-creature-clips.mjs
T
2026-08-14 15:56:39 -04:00

64 lines
2.6 KiB
JavaScript

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.`);