Docs: DR-052 SoD facing + underwater feel — build spec, session log, gotchas archive 07-16, skinned-kit cookbook recipe, CLAUDE.md facing contract (net-zero condensations)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 00:54:53 -07:00
parent 4fa2cb0de4
commit a083e77a9e
6 changed files with 207 additions and 9 deletions
@@ -0,0 +1,79 @@
---
title: 2026-07-16 — SoD facing, underwater feel, Bathynaut kit in-engine
date: 2026-07-16
links: "[[DR-052_SoD_Facing_Underwater_Feel]], [[Facing_Underwater_Feel_Build_Spec]], [[DR-051_Lantern_Realignment_Purge]], [[Art_Direction_Lantern]]"
---
# 2026-07-16 — SoD facing, underwater feel, Bathynaut kit (movement/animation tuning session 1)
Operator brief (07-15, /art-dev + /dots-dev): get the lamp/helmet/etc. in-engine on the character; kill rotate-to-cursor — facing "exactly like Shape of Dreams"; heavier underwater movement; heavier animation + footstep sounds; gap-analyze the movement+animation combo.
Forks locked by operator: facing model (a) move-facing + cast-turn + idle-hold · suit = dome+tank+shoulder lamp, **no chest beacon** · **keep cursor-dash** (stationary dash/blink fallback Move→Aim→facing) · **dome + bare male head** (Synty sci-fi helmet removed).
## Shipped (working tree, uncommitted — 22 modified + new files)
### B — Shape-of-Dreams facing (the rework, as corrected by the pre-code review)
- **Pre-code design review `wf_a13d4395-1c7`** (3 lenses → 25 findings → 21 confirmed / 3 refuted / 1 critic died on session-limit; the dead critic's ordering claim was independently refuted by a sibling with full code evidence). **Blocking catch: fire directions did NOT read raw Aim — every archetype + the melee cleave aimed from `PlayerFacing`**; under move-facing every skillshot would have fired along the travel direction. The Build Spec ([[Facing_Underwater_Feel_Build_Spec]]) records all verdicts.
- `FacingMath` (new, Simulation): `ResolveAim` (THE fire-direction resolver: Aim→facing→+Z), `SelectTarget` (shared Aim→Move→hold cascade; castActive grants Aim *priority* only — a resting gamepad stick mid-cast falls to Move), `RotateToward` (verbatim extraction).
- `TickWindowMath` (new, Simulation/Combat): `SwingActive`/`FireActive`/`SocketFireAndCone` moved verbatim from PlayerAnimationDriveSystem (window length stays a caller param) + **Movement-archetype skip** — a blink is a dodge, never a cast; also kills the latent cooldown-tail phantom `IsFiring` pulse. Anim path now shares the same code.
- `PlayerAimSystem`: castActive = melee swing non-Movement socket windows; locomotion rate = stat (360°/s), cast rate = const 1080°/s, both knob-overridable; **no `IsFirstTimeFullyPredictingTick` gate** (incremental integrator over the snapshot-restored [GhostField] — documented in header); `[UpdateAfter(MeleeComboSystem)]` hygiene pin (cycle-free, verified; sorter tie-breaks are deterministic cross-world so this pins semantics, not a divergence fix).
- **Fire-direction decoupling**: AbilityFireSystem (cone :174 / aoe :209 / hitscan :242 / projectile+assist-seed :279) + MeleeComboSystem cleave (:163) → `FacingMath.ResolveAim(input.Aim, facing)`. PlayerFacing = body yaw only. Windup keeps current-tick Aim (existing contract).
- Presentation coupled to the damage source: AimReticleSystem gamepad ring + lock-on tether, CombatFeedbackSystem local socket cue + melee arc (remotes stay facing-based — Aim isn't replicated to non-owners, documented asymmetry).
- DashSystem/BlinkSystem stationary fallback → Move→**Aim**→facing (cursor-dash kept).
### C — Heavier underwater movement
- `DefaultGroundedSharpness` 15→6 — **all coupled sites together** (const + Player.prefab serialized value + 4 DashSystemTests assertions → const refs; the review confirmed DashSystem's every-tick restore stomps a prefab-only edit within one tick).
- `Character_Default.asset` TurnRateDegreesPerSec 720→360 (the asset, not just the initializer).
- TuningKnob 26/27/28 (`TurnRateDeg`/`CastTurnRateDeg`/`MoveSharpness`), Count=29, **0 = no-override sentinel** (explicit ClampKnob case ≥0 — the default ≥1 branch would force a 1-unit override); all six maps + `DebugTuningReport` fields extended (**dev-protocol bump**: a stale standalone dev build will refuse the handshake vs a fresh peer — expected, rebuild both) + DebugOverlay rows + TuningConfigTests pin. Sharpness knob honored at all 4 restore sites (dash tail/idle, blink, death).
### D — Heavier animation + footsteps
- Stride-distance footsteps (`FootstepStrideMeters` 1.5m; accumulate planar travel → cadence tracks the drifting velocity), 3 deep-thud clip variants (95120Hz noise sweeps, decay 7) + ±12% volume jitter + a dark silt puff at the feet (new `SiltPuff` pool emitter). Old fixed-interval timer + `FootstepIntervalSec` deleted.
- `RetimeLocomotionGait` (PlayerRigTools): Locomotion blend-state speed 0.85, idle⇄locomotion blends 0.25s (AnimatorController API).
### A — Bathynaut kit in-engine (art track)
- Blender: 33 kitbash pieces rigid-skinned to Head/Spine_03/Clavicle_L (vgroup w=1 + armature modifier, saved in the master), joined per shader role → `SM_Suit_KitBrass` (940 tris) + `SM_Suit_KitGlow` (104: porthole glass + lamp lens), FBX to `ArtSource/Blender/`. ChestLamp + LampRim excluded per fork.
- Unity: `PlayerRigTools.AttachBathynautKit` — grafts the kit SMRs + the bare male head onto Player.prefab's flattened skeleton by bone name (in place, GUID preserved, ghost surface unchanged); removes the Synty sci-fi helmet. **GraftSmr REBASES**: verts baked to rest-world space, bindposes = inverse of **rigid (scale-stripped)** rest matrices, `RecalculateTangents()` — persisted as `Rebased_*.asset` meshes (GUID-stable Clear+refill).
- New `ProjectM/EmissiveGloamSkinned` shader (hand-written HLSL + Rukhanka `ComputeDeformedVertex`, in-place skinning) on `M_EmissiveGloam_WarmSkinned` (steady warm gold — true light): porthole + lamp lens glow and DEFORM.
- Player.prefab children now: Root skeleton · body · armour · KitBrass · KitGlow · bare head. Suit tris ~8.9k (body 7.9k + kit 1k) — still over the ≤6k hero budget; decimate pass stays parked (07-14).
## Validation
- L1: console clean (only known tick-batching noise from execute_code stalls).
- L2: **409/409 EditMode green** (+19: FacingMathTests 11, TickWindowMathTests 7 incl. the Movement-skip + resting-stick-cast cases, TuningConfigTests sentinel pin; SystemOrderingCycleTests now co-registers PlayerAim/PlayerControl/Blink/PlayerDeathState).
- Play smoke (DevSandbox, client+server): **server==client facing under injected input**; move-east → facing (1,0) both worlds; **idle + aim-north holds facing** (no passive cursor tracking); cast opens → facing swings fully north at cast rate and returns; blink socket does NOT open a cast window; **projectile fired while moving east flies (0.00, 1.00) = raw Aim** — the SoD manual-aim contract, live. World creation clean (ordering pin acyclic).
- L3: screenshots (`Assets/Screenshots/suitkit_front_glow.png`, `suitkit_back_rigid.png`) — brass dome + glowing amber porthole behind the grille + shoulder lamp from the front; dome crown + tank pack silhouette from the back/game angle. Kit deforms with the rig.
- Post-impl diff review `wf_9a8d6162-e72` (20/20 agents, no failures): **17 findings → 15 confirmed (0 blocking: 3 should-fix + 12 nits) / 2 refuted.** All triaged and closed same-session:
- **Fixed in code**: blink no longer fires the muzzle/fire cue (the third SocketCooldown consumer got the Movement skip); footsteps got a 0.18s cadence floor (a dash could machine-gun 24 thuds); `GraftSmr` now validates the bone rebind BEFORE destroying the old child (a mismatch used to silently strip the piece) + a rigid-skinning guard (`weight0<0.999` aborts) + `rebase:false` for the meter-scale blend-skinned head (tool now reproduces the committed prefab); orphaned doc-blocks deleted from PlayerAnimationDriveSystem; `k_AttackAnimTicks` now structurally = `PlayerAimSystem.CastFacingTicks`; dead ResolveAim re-guard removed from AimReticleSystem; remote-arc asymmetry documented at UpdateRemoteSwings; wrap (0-sentinel) + no-blob tests added to TickWindowMathTests. **Suite 411/411 green after fixes.**
- **Docs amended**: Build Spec B.6 (local FX = ResolveAim, deliberately better than the draft cascade — cue==damage in every case) + Part D (pitch jitter shipped as 3 clip variants + volume jitter).
- **Accepted**: the Move→Aim→facing cascade is duplicated verbatim in DashSystem/BlinkSystem (byte-identical, commented; a `ResolveDashDir` helper is available cleanup if a third site ever appears).
## Gotchas learned (new, durable)
1. **Blender skinned-kit export: a HIDDEN armature can't be selected → `use_selection` FBX export silently drops the skeleton + all vgroups** (the mesh imports as static MeshRenderers). `hide_set(False)` before selecting; restore after.
2. **Blender→Unity skinned roundtrip imports cm bones under a 0.01 armature** (regardless of FBX_SCALE_UNITS vs FBX_SCALE_ALL) while Synty-native skeletons are meter-scale. **Rebind by bindpose reuse EXPLODES (×100)**. The fix that works: rebase at graft time — bake verts to rest-world, bindposes = inverse of **rigid (scale-stripped)** rest matrices (`Matrix4x4.TRS(pos, m.rotation, one).inverse`).
3. **A procedural skinned Mesh asset without TANGENTS fails Rukhanka/BRG registration** (`BatchMeshID not present` + assertion spam, whole rig vanishes) — `RecalculateTangents()` mandatory.
4. **Hand-written Rukhanka deformation shaders**: the `UNITY_DOTS_INSTANCING_START` block with `_DeformedMeshIndex` must be declared **BEFORE** including `ComputeDeformedVertex.hlsl` (macro expands at include time → else "undeclared identifier ..._DOTSInstancingOverrideMode" only in the DOTS_INSTANCING_ON variant = magenta), AND `_DeformedMeshIndex` must ALSO be a Properties-block entry — Rukhanka's SkinnedMeshBaker validates `material.HasProperty`.
5. Synty variant containers hold BOTH genders' heads — match grafts by exact name (`Contains("Head")` grabbed the female head first).
6. `manage_asset rename` can half-fail (moved the asset to `Assets/` root, extensionless, while reporting an error) — verify on disk; a plain file copy + `refresh_unity force` is the reliable fallback.
## Follow-ups (surfaced, not silent)
- `Ability_Blink.asset` CooldownTicks=1 vs BlinkSystem's 150-tick stamp (HUD bar + window-math hygiene) — align to 150 later.
- Bathynaut game-ready decimate pass (~8.9k tris vs ≤6k hero budget) — parked since 07-14.
- EmissiveGloamSkinned is BRG-only by design (invisible in plain classic scenes — same class as all EG-skinned materials).
- Operator eyes-on tuning pass: knobs live in the DevSandbox overlay (Turn rate deg / Cast turn deg / Move sharp + the melee/dash rows); FeelConfig footstep values compile-time for now.
## Part E — gap analysis (the operator deliverable)
What the movement+animation combo still lacks for the underwater-heavy feel, in recommended order:
1. **Idle sway/breathing** — the suit stands statue-still; a subtle full-body idle drift (Blender clip on the humanoid pipeline) would sell suspension-in-water more than any tuning knob.
2. **Start/stop weight reads** — sharpness 6 gives the velocity drift, but there's no lean-in/lean-back pose; a 2-frame lean overlay driven off acceleration (AnimParamMath already computes the basis) is the cheap version.
3. **Bubble exhaust** — periodic bubble trickle from the dome (pool emitter, warm-neutral, rises) synced loosely to the footstep cadence; strongest cheap underwater cue after the silt puffs.
4. **Turn lean/banking** — body roll proportional to the facing turn rate (client-only, presentation quaternion tweak).
5. **Camera weight** — PrototypeCameraRig positional lag/damping scaled up slightly so the camera "drags" through water with the player.
6. **Underwater ambience loop** — procedural low rumble + occasional distant groans; the soundscape is currently just SFX.
7. **Walk-cycle authoring** — the retimed Synty walk still reads "land walk slowed down"; a Blender heavy-trudge clip (lead with the chest, delayed foot plants) is the real fix per the humanoid clip pipeline.
## Next session
Operator eyes-on feel pass with the live knobs (turn rates, sharpness, footstep values), then pick from the gap list (recommend 1+3 first). Post-impl review findings (wf_9a8d6162-e72) to triage if any confirmed.