feat(client): purchased UAL tiers wired — lib/Clip addressing, stance enter/exits, turn-in-place (T-1088)

ual1.glb (120 clips) + ual2.glb (134) replace the free-tier subsets;
CharacterVisual loads both under explicit library names with lib/Clip exact
addressing (bare names keep cross-library search). Gait table re-pointed;
two new one-shot overlays in the gait machine: stance transitions
(Sprint/Crouch Enter/Exit, destination-Enter priority, clip-length timer,
movement never stalls) and turn-in-place (Turn90/180 L/R from shortest-arc
sign, >=60/135 deg idle yaw jumps, retrigger-guarded). All clip names
verified by dumping the imported GLBs. 70 gait tests + live smoke green
(both libraries load with exact counts).

S9 live-tune list: turn handedness (unverified headless — one-line swap),
TURN_SPEED_SCALE=2.0 compromise vs the snappy yaw ease, turn-on-stop feel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 21:53:35 +02:00
co-authored by Claude Fable 5
parent e46aa404fc
commit c82ea3ec92
10 changed files with 580 additions and 94 deletions
+64 -9
View File
@@ -56,20 +56,23 @@ const TURN_BUDGET_DEG := {
# -- Animation (design §6) -------------------------------------------------------
## Gait table (§6.1) — the ONLY place animation clip strings live.
## Clip names are the imported bare names: library "", "_Loop" stripped by the glTF
## importer, case-sensitive (design-input §2.2). Careful = Walk_Formal (D-053 stance
## readability at ortho distance; fallback if it reads "parade march": Walk at 0.6x —
## one table cell). Jog_Fwd is the reserve if Sprint reads too aggressive at 2.5 m/s.
## Gait table (§6.1) — the ONLY place gait clip strings live. Explicit "lib/Clip"
## addressing (D-248 / T-1088): every locomotion loop lives in the UAL1 tier. Names
## are the verified imported names (dumped 2026-07-06 from ual1.glb): the glTF importer
## strips the "_Loop" suffix and sets loop mode, so "Walk_Loop" -> "Walk" (loop=1).
## Careful = Walk_Formal (D-053 stance readability at ortho distance; fallback if it
## reads "parade march": Walk at 0.6x — one table cell). Jog_Fwd (ual1) is the reserve
## if Sprint reads too aggressive at 2.5 m/s.
const GAIT_CLIP := {
"Sprint": {"idle": &"Idle", "moving": &"Sprint"},
"Walk": {"idle": &"Idle", "moving": &"Walk"},
"Careful": {"idle": &"Idle", "moving": &"Walk_Formal"},
"Crouch": {"idle": &"Crouch_Idle", "moving": &"Crouch_Fwd"},
"Sprint": {"idle": &"ual1/Idle", "moving": &"ual1/Sprint"},
"Walk": {"idle": &"ual1/Idle", "moving": &"ual1/Walk"},
"Careful": {"idle": &"ual1/Idle", "moving": &"ual1/Walk_Formal"},
"Crouch": {"idle": &"ual1/Crouch_Idle", "moving": &"ual1/Crouch_Fwd"},
}
## Native clip ground speed (m/s) for cadence sync (§6.2):
## speed_scale = clamp(rig_speed / NATIVE_MPS[clip], SPEED_SCALE_CLAMP.x, .y).
## Keyed by clip BASENAME (the part after "lib/") so it is addressing-agnostic.
## Initial guesses — tuned live against the DebugHud readout.
const NATIVE_MPS := {"Walk": 1.4, "Walk_Formal": 1.2, "Sprint": 3.2, "Crouch_Fwd": 0.9}
const SPEED_SCALE_CLAMP := Vector2(0.6, 1.8)
@@ -84,6 +87,58 @@ const BLEND := {
"teleport": 0.0,
}
# -- Stance-transition one-shots (T-1088; UAL1 Enter/Exit clips) ----------------------
#
# On a stance change, if the destination stance has an Enter clip OR the source stance
# has an Exit clip, play that one-shot (loop=0) and blend into the target loop when it
# elapses. The rig position channel is independent, so the character keeps gliding while
# the transition plays (movement never stalls). Only Sprint and Crouch have Enter/Exit
# clips in UAL1 (verified 2026-07-06); Walk/Careful transition with a plain gait blend.
# Priority when both apply (only Crouch<->Sprint): destination Enter wins over source
# Exit — "entering the new stance" is the salient action. Live-tune the edge if it reads
# wrong. Names are verified imported names (all loop=0 one-shots).
const STANCE_ENTER_CLIP := {
"Sprint": &"ual1/Sprint_Enter",
"Crouch": &"ual1/Crouch_Enter",
}
const STANCE_EXIT_CLIP := {
"Sprint": &"ual1/Sprint_Exit",
"Crouch": &"ual1/Crouch_Exit",
}
## Cross-fade (s) INTO a stance-transition one-shot. The blend back OUT to the target
## loop reuses the BLEND table (crouch clips take BLEND.crouch).
const STANCE_TRANSITION_BLEND := 0.12
# -- Turn-in-place one-shots (T-1088; UAL1 Turn90, UAL2 Turn180) ----------------------
#
# When IDLE and the yaw TARGET jumps (mouse-driven octant snap) by >= TURN_TRIGGER_DEG,
# play a turn one-shot synced to the rig's yaw ease, then fall back to the stance idle
# clip. L/R is chosen from the sign of the shortest-arc delta; >= TURN_180_DEG uses the
# 180 clip (UAL2). Retrigger is guarded — a fresh turn cannot start until the current
# one elapses. Octants snap at 45 deg, so effective triggers are 90 deg -> Turn90 and
# 135/180 deg -> Turn180. Names are verified imported names (all loop=0 one-shots).
## Minimum yaw-target jump (deg) that triggers a turn-in-place. Below this, the rig's
## normal yaw ease handles the reface with no clip.
const TURN_TRIGGER_DEG := 60.0
## At/above this jump (deg), use the Turn180 clip instead of Turn90.
const TURN_180_DEG := 135.0
## Turn one-shot clips keyed by "<arc>_<hand>". Turn90 is UAL1, Turn180 is UAL2.
## Hand: positive shortest-arc yaw delta -> "_L", negative -> "_R" (VERIFY handedness
## live — a swap is a one-line fix if the model turns the wrong way).
const TURN_CLIP := {
"90_L": &"ual1/Turn90_L",
"90_R": &"ual1/Turn90_R",
"180_L": &"ual2/Turn180_L",
"180_R": &"ual2/Turn180_R",
}
## Cross-fade (s) into and out of a turn one-shot.
const TURN_BLEND := 0.15
## Playback rate for turn one-shots. The UAL turn clips are deliberate (~1.7-2.0 s); the
## rig's idle yaw ease is snappy (~0.3 s for 180 deg), so at 1.0x the turn shuffle drags
## long past the settled body. 2.0x is an initial compromise — S9 live-tunes it against
## the on-screen yaw-ease duration (raise to shorten the turn, lower for weightier turns).
const TURN_SPEED_SCALE := 2.0
# -- Camera (design §7; D-148, D-015, D-158) -------------------------------------
## Pitch presets in degrees from horizontal — D-148 preset list is authoritative;