146 lines
4.8 KiB
JavaScript
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.`);
|