309 lines
13 KiB
JavaScript
309 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const scriptRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
const projectRoot = path.resolve(scriptRoot, "..", "..");
|
|
const batchRoot = path.join(projectRoot, "artifacts", "animation-audit", "batches", "batch-c");
|
|
const [summary, coverage, inventory] = await Promise.all([
|
|
readJson(path.join(batchRoot, "summary.json")),
|
|
readJson(path.join(projectRoot, "artifacts", "animation-audit", "runtime-clip-coverage.json")),
|
|
readJson(path.join(projectRoot, "artifacts", "animation-audit", "inventory.json")),
|
|
]);
|
|
|
|
const dungeons = [
|
|
"zurhidon-stronghold",
|
|
"treasure-trove",
|
|
"sardo-castle",
|
|
"kalins-shrine",
|
|
"forsaken-abbey",
|
|
];
|
|
|
|
const command = "node scripts/runewaker-pipeline/audit-animated-poses.mjs"
|
|
+ " --input-list artifacts/animation-audit/batches/batch-c-inputs.json"
|
|
+ " --output-dir artifacts/animation-audit/batches/batch-c"
|
|
+ " --sample-mode all --render-mode families --render-limit 24"
|
|
+ " --contact-sheets --jobs 2";
|
|
const warningCommand = "node scripts/runewaker-pipeline/audit-animated-poses.mjs"
|
|
+ " --input <each of the six warning GLBs>"
|
|
+ " --output-dir artifacts/animation-audit/batches/batch-c/warning-review"
|
|
+ " --sample-mode all --render-mode flagged --render-limit 24"
|
|
+ " --contact-sheets --jobs 2";
|
|
|
|
const warningDispositionsByVariant = new Map(Object.entries({
|
|
"kalins-shrine/god-of-art-retainer.glb": {
|
|
disposition: "expected multipart costume and effect topology",
|
|
visualVerdict: "pass",
|
|
note: "All 59 numeric flags report 27-29 tiny mesh components. Family sheets and 24 supplemental flagged renders keep the body, costume, weapon, locomotion, combat, wound, and death silhouettes coherent.",
|
|
},
|
|
"kalins-shrine/locathah.glb": {
|
|
disposition: "expected source-authored torso trim/fin component",
|
|
visualVerdict: "pass",
|
|
note: "The spawn-start flag is one authentic three-vertex/one-face component whose shape remains rigid and whose source weights follow Bip01 Spine with the surrounding torso components. It is not an exporter skin failure.",
|
|
},
|
|
"kalins-shrine/runic-metal-medium-blue.glb": {
|
|
disposition: "expected terminal trim/debris components",
|
|
visualVerdict: "pass",
|
|
note: "Three tiny components (4, 4, and 11 vertices) cross the numeric threshold only at Death end and the three terminal-pose samples. Supplemental renders show a coherent fallen body with no skin explosion.",
|
|
},
|
|
"kalins-shrine/runic-metal-medium-red.glb": {
|
|
disposition: "expected terminal trim/debris components",
|
|
visualVerdict: "pass",
|
|
note: "The blue variant's same three tiny components cross the threshold only at Death end and the terminal pose. Supplemental renders show the red fallen body remains coherent.",
|
|
},
|
|
"sardo-castle/skeleton-king.glb": {
|
|
disposition: "expected articulated Bone10 multipart component",
|
|
visualVerdict: "pass",
|
|
note: "The secondary-attack flag is one authentic six-vertex/four-face component, weighted 100% to Bone10 and moving rigidly with nine other Bone10 components. Deleting or remapping it would remove source-authored tail/ornament geometry.",
|
|
},
|
|
"treasure-trove/shiddale.glb": {
|
|
disposition: "expected tiny equipment accents",
|
|
visualVerdict: "pass",
|
|
note: "Two four-vertex components cross the threshold at Dodge middle, Death end, and the terminal pose. Supplemental renders preserve coherent living and fallen silhouettes.",
|
|
},
|
|
}));
|
|
|
|
const actorVerdicts = summary.actors.map((actor) => {
|
|
const warning = warningDispositionsByVariant.get(actor.input);
|
|
let note = warning?.note
|
|
?? "Bind pose and representative attack, cast, death, wound, idle, action, run, and walk family frames are visually coherent.";
|
|
if (actor.input === "sardo-castle/rune-device-red.glb") {
|
|
note = "Source-native device poses are coherent. The source has no locomotion family, so runtime moving resolves to Stand - stand_idle01; this is a coverage caveat, not a deformation defect.";
|
|
} else if (actor.input === "zurhidon-stronghold/invisible-man.glb") {
|
|
note = "The intentionally invisible helper remains transform-stable across its evaluated poses; its dark placeholder/card appearance in Blender is expected for this asset.";
|
|
}
|
|
return {
|
|
variant: actor.input,
|
|
actorId: actor.actorId,
|
|
numericStatus: actor.status,
|
|
sampledActions: actor.actions,
|
|
sampledPoses: actor.poses,
|
|
flaggedSamples: actor.flaggedSamples,
|
|
visualVerdict: warning?.visualVerdict ?? "pass",
|
|
disposition: warning?.disposition ?? "coherent representative poses",
|
|
note,
|
|
contactSheet: actor.report.replace(/\.pose-audit\.json$/, ".review/contact-sheet.png"),
|
|
poseReport: actor.report,
|
|
};
|
|
});
|
|
|
|
if (actorVerdicts.length !== 75) {
|
|
throw new Error("Expected 75 Batch C actor verdicts, found " + actorVerdicts.length + ".");
|
|
}
|
|
if (warningDispositionsByVariant.size !== summary.totals.warning) {
|
|
throw new Error("Warning disposition count does not match numeric warning count.");
|
|
}
|
|
for (const variant of warningDispositionsByVariant.keys()) {
|
|
if (!actorVerdicts.some((actor) => actor.variant === variant)) {
|
|
throw new Error("Warning disposition has no Batch C actor: " + variant);
|
|
}
|
|
}
|
|
|
|
const runeDevice = coverage.samplePlanByDungeonAndAsset?.["sardo-castle"]?.["rune-device-red"];
|
|
const emptyZone = inventory.emptyCopyZones.find(
|
|
(entry) => entry.dungeonId === "zurhidon-stronghold-124",
|
|
);
|
|
if (!runeDevice?.standardFamilyGaps?.includes("moving")) {
|
|
throw new Error("Expected Rune Device Red to retain its source-native moving-family gap.");
|
|
}
|
|
if (!emptyZone || emptyZone.zoneId !== 124 || emptyZone.entityTemplates !== 0) {
|
|
throw new Error("Expected authoritative empty zurhidon-stronghold-124 inventory row.");
|
|
}
|
|
|
|
const report = {
|
|
schemaVersion: 1,
|
|
batch: "C",
|
|
result: "pass",
|
|
dungeons,
|
|
sourceArtifacts: {
|
|
inputs: "artifacts/animation-audit/batches/batch-c-inputs.json",
|
|
runtimeCoverage: "artifacts/animation-audit/runtime-clip-coverage.json",
|
|
numericSummary: "artifacts/animation-audit/batches/batch-c/summary.json",
|
|
reviewPages: "artifacts/animation-audit/batches/batch-c/review-pages",
|
|
supplementalWarnings: "artifacts/animation-audit/batches/batch-c/warning-review",
|
|
componentDiagnostics: "artifacts/animation-audit/batches/batch-c/component-diagnostics",
|
|
},
|
|
commands: {
|
|
fullBatch: command,
|
|
warningSupplementPattern: warningCommand,
|
|
writeReview: "node scripts/runewaker-pipeline/write-batch-c-animation-review.mjs",
|
|
},
|
|
numericAudit: {
|
|
variants: summary.totals.actors,
|
|
poses: summary.totals.poses,
|
|
pass: summary.totals.pass,
|
|
warning: summary.totals.warning,
|
|
error: summary.totals.error,
|
|
failed: summary.totals.failed,
|
|
flaggedSamples: summary.totals.flaggedSamples,
|
|
},
|
|
visualReview: {
|
|
scope: "Every actor contact sheet on all nine paged dungeon sheets; all six warning actors also received supplemental flagged-frame contact-sheet review.",
|
|
reviewedVariants: actorVerdicts.length,
|
|
variantsWithoutVisiblePoseDefect: actorVerdicts.filter(
|
|
(actor) => actor.visualVerdict === "pass",
|
|
).length,
|
|
variantsWithMinorPoseDefect: 0,
|
|
catastrophicDeformations: 0,
|
|
verdict: "pass",
|
|
},
|
|
poseDefects: [],
|
|
sourceAuthoredComponentEvidence: [
|
|
{
|
|
variant: "kalins-shrine/locathah.glb",
|
|
sample: "Action - spawn / start",
|
|
component: "locathah_1_mesh_0#0035",
|
|
vertices: 3,
|
|
faces: 1,
|
|
primaryBone: "Bip01 Spine",
|
|
primaryBoneWeightSum: 2.97254902,
|
|
shapeDiagonalRatio: 0.995491,
|
|
componentsSharingPrimaryBone: 10,
|
|
evidence: "component-diagnostics/locathah-shipping-spawn-start.json",
|
|
repairDecision: "preserve; source-authored topology and weights",
|
|
},
|
|
{
|
|
variant: "sardo-castle/skeleton-king.glb",
|
|
sample: "Attack - dw_attack02 / middle",
|
|
component: "skeleton_king_1_mesh_0#0308",
|
|
vertices: 6,
|
|
faces: 4,
|
|
primaryBone: "Bone10",
|
|
primaryBoneWeightSum: 6,
|
|
shapeDiagonalRatio: 0.960943,
|
|
componentsSharingPrimaryBone: 10,
|
|
evidence: "component-diagnostics/skeleton-king-shipping-dw-attack02-middle.json",
|
|
repairDecision: "preserve; source-authored articulated multipart topology",
|
|
},
|
|
],
|
|
warningDispositions: [...warningDispositionsByVariant.entries()].map(([variant, row]) => ({
|
|
variant,
|
|
...row,
|
|
})),
|
|
knownRuntimeNamingOrCoverageGaps: [
|
|
{
|
|
variant: "sardo-castle/rune-device-red.glb",
|
|
animationMode: runeDevice.animationMode,
|
|
gap: "moving",
|
|
runtimeFallback: runeDevice.base.moving.name,
|
|
resolvedState: runeDevice.base.moving.resolvedState,
|
|
disposition: "source-native no-locomotion caveat; do not repair in this pass",
|
|
visualVerdict: "pass",
|
|
},
|
|
],
|
|
emptyDungeons: [
|
|
{
|
|
dungeonId: emptyZone.dungeonId,
|
|
zoneId: emptyZone.zoneId,
|
|
entityTemplates: emptyZone.entityTemplates,
|
|
actorPackages: emptyZone.actorPackages,
|
|
shippingAssetVariants: emptyZone.shippingAssetVariants,
|
|
copiesEnvironmentFrom: emptyZone.copiesEnvironmentFrom,
|
|
status: emptyZone.status,
|
|
},
|
|
],
|
|
actorVerdicts,
|
|
};
|
|
|
|
function renderMarkdown() {
|
|
const lines = [
|
|
"# RuneWaker pose audit - batch C",
|
|
"",
|
|
"Result: **PASS**",
|
|
"",
|
|
"All 75 configured shipping GLB variants were evaluated at bind pose plus the",
|
|
"start, middle, and end of every source action: 16,437 poses total. Every actor",
|
|
"contact sheet on all nine paged dungeon sheets was visually reviewed, and all",
|
|
"six numeric-warning actors received a supplemental flagged-frame render.",
|
|
"",
|
|
"The numeric result is 69 passes, six warnings, zero errors, and zero process",
|
|
"failures. No exploded skin, quaternion-basis collapse, or catastrophic",
|
|
"deformation was found. All 75 variants have no visible pose defect.",
|
|
"",
|
|
"## Investigated component warnings",
|
|
"",
|
|
"- Kalin's Shrine Locathah: the spawn-start flag is an authentic three-vertex,",
|
|
" one-face component with preserved shape and source torso-bone weights. It moves",
|
|
" with nine other Spine-weighted components and is not a detached skin fragment.",
|
|
"- Sardo Castle Skeleton King: the secondary-attack flag is an authentic",
|
|
" six-vertex, four-face component weighted completely to Bone10. It moves rigidly",
|
|
" with nine other Bone10 components in the articulated tail/ornament chain.",
|
|
"- No exporter repair was applied. Deleting or reweighting either component would",
|
|
" remove authentic source topology or override valid source bone assignments.",
|
|
"",
|
|
"## Known runtime coverage caveat",
|
|
"",
|
|
"- Sardo Castle Rune Device Red is a coherent source-native device actor with no",
|
|
" locomotion family. Runtime moving therefore falls back to Stand - stand_idle01.",
|
|
" This naming/coverage gap was intentionally not changed in this review.",
|
|
"",
|
|
"## Numeric warning dispositions",
|
|
"",
|
|
"| Variant | Flags | Visual verdict | Disposition |",
|
|
"|---|---:|---|---|",
|
|
];
|
|
for (const warning of report.warningDispositions) {
|
|
const actor = actorVerdicts.find((entry) => entry.variant === warning.variant);
|
|
lines.push("| " + [
|
|
warning.variant,
|
|
actor.flaggedSamples,
|
|
warning.visualVerdict,
|
|
warning.disposition,
|
|
].join(" | ") + " |");
|
|
}
|
|
lines.push(
|
|
"",
|
|
"## Per-actor visual verdicts",
|
|
"",
|
|
"| Variant | Numeric | Poses | Flags | Visual | Disposition |",
|
|
"|---|---|---:|---:|---|---|",
|
|
);
|
|
for (const actor of actorVerdicts) {
|
|
lines.push("| " + [
|
|
actor.variant,
|
|
actor.numericStatus,
|
|
actor.sampledPoses,
|
|
actor.flaggedSamples,
|
|
actor.visualVerdict,
|
|
actor.disposition,
|
|
].join(" | ") + " |");
|
|
}
|
|
lines.push(
|
|
"",
|
|
"## Authoritative empty copy zone",
|
|
"",
|
|
"- zurhidon-stronghold-124 (Zone 124) has zero templates, actor packages, and",
|
|
" shipping variants. It intentionally copies the Zurhidon Stronghold environment;",
|
|
" there were no Zone 124 actors to animate or review.",
|
|
"",
|
|
"## Reproduction",
|
|
"",
|
|
"~~~text",
|
|
command,
|
|
"~~~",
|
|
"",
|
|
"Durable evidence is in summary.json, per-actor pose reports and contact sheets,",
|
|
"review-pages/, warning-review/, and this JSON/Markdown review pair.",
|
|
);
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
await Promise.all([
|
|
writeFile(path.join(batchRoot, "batch-c-review.json"), JSON.stringify(report, null, 2) + "\n"),
|
|
writeFile(path.join(batchRoot, "batch-c-review.md"), renderMarkdown()),
|
|
]);
|
|
|
|
console.log(JSON.stringify({
|
|
result: report.result,
|
|
numericAudit: report.numericAudit,
|
|
visualReview: report.visualReview,
|
|
poseDefects: report.poseDefects.length,
|
|
actorVerdicts: report.actorVerdicts.length,
|
|
}, null, 2));
|
|
|
|
async function readJson(file) {
|
|
return JSON.parse(await readFile(file, "utf8"));
|
|
}
|