feat(client): add visual test harness with golden regression
Gives Claude eyes: `make screenshot` captures a rendered frame, `make test-visual` compares against golden PNGs, `make visual-update` regenerates goldens. Built to debug the Sprint 22 fog regression and prevent future visual regressions across fog, HUD, dialogue, and UI. Config-driven via tests/visual.json (11 scenarios, 2 flows). Capture engine boots main.tscn with real GPU rendering (not --headless), waits for NoiseTexture2D async gen, uses deterministic shader time. Components: - visual_capture.gd: SceneTree capture engine (scenario + movie modes) - visual_scenarios.gd: per-scenario setup hooks - tooling/visual-diff: pixel comparator (PIL primary, struct fallback) - tooling/visual-thumbnail: contact sheet + crop tool - tests/run-visual: suite script (xvfb wrapping, golden workflow) - fog_state.gd: override_time for deterministic captures - fog_shader.gd: fog_noise_ready signal for settle sequencing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,11 @@ var _prev_visible: Dictionary = {} # Tiles visible last frame (for incremental
|
||||
## Toggle via FogState.debug_exploration = true in the console.
|
||||
var debug_exploration: bool = false
|
||||
|
||||
## Deterministic shader time for visual test captures.
|
||||
## When >= 0, fog_shader.gd uses this instead of Time.get_ticks_msec().
|
||||
## Set before settle frames so noise phase is reproducible across runs.
|
||||
var override_time: float = -1.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_resize(Rect2i(0, 0, 64, 64))
|
||||
|
||||
@@ -4,6 +4,9 @@ extends Node2D
|
||||
## Reads textures from FogState autoload, positions rect to cover viewport.
|
||||
## Architecture: docs/architecture/fog-shader-spec.md
|
||||
|
||||
signal fog_noise_ready
|
||||
var _noise_ready: bool = false
|
||||
|
||||
var _fog_rect: ColorRect
|
||||
var _shader_mat: ShaderMaterial
|
||||
|
||||
@@ -36,10 +39,11 @@ func _ready() -> void:
|
||||
noise_tex.width = 256
|
||||
noise_tex.height = 256
|
||||
noise_tex.seamless = true
|
||||
noise_tex.changed.connect(func(): _noise_ready = true; fog_noise_ready.emit())
|
||||
_shader_mat.set_shader_parameter("noise_tex", noise_tex)
|
||||
_shader_mat.set_shader_parameter("tile_size", TILE_SIZE)
|
||||
|
||||
print("FogShader: Initialized (D-059 5-layer)")
|
||||
print("FogShader: Initialized (D-059 3-state)")
|
||||
|
||||
|
||||
func update_fog() -> void:
|
||||
@@ -68,5 +72,6 @@ func update_fog() -> void:
|
||||
_shader_mat.set_shader_parameter("rect_sz", _fog_rect.size)
|
||||
_shader_mat.set_shader_parameter("map_offset", Vector2(FogState.map_bounds.position))
|
||||
_shader_mat.set_shader_parameter("map_size", Vector2(FogState.map_bounds.size))
|
||||
_shader_mat.set_shader_parameter("time", Time.get_ticks_msec() / 1000.0)
|
||||
var t: float = FogState.override_time if FogState.override_time >= 0.0 else Time.get_ticks_msec() / 1000.0
|
||||
_shader_mat.set_shader_parameter("time", t)
|
||||
_shader_mat.set_shader_parameter("debug_exploration", FogState.debug_exploration)
|
||||
|
||||
Reference in New Issue
Block a user