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