48 lines
2.5 KiB
JavaScript
48 lines
2.5 KiB
JavaScript
import { mkdir, stat } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { NodeIO } from "@gltf-transform/core";
|
|
import { EXTMeshoptCompression, EXTTextureWebP, KHRMeshQuantization } from "@gltf-transform/extensions";
|
|
import { MeshoptDecoder, MeshoptEncoder } from "meshoptimizer";
|
|
|
|
const rootDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const playerDirectory = path.join(rootDirectory, "game_assets/models/claudecraft/chars/players");
|
|
const outputDirectory = path.join(rootDirectory, ".tmp/pruned-party-models");
|
|
const writeInPlace = process.argv.includes("--write");
|
|
|
|
const REQUIRED_CLIPS = {
|
|
druid: ["Death_A", "Hit_A", "Idle", "Running_A", "Walking_A", "Spellcasting", "2H_Melee_Attack_Chop"],
|
|
knight: ["Death_A", "Hit_A", "Idle", "Running_A", "Walking_A", "Spellcasting", "1H_Melee_Attack_Chop"],
|
|
ranger: ["Death_A", "Hit_A", "Idle", "Running_A", "Walking_A", "Spellcasting", "2H_Ranged_Shoot"],
|
|
mage: ["Death_A", "Hit_A", "Idle", "Running_A", "Walking_A", "Spellcasting", "Spellcast_Shoot"],
|
|
rogue: ["Death_A", "Hit_A", "Idle", "Running_A", "Walking_A", "Spellcasting", "Dualwield_Melee_Attack_Chop"],
|
|
};
|
|
|
|
await MeshoptEncoder.ready;
|
|
const io = new NodeIO()
|
|
.registerExtensions([EXTMeshoptCompression, EXTTextureWebP, KHRMeshQuantization])
|
|
.registerDependencies({
|
|
"meshopt.decoder": MeshoptDecoder,
|
|
"meshopt.encoder": MeshoptEncoder,
|
|
});
|
|
|
|
if (!writeInPlace) await mkdir(outputDirectory, { recursive: true });
|
|
|
|
for (const [modelName, requiredClips] of Object.entries(REQUIRED_CLIPS)) {
|
|
const inputPath = path.join(playerDirectory, `${modelName}.glb`);
|
|
const document = await io.read(inputPath);
|
|
const animations = document.getRoot().listAnimations();
|
|
const available = new Set(animations.map((animation) => animation.getName()));
|
|
const missing = requiredClips.filter((clip) => !available.has(clip));
|
|
if (missing.length) throw new Error(`${modelName}.glb is missing required clips: ${missing.join(", ")}`);
|
|
|
|
for (const animation of animations) {
|
|
if (!requiredClips.includes(animation.getName())) animation.dispose();
|
|
}
|
|
const outputPath = writeInPlace ? inputPath : path.join(outputDirectory, `${modelName}.glb`);
|
|
await io.write(outputPath, document);
|
|
const sourceSize = (await stat(inputPath)).size;
|
|
const outputSize = (await stat(outputPath)).size;
|
|
console.log(`${path.basename(outputPath)}: ${sourceSize} → ${outputSize} bytes (${requiredClips.length} clips)`);
|
|
}
|