# RuneWaker Original Animation Import This is the portable, build-time pipeline used to bring original RuneWaker creature skeletons and animations into HealerMan. The shipped game loads ordinary GLBs from `public/assets/creatures/`; it does not load RuneWaker files, contact SQL Server, or require network access. ## Current status Pasper's Shrine remains the compact direct-mesh reference, but the same pipeline is applied across the complete recipe-referenced combat set. Directly mesh-convertible actors ship with native skins and sampled RAS clips. Runtime paperdoll containers are resolved separately into exact, image-specific assembled actors from the portable appearance snapshot. Any genuinely unsupported display remains an explicit pose-only exception. The inventory, runtime-selector, and pose commands below derive their counts from recipes, source graphs, manifests, appearance provenance, and shipping GLBs instead of relying on a hand-maintained list. The authoritative mapping is deliberately expressed at three levels: - 436 runtime entity templates select 268 base actor configurations; - per-dungeon namespacing and paperdoll appearances expand those configurations to 322 runtime-referenced shipping variants; and - 321 variants are expected to be native animated GLBs. Mantarick (Hall of Survivors template `102452`, asset `judgement-light-pose`) is the single documented pose-only variant. `demon-stronghold-125` and `zurhidon-stronghold-124` are authoritative empty copy zones. They must remain present in the 30-dungeon inventory with zero templates and zero actor variants; they are not missing imports. The v5 acceptance pass completed those gates: - all 29 generic population recipes rebuilt green, and Forsaken Abbey's nine actor GLBs passed Khronos validation with zero errors and zero warnings; - the inventory is green at 30 dungeons, 436 templates, 268 base actor configurations, and 322 runtime-referenced variants with zero blockers; - runtime semantic near misses are zero. Mantarick remains the sole pose-only exception and Sardo's Rune Warning Device remains the sole native no-locomotion exception; - the post-v5 Blender audit in `artifacts/animation-audit/post-v5/summary.json` evaluated 44,305 poses across all 322 variants: 282 numeric passes, 40 accepted-warning candidates, zero errors, zero failures, and 634 flagged samples. Those totals exactly match the visually reviewed v4 batches, so the v5 cast-semantics rebuild introduced no numeric pose drift; and - the live packaged/offline browser showed all 30 dungeons Ready and loaded the Pasper's Shrine and Hall of Survivors environments. The only console message was an unrelated library deprecation warning. The 19 focused RuneWaker animation tests pass. The remaining repository-wide build/test blockers belong to the unfinished RoM class catalog and missing Manastorm fixtures, not this animation pipeline. ## Why the first imports were procedural The original exporter loaded each ROS, flattened its meshes to OBJ, and let Blender create a static GLB. Geometry and textures survived, but the GR2 skeleton and RAS controls did not. HealerMan could only animate those static models procedurally. GR2 actors must use the RuneWaker engine's `LoadAnimation_GR2()` path. A GR2 RAS header starts with the `CRuAnimation_GR2` identifier; passing it to the legacy keyframe reader treats that identifier as a huge keyframe count and crashes. ## Conversion flow 1. The native exporter loads the ROS and ACT-referenced RAS files through the original engine and Granny runtime. 2. It exports geometry and textures, selects the body GR2 hierarchy, obtains that hierarchy's source skeleton, and samples each ACT animation control at 30 Hz. 3. It writes `actor-rig.json` and `actor-rig.bin`. The JSON contains schema/version data, bones, clips, durations, and typed-array byte ranges. The binary contains skin influences and sampled translation/rotation/scale tracks. 4. Blender converts the OBJ and textures to a raw static GLB. 5. `inject-actor-rig.mjs` adds the joint hierarchy, inverse-bind matrices, `JOINTS_0`, `WEIGHTS_0`, skin binding, and glTF animation channels. 6. The injector copies influences onto vertices Blender duplicated at material or UV seams, normalizes weights, and bakes Blender's mesh basis into its accessors so skeleton and mesh share a coordinate system. 7. glTF Transform optimizes the asset while preserving the animated hierarchy. 8. The Khronos validator rejects structural errors and warnings. 9. The population generator records the clips and emits `animationMode: native`. At runtime Three.js chooses semantic `Stand`, `Walk`, `Run`, `Attack`, `Cast`, `Wound`, and `Death` clips. ## Runtime paperdoll actors Some `model/character/.../*.ros` files are display containers rather than triangle-bearing creatures. Their source ROS points at `paperdoll.ros`; the actual head, hair, body, hands, feet, and equipment come from the template's `ImageObjectDB` row. Treating that container as a failed static actor produced the generic proxy bodies seen in the first Pasper/all-dungeon pass. Paperdoll conversion is now split into a forensic refresh and a portable normal build: 1. `npm run runewaker:paperdolls:refresh` discovers only image IDs used by combat/boss templates, temporarily restores `ObjectEdit.bak` read-only, exports those component/color rows, writes `snapshots/runewaker-paperdoll-appearances.json`, and drops the temporary database in `finally` cleanup. 2. Normal actor builds read only that checked-in JSON. They write a small TSV assembly request for the native exporter, including reserved `@skin` and `@hair` rows plus the ten supported component slots. 3. The preserved `CRuPaperdoll` path assembles the source components against the container's original skeleton. The exporter then uses the same hierarchy selection, ACT/RAS sampling, skin injection, optimization, and validation gates as a direct actor. 4. The asset ID includes the source image ID, so different appearances that use the same container cannot overwrite or alias one another. Source composite masks can apply runtime color layers. The snapshot and actor manifest preserve the layer values and `requiresColorLayerBake` flag. The current exporter resolves the non-compmask base DDS so geometry, textures, skeleton, and animation remain valid; exact mask-color baking still requires a separate visual-fidelity pass wherever that flag is true. ## Body-rig selection An ACT container can expose more than one `CRuACTEntity` or `CRuHierarchy_GR2`. In particular, some actors put a one-bone effects hierarchy before the actual body. Selecting the first GR2 hierarchy binds body vertex indices and motions to the wrong skeleton. The exporter walks the complete entity tree and retains: - the `CRuACTEntity` whose template has the largest motion library; and - the `CRuHierarchy_GR2` with the largest subnode/bone count. Skin indices, the source Granny skeleton, and sampled ACT controls are then taken from that selected body hierarchy. The v4 cache generation introduced the correct Z-reflection quaternion conversion. The current build recipe uses `actorCacheVersion = gr2-native-animation-v5-z-reflection-complete-cast-semantics-30hz`: v5 retains the v4 geometry repair and also invalidates actors for the complete cast-semantics pass. Changing any hierarchy, transform, clip enumeration, semantic naming, sampling, or rig-injection behavior requires another fresh actor cache version. ## Coordinate-basis and quaternion conversion RuneWaker positions are reflected across Z when they enter the HealerMan/Three basis: ```text S = diag(1, 1, -1) (x, y, z) -> (x, y, -z) ``` A rotation cannot be converted by reflecting the quaternion's Z component. The rotation matrix must receive the same basis change, `R' = S R S`, which is represented by this quaternion mapping: ```text (x, y, z, w) -> (-x, -y, z, w) ``` The earlier `(x, y, -z, w)` mapping left bind poses and GLB structure valid but made animated limbs, armor, and weapons follow the wrong axes. Giant Assassin, Giant Assassin Captain, and Blackhorn Silencer in Pasper's Shrine exposed the problem most clearly. `CopyGrannyTransform` applies the corrected mapping to both skeleton-local bind transforms and every sampled animation frame, then normalizes quaternions and preserves hemisphere continuity. ## v5 complete cast semantics The v5 contract is broader than finding one clip named `Cast`. Every source clip exported from the selected ACT body must remain represented in the actor manifest, and every cast-family clip intended for runtime combat must be classified and reachable by the runtime selector. Attack, cast, and wound one-shots rotate through all semantic matches in manifest order (or all matches of the highest-priority numeric motion ID). A successful rebuild alone does not prove this contract: `npm run runewaker:audit:runtime-clips` is the authority for selector reachability and family gaps. The completed v5 actor cache, manifests, runtime coverage, and evaluated-pose summary are from the same rebuild. Runtime cast coverage is 321/322, semantic near misses are zero, and the remaining non-native row is the documented pose-only Mantarick asset rather than a missed cast export. ## OBJ vertex-remap workflow The native exporter writes one named OBJ group for each exported render mesh and stores the source mesh's dense joint and weight arrays in the rig sidecar. Blender may reorder vertices or duplicate them at UV and material seams, so source and GLB accessor indices cannot be assumed to match. The injector therefore: 1. reads the original `scene.obj` group positions; 2. converts OBJ Y-up coordinates to Blender's local accessor basis `[x, z, -y]`; 3. builds spatial buckets at a `1e-4` tolerance; 4. maps every Blender output vertex to the nearest accepted source vertex, including seam duplicates; 5. copies that source vertex's four joint indices and weights; 6. rejects unmapped vertices or joint indices outside the selected skeleton; and 7. normalizes weights before creating `JOINTS_0`, `WEIGHTS_0`, the skin, and inverse-bind matrices. This position remap is intentionally strict. A failed remap is an extraction error to fix, not a reason to ship a partially skinned actor. ## Required invariants - Keep `SkeletonUtils.clone()` in the creature runtime. Normal Three.js cloning does not independently rebind cloned skinned meshes. - RuneWaker blend indices in these actors are full GR2 skeleton indices; do not remap them through a guessed palette. - For the Z reflection, convert quaternions as `(-x, -y, z, w)`; do not negate quaternion Z. - Preserve quaternion hemisphere continuity while sampling. - Name the single-frame source `dead` clip `Terminal Pose - dead`; the active death action is `Death - death`. - Optimize animated models with `--flatten false --join false --instance false`. - Change `actorCacheVersion` in `build-population.mjs` whenever native export or rig-injection semantics change. - Preserve every exported cast-family clip in manifest order and verify that the runtime cast selector can reach it. - Treat Sardo Castle template `105758` (`rune-device-red`, Rune Warning Device) as a native stationary device. Its source has no locomotion clip by design; `missing-exact-moving` is an approved exception for this asset only. - Treat Mantarick template `102452` (`judgement-light-pose`) as the single pose-only exception. Do not silently convert it into a generic animated proxy. ## Build commands Run from `D:\Projects\HealerMan`. Build the native exporter (development/extraction dependency only): ```powershell powershell -ExecutionPolicy Bypass -File scripts/runewaker-pipeline/native/build-exporter.ps1 ` -RunewakerSourceRoot D:\Projects\rom-pvt-server-backups\Runewaker\SourceCodeVS2019 ``` Rebuild Pasper's Shrine actors while iterating: ```powershell node scripts/runewaker-pipeline/build-population.mjs ` --recipe scripts/runewaker-pipeline/recipes/paspers-shrine-population.json ` --actors-only ``` Rebuild the full portable Pasper package: ```powershell node scripts/runewaker-pipeline/build-population.mjs ` --recipe scripts/runewaker-pipeline/recipes/paspers-shrine-population.json ``` Runtime outputs are under `public/assets/creatures/paspers-shrine/` and `src/game/generated/paspersShrinePopulation.generated.ts`. Render a native actor's uncompressed diagnostic GLB before accepting a rig or exporter change. Blender cannot import the meshopt-compressed shipping GLB, so use `.animated.raw.glb` from `runewaker-export-work`: ```powershell & $env:BLENDER_BIN --background --factory-startup ` --python scripts/runewaker-pipeline/blender/render-animated-actor.py -- ` runewaker-export-work/paspers-shrine-population/actors/fog-ferocity-03/prepared/fog-ferocity-03.animated.raw.glb ` artifacts/animation-diagnostics/fog-ferocity-03-runtime-clips ` "Bind Pose" "Idle - 1h_idle" "Run - run_forward" ` "Attack - 1h_attack01" "Attack - 1h_attack02" ``` The renderer writes the bind pose plus first, middle, and final frames for every requested clip. Inspect the exact semantic clips the runtime will choose; clip names, skins, finite transforms, and Khronos validation alone do not prove pose quality. Refresh the portable paperdoll appearance evidence only when the source backup or combat-template image assignments change: ```powershell npm run runewaker:paperdolls:refresh ``` This is an extraction command, not a game/build dependency. SQL Server may be stopped or removed before every normal actor, population, test, build, and offline browser command. Regenerate all three machine-readable audit layers without rebuilding assets: ```powershell npm run runewaker:audit:animation-inventory npm run runewaker:audit:runtime-clips npm run runewaker:audit:animation-readiness npm run runewaker:audit:animation-readiness -- --json ``` The inventory writes `artifacts/animation-audit/inventory.json` and `inventory.md`. Runtime selection writes `artifacts/animation-audit/runtime-clip-coverage.json` and `runtime-clip-coverage.md`. Readiness Markdown is written to stdout by default; `--json` emits the same per-dungeon and per-actor findings as structured data. The readiness audit exits nonzero only for shipping blockers; unresolved optional source references and unused recipe actors remain visible as warnings. The exact 322-variant Blender command and human-review workflow are in [POSE_AUDIT.md](./POSE_AUDIT.md). ## Per-dungeon verification gate 1. Run the inventory and prove that all 436 entity templates map through 268 base actor configurations to exactly 322 runtime-referenced namespaced variants. Keep both authoritative empty zones in the report. 2. Rebuild with a fresh cache version if exporter behavior changed. 3. Run the runtime selector audit. Confirm 321 variants have `animationMode: native`, Mantarick alone is pose-only, all configured combat clips are reachable, and the Sardo red rune device is the only approved native no-locomotion exception. 4. Run the evaluated-pose audit against the exact 322 shipping paths. Blender must evaluate bind plus first/middle/final samples for every action. Render semantic-family contact sheets, visually review every variant, and expand suspicious actors to `--render-mode all`. Reject collapsed silhouettes, detached equipment, unexpected axis changes, or discontinuous poses. 5. Run the Khronos validator on every shipping GLB; accept zero errors and zero warnings. 6. Run: ```powershell npx vitest run src/game/runewakerDungeonAnimation.test.ts src/game/mobAnimation.test.ts npm test npm run build ``` 7. In a browser, observe idle/stand, locomotion where applicable, every configured attack/cast family, wound, and death. Cover every mob/boss variant over the per-dungeon pass, not only a representative rig. Check loading, skeleton, WebGL, and animation errors. Record evidence and limitations in `PLAYTEST_NOTES.md`. 8. Stop the development server and disable network access for an offline smoke test. Geometry, populations, skeletons, clips, and textures must load from the packaged build. ## Rebuilding or extending a dungeon Use Pasper's Shrine as the compact reference and rebuild one recipe at a time. Do not label a new actor native merely because conversion completed: it needs actor accounting, GLB validation, animation-family coverage tests, and a browser playtest. Document missing/corrupt RAS clips per template and use procedural movement only for explicit pose-only or runtime/dynamic no-mesh exceptions. Encounter mechanics are a separate layer from skeletal animation. Doors, summons, patrols, phases, hazards, and loot should consume the same packaged population IDs without introducing any runtime dependency on the original databases or engine. Development inputs are the RuneWaker ROS/ACT/RAS files, textures, source, Granny runtime, and forensic database snapshot. Runtime inputs are packaged GLBs/textures, generated population data, and HealerMan's in-memory encounter state. Offline and online modes therefore use the same local dungeon package.