Files
healer-man/scripts/runewaker-pipeline/finalize-player-avatar-manifest.mjs
T
2026-08-14 15:56:39 -04:00

146 lines
4.8 KiB
JavaScript

import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const pipelineDirectory = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(pipelineDirectory, "../..");
const packRoot = path.join(projectRoot, "public/assets/characters/runewaker-v1");
const importFile = path.join(packRoot, "import-manifest.json");
const outputFile = path.join(packRoot, "manifest.json");
const imported = JSON.parse(await readFile(importFile, "utf8"));
if (imported.status !== "green" || imported.assets?.length !== 5) {
throw new Error("RuneWaker player avatar import must contain all five validated native rigs.");
}
const identityByAsset = {
"rom-human-male": ["rom-human", "male", 1, 0],
"rom-human-female": ["rom-human", "female", 1, 1],
"rom-elf-male": ["rom-elf", "male", 2, 0],
"rom-elf-female": ["rom-elf", "female", 2, 1],
"rom-dwarf-male": ["rom-dwarf", "male", 3, 0],
};
const counts = { skinColor: 8, face: 8, hairStyle: 8, hairColor: 8, feature: 8 };
const indices = Array.from({ length: 8 }, (_, index) => index);
const triplet = ["", "", ""];
const blankByColor = indices.map(() => triplet);
const skinOptions = indices.map((index) => ({
dbcColor: index,
base: "",
underwear: triplet,
}));
const faceOptions = indices.map((index) => ({ dbcVariation: index, bySkin: blankByColor }));
const hairOptions = indices.map((index) => ({
dbcVariation: index,
geoset: 1,
showScalp: true,
byColor: blankByColor,
}));
const featureOptions = indices.map((index) => ({
dbcVariation: index,
geosets: [],
byColor: blankByColor,
}));
const entries = Object.fromEntries(imported.assets.map((asset) => {
const identity = identityByAsset[asset.id];
if (!identity) throw new Error(`Unexpected RuneWaker player avatar ${asset.id}.`);
const [raceId, gender, dbcRaceId, dbcSexId] = identity;
const requiredClips = [
"Stand - stand_idle01",
"Walk - walk_forward",
"Run - run_forward",
"Attack - unarmed_attack01",
"Cast - cast01",
"Wound - hurt",
"Death - death",
];
for (const clip of requiredClips) {
if (!asset.animations.includes(clip)) {
throw new Error(`${asset.id} is missing required native animation ${clip}.`);
}
}
return [`${raceId}:${gender}`, {
raceId,
gender,
dbcRaceId,
dbcSexId,
sourceModel: asset.sourceModel,
model: asset.url,
bytes: asset.size,
scale: 1,
groundOffset: asset.groundOffset,
rotationY: asset.rotationY,
materialTokens: { body: "", hair: "" },
animations: {
idle: "Stand - stand_idle01",
walk: "Walk - walk_forward",
run: "Run - run_forward",
attack: "Attack - unarmed_attack01",
cast: "Cast - cast01",
wound: "Wound - hurt",
death: "Death - death",
},
counts,
dbcIndices: {
skinColor: indices,
face: indices,
hairStyle: indices,
hairColor: indices,
feature: indices,
},
skinOptions,
faceOptions,
hairOptions,
featureOptions,
rendererStrategy: {
kind: "runewaker-modular",
nativeBodyRig: true,
sharedSkeleton: true,
starterPaperdoll: true,
cachedDimensions: ["skinColor", "face", "hairStyle", "hairColor", "feature", "gear"],
sourceParts: ["head", "hair", "torso", "hand", "leg", "foot"],
skinPalette: ["#f2d1bc", "#deb39a", "#c89373", "#aa7354", "#82533f", "#eed4c7", "#d6a887", "#9d654c"],
hairPalette: ["#211a17", "#493124", "#73482a", "#9c713f", "#b9a27a", "#7a2630", "#c8c3b4", "#342c43"],
},
provenance: {
glbSha256: asset.checksum,
sourceModelSha256: asset.sourceModelSha256,
assemblySha256: asset.paperdollAssemblySha256,
nativeClipCount: asset.animations.length,
},
}];
}));
const sourceMaterial = Object.values(entries)
.map((entry) => `${entry.provenance.sourceModelSha256}:${entry.provenance.glbSha256}`)
.join("|");
const manifest = {
version: 1,
sourceBuild: `runewaker-v1-${createHash("sha256").update(sourceMaterial).digest("hex").slice(0, 16)}`,
publicRoot: "/assets/characters/runewaker-v1",
textureLayout: {
width: 1,
height: 1,
regions: {
base: [0, 0, 1, 1],
faceUpper: [0, 0, 1, 1],
faceLower: [0, 0, 1, 1],
torsoUpper: [0, 0, 1, 1],
pelvisUpper: [0, 0, 1, 1],
},
},
entries,
provenance: {
generator: "scripts/runewaker-pipeline/finalize-player-avatar-manifest.mjs",
importManifest: "public/assets/characters/runewaker-v1/import-manifest.json",
runtimeRuneWakerDependency: false,
renderer: "native shared-skeleton modular paperdoll",
},
};
await writeFile(outputFile, JSON.stringify(manifest, null, 2) + "\n", "utf8");
console.log(`Generated ${Object.keys(entries).length} RuneWaker playable avatar rigs.`);