#!/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 " + " --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")); }