--- title: Facing & Underwater Feel — Build Spec (SoD facing + heavier movement + suit kit) date: 2026-07-15 links: '[[DR-051_Lantern_Realignment_Purge]], [[Roadmap_Lantern_Slice]], [[Identity_Lantern]]' permalink: gamevault/06-roadmap/facing-underwater-feel-build-spec --- # Facing & Underwater Feel — Build Spec Pre-code design review run `wf_a13d4395-1c7` (3 lenses, 25 findings → 21 confirmed / 3 refuted / 1 critic died on session-limit — its ordering claim was independently refuted by a sibling critic with full code evidence). This spec = the approved plan **as corrected by the review**. Operator forks locked 07-15: facing model (a) move-facing + cast-turn; suit = dome+tank+shoulder-lamp (no chest beacon); **keep cursor-dash** (Move→Aim→facing fallback); **dome + bare head** (sci-fi helmet removed). ## The review's blocking correction ★ **Fire directions do NOT read `PlayerInput.Aim` today — they read `PlayerFacing`** (AbilityFireSystem cFace :174 / aFace :209 / hFace :242 / rawAim :279 + auto-target seed :286; MeleeComboSystem face :163). Under move-facing every skillshot/cleave would fire along the travel direction. Fix (wire-neutral, both queries already carry `PlayerInput`): re-source all five sites to `FacingMath.ResolveAim(input.Aim, facing.Direction)` = normalize(Aim) if lengthsq>1e-6 else facing else (0,1). **`PlayerFacing` becomes body-yaw/presentation only.** Windup path keeps current-tick Aim (existing contract). ## Part B — facing rework (Simulation) 1. **`Simulation/Combat/TickWindowMath.cs`** (new): `SwingActive`/`FireActive`/`SocketFireAndCone` moved **verbatim** from PlayerAnimationDriveSystem (:118-162), window length stays a caller param. Add Movement-archetype skip (hoist the blob lookup, `continue` on `AbilityArchetype.Movement`) — used by BOTH the facing path and the anim path (kills the latent cooldown-tail phantom `IsFiring` from the blink socket; anim behavior change is a bug fix). Blink socket must never count as a cast (review: blink stamps its cooldown row → facing would yank toward cursor at the cooldown tail). 2. **`Simulation/FacingMath.cs`** (new): `ResolveAim` (above) + `SelectTarget` + `RotateToward` (verbatim extraction of PlayerAimSystem :39-59). **One shared Aim→Move→hold cascade in BOTH branches** — castActive selects turn RATE + Aim-priority, never bypasses the cascade (gamepad Aim is zeroed at rest → a resting-stick cast must fall through to Move). 3. **PlayerAimSystem**: target = castActive(melee swing window ∪ non-Movement socket windows) ? cascade-with-Aim-first : (Move | hold). Locomotion rate = `EffectiveCharacterStats.TurnRateRadiansPerSec` (knob-overridable), cast rate = const 1080°/s (knob-overridable). **No `IsFirstTimeFullyPredictingTick` gate — the rate-limited turn is an incremental integrator over the snapshot-restored [GhostField]; it must re-integrate on EVERY predicted pass** (document in header, DashSystem wording). Add `[UpdateAfter(typeof(MeleeComboSystem))]` — optional hygiene pin (cycle-free, verified against the full predicted-group edge set; sorter tie-break is deterministic and cross-world-identical, so this pins same-tick cast-window semantics, not a divergence fix). Socket windows open 1 tick late by construction (AbilityFire stamps after PlayerAim) — acceptable, cosmetic-only once fire directions are decoupled. 4. **DashSystem :67 + BlinkSystem :101**: stationary fallback Move → **Aim** → facing (operator: keep cursor-dash). Update the :64-65 comment. 5. **AimReticleSystem**: add `RefRO`; gamepad ring + tether axis from `ResolveAim(Aim, facing)` (tether stays consistent with the server assist cone because the assist seed also moves to Aim). 6. **CombatFeedbackSystem**: LOCAL socket cue (:330-335) + melee arc (:409-413) direction from `FacingMath.ResolveAim(Aim, facing)` — the SAME resolver as the sim fire sites, so cue == damage direction in every case *(amended post-impl: the draft's literal Aim→Move→facing could diverge from damage on a moving resting-stick gamepad cast)*; remote arcs stay facing-based (Aim not replicated to non-owners; asymmetry documented at UpdateRemoteSwings). ## Part C — heavier movement (all coupled sites together ★) - Sharpness 15→6: `CharacterComponent.DefaultGroundedSharpness` const AND `Player.prefab` serialized `GroundedMovementSharpness: 15`→6 AND DashSystemTests literal 15f assertions (:94/:113/:207/:230) → const reference. (DashSystem's non-dash branch restores to the const EVERY tick — a prefab-only edit is stomped within one tick.) - Turn rate 720→360: **`Character_Default.asset`** `TurnRateDegreesPerSec` (the asset — serialized wins) + `CharacterStatsDefinition.cs:18` initializer hygiene. Re-bake accepted (DependsOn; no ghost-hash change). - **Knobs** (full checklist in one pass): `TuningKnob.TurnRateDeg=26, CastTurnRateDeg=27, MoveSharpness=28, Count=29` (retired 20-23 untouched). Semantics: **0 = no override** (use stat/const/authored). Extend Defaults(0)/explicit ClampKnob case (`max(0f,v)` — must NOT fall into the default ≥1 branch)/Apply/Get/ToReport/FromReport/`DebugTuningReport` fields (unconditional, no #if)/TuningConfigTests pin/DebugOverlay rows. Consumption: PlayerAimSystem rates; sharpness override at the restore sites (DashSystem :97/:101, BlinkSystem :127, PlayerDeathStateSystem :51) so the knob is live within a tick. **Dev-protocol bump**: DebugTuningReport layout changes → a stale standalone dev build refuses the handshake vs a fresh peer; expected, rebuild both. ## Part D — footsteps + gait (client-only) - Stride-distance footsteps (accumulate planar distance, step every `FootstepStrideMeters`≈1.5, + a 0.18s cadence floor so a dash can't machine-gun thuds — post-impl review), heavier clip (~100Hz, longer decay, noise), jitter *(shipped as 3 fixed pitch-variant clips 95/108/120Hz + ±12% volume jitter — `PlayClipAtPoint` has no pitch control)*, silt-puff particle per step. New FeelConfig fields. - Locomotion blend state speed ~0.85 + idle↔locomotion transitions ~0.25s via AnimatorController API (PlayerRigTools). ## Part A — suit kit (art) Blender export was missing the armature (FBX had no skin — cause under diagnosis; re-export with verified selection). Kit = `SM_Suit_KitBrass` (940 tris, Head/Spine_03/Clavicle_L rigid-skinned) + `SM_Suit_KitGlow` (104 tris) → FBX → `AttachBathynautKit` re-binds SMRs by bone name onto Player.prefab. Brass = `M_Skinned_Palette`; glow = new `ProjectM/EmissiveGloamSkinned` (hand-written HLSL + Rukhanka `ComputeDeformedVertex`, DOTS-instanced `_DeformedMeshIndex`) on `M_EmissiveGloam_WarmSkinned` (steady warm). Plus: **remove `SM_Chr_Attach_SpaceSoldier_Male_Helmet_01`, graft the bare head** (Bathynaut material). ## Tests / validation FacingMathTests (cascade incl. resting-stick cast; RotateToward step/snap), TickWindowMathTests (windows + Movement skip + wrap), DashSystemTests → const, SystemOrderingCycleTests + PlayerAimSystem (+PlayerControl/Blink) in the fixture, TuningConfigTests knob pins. L1 console clean · L2 suite green + Play smoke (server==client PlayerFacing under injected input) · L3 suit + steps screenshots. Post-impl diff review (same lenses). ## Deferred (surfaced, not silent) - `Ability_Blink.asset` CooldownTicks 1 vs BlinkSystem's 150 stamp (HUD bar + window-math hygiene) — align later. - Skinned EG shader on non-DOTS/classic path renders undeformed (BRG-only by design, like all EG-skinned materials). - Bathynaut body still over hero tri budget (game-ready decimate pass parked since 07-14; kit adds ~1044).