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
+31 -5
View File
@@ -1,5 +1,6 @@
## Visual test capture engine — boots main.tscn, runs a named scenario or flow,
## captures viewport PNGs for golden comparison or ad-hoc inspection.
## Visual test capture engine — boots the entry's "scene" (default main.tscn),
## runs a named scenario or flow, captures viewport PNGs for golden comparison
## or ad-hoc inspection.
##
## Usage:
## godot --rendering-driver opengl3 --fixed-fps 60 --resolution 960x540 \
@@ -66,10 +67,22 @@ func _run(): # gdlint:disable=max-returns
# Ensure output directory exists
DirAccess.make_dir_recursive_absolute(_output_dir)
# Load main scene (autoloads already initialized from project.godot)
var main_scene = load("res://scenes/main.tscn")
# Per-entry overrides (T-1088, design §10.2 — both additive; existing entries
# without these keys behave byte-identically):
# "scene": alternate root scene (default res://scenes/main.tscn)
# "env": env vars set before the scene boots (e.g. SR_AUTOPILOT) — OS-level,
# so the scene's _ready() reads them exactly like shell-exported vars;
# run-visual has no per-scenario env mechanism, this is it.
var entry_cfg := _entry_config()
var env_vars: Dictionary = entry_cfg.get("env", {})
for env_key: String in env_vars:
OS.set_environment(env_key, str(env_vars[env_key]))
# Load root scene (autoloads already initialized from project.godot)
var scene_path: String = entry_cfg.get("scene", "res://scenes/main.tscn")
var main_scene = load(scene_path)
if main_scene == null:
push_error("visual_capture: failed to load res://scenes/main.tscn")
push_error("visual_capture: failed to load %s" % scene_path)
quit(1)
return
@@ -298,6 +311,19 @@ func _capture(path: String) -> void:
print("visual_capture: captured -> %s (%dx%d)" % [path, image.get_width(), image.get_height()])
## Config entry for the active scenario or flow ({} when the name is unknown —
## the mode runners report that error themselves).
func _entry_config() -> Dictionary:
var section: Dictionary = {}
if not _scenario.is_empty():
section = _config.get("scenarios", {})
return section.get(_scenario, {})
if not _flow.is_empty():
section = _config.get("flows", {})
return section.get(_flow, {})
return {}
func _load_config() -> Dictionary:
# Config is at tests/visual.json relative to repo root.
# Repo root = parent of Godot project root (client/).