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>
78 lines
3.0 KiB
GDScript
78 lines
3.0 KiB
GDScript
extends Node2D
|
|
|
|
## Fog overlay controller — manages ColorRect + shader uniforms for D-059 fog.
|
|
## 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
|
|
|
|
const TILE_SIZE := float(Constants.TILE_SIZE)
|
|
|
|
|
|
func _ready() -> void:
|
|
# Create the fog overlay ColorRect — transparent fallback so a shader failure
|
|
# doesn't paint solid white over the entire world (z:900 covers everything).
|
|
_fog_rect = ColorRect.new()
|
|
_fog_rect.name = "FogRect"
|
|
_fog_rect.color = Color.TRANSPARENT
|
|
add_child(_fog_rect)
|
|
|
|
# Load shader and create material
|
|
var shader := load("res://shaders/fog.gdshader") as Shader
|
|
if shader == null:
|
|
push_error("FogShader: failed to load res://shaders/fog.gdshader — fog disabled")
|
|
return
|
|
_shader_mat = ShaderMaterial.new()
|
|
_shader_mat.shader = shader
|
|
_fog_rect.material = _shader_mat
|
|
|
|
# Create seamless noise texture for fog animation
|
|
var noise := FastNoiseLite.new()
|
|
noise.noise_type = FastNoiseLite.TYPE_PERLIN
|
|
noise.frequency = 0.03
|
|
var noise_tex := NoiseTexture2D.new()
|
|
noise_tex.noise = noise
|
|
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 3-state)")
|
|
|
|
|
|
func update_fog() -> void:
|
|
if _shader_mat == null:
|
|
return # Shader failed to load — fog disabled
|
|
|
|
# 1. Update FogState textures from GameState
|
|
FogState.update_from_state()
|
|
|
|
# 2. Position ColorRect to cover the current viewport.
|
|
# Use the camera's actual position (not player position) so the fog rect
|
|
# tracks the smoothed camera and never desynchronizes during smooth pan.
|
|
var vp_size := get_viewport().get_visible_rect().size
|
|
var cam := get_viewport().get_camera_2d()
|
|
var zoom := cam.zoom if cam else Vector2(2.0, 2.0)
|
|
var camera_pos := cam.global_position if cam else GameState.player_position * TILE_SIZE
|
|
var half_view := vp_size / (2.0 * zoom)
|
|
_fog_rect.position = camera_pos - half_view
|
|
_fog_rect.size = vp_size / zoom
|
|
|
|
# 3. Update shader uniforms
|
|
_shader_mat.set_shader_parameter("visibility_tex", FogState.visibility_texture)
|
|
_shader_mat.set_shader_parameter("exploration_tex", FogState.exploration_texture)
|
|
_shader_mat.set_shader_parameter("zone_tint_tex", FogState.zone_tint_texture)
|
|
_shader_mat.set_shader_parameter("rect_pos", _fog_rect.position)
|
|
_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))
|
|
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)
|