feat(client): 3D locomotion sandbox — character walks the live Gauntlet (T-1088)

New SR_LIVE sandbox scene: CharacterVisual composited in a 3D greybox world
derived from server snapshots. Per-leg constant-velocity interpolation keyed
to the stance throttle, 'server feet / client eyes' facing (wire octant while
moving, client aim octant idle), cadence-synced gait state machine on
AnimationPlayer custom blends, D-148 orthographic follow camera (-30deg
default, T-cycle presets), sim-space grid shader, camera-side wall cutaway,
accumulating never-evict tile store with four-state visibility tint.

Additive seams only: InputMapper.facing_angle_provider (2D path unchanged),
CharacterVisual.play_animation blend_time param + get_animation_player().
Visual harness gains per-scenario scene field + SR_AUTOPILOT input scripting.
210 new gdUnit assertions across five suites; verified live (230/230 total,
clean smoke, screenshot at .cache/screenshots/locomotion_idle_live.png).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 13:23:16 +02:00
co-authored by Claude Fable 5
parent ce1811b5f9
commit a7801a942a
21 changed files with 3555 additions and 8 deletions
+17
View File
@@ -59,6 +59,13 @@ var facing_octant: String = "North" # Derived from facing_angle
var _last_sent_octant: String = "North" # Track to avoid redundant sends
var _last_move_msec: int = 0
# T-1088 (design §9): 3D facing seam. A 3D scene installs a provider returning
# sim-space radians (0=East, +PI/2=South — same convention as facing_angle), or
# NAN = no update (deadzone/degenerate ray). While installed it fully replaces the
# 2D canvas-transform path in _update_facing_from_mouse(); unset (default) leaves
# the 2D path unchanged. Untyped Callable per the autoload parse-order rule.
var facing_angle_provider = Callable()
# Hold-to-move: poll held direction keys each frame, throttled by stance.
# D-054: WASD is now mouse-relative. W = toward cursor, A/D = strafe.
@@ -180,6 +187,16 @@ func reset_facing_state() -> void:
# Intentional coupling: reads GameState.player_position directly — InputMapper is an
# autoload that runs before game loop rendering, so position is always current-tick.
func _update_facing_from_mouse() -> void:
# T-1088 (design §9): provider seam. In a 3D scene the canvas-transform anchor
# below is a far-off-screen point, and the screen-space mouse angle is not a
# sim-space angle under the 45° map rotation + ortho pitch — an installed
# provider (e.g. SandboxMouseAimProvider) replaces this whole path.
if facing_angle_provider.is_valid():
var a: float = facing_angle_provider.call()
if is_finite(a):
facing_angle = a
facing_octant = _angle_to_octant(a)
return
var vp := get_viewport()
if vp == null:
return