fix(client): fix fog system — blocky edges and zero explored visibility (#569)
Root cause: update_from_state() used visible_tiles (always empty in live server mode) instead of visible_positions for bounds calculation. Bounds never grew beyond 64x64, so tiles outside that area rendered as solid unexplored black. Fix: new _grow_bounds_from_positions() method reads visible_positions (always populated from server snapshots). Shader fix: removed the (explored < 0.01 && vis_raw < 0.01) guard that cut off the Gaussian gradient at unexplored tile boundaries. Doubled blur step size for D-066 compliant 6-8 tile soft gradient. Added debug_exploration mode for diagnostic rendering of the exploration texture. 19 acceptance tests covering exploration persistence, bounds grow-only invariant, gradient margin, Forward-only visibility writes, and exploration data surviving texture resize. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,11 @@ var _width: int = 1
|
||||
var _height: int = 1
|
||||
var _prev_visible: Dictionary = {} # Tiles visible last frame (for incremental decay)
|
||||
|
||||
## Debug flag — when true, fog.gdshader renders raw exploration texture
|
||||
## as colored overlay (green=visible, blue=explored, red=unexplored).
|
||||
## Toggle via FogState.debug_exploration = true in the console.
|
||||
var debug_exploration: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_resize(Rect2i(0, 0, 64, 64))
|
||||
@@ -84,15 +89,17 @@ func update_from_state() -> void:
|
||||
# Grow bounds to include newly visible tiles — never shrink, so explored
|
||||
# tiles behind the player stay in the texture and render as deep fog
|
||||
# instead of black. Exploration data is preserved across resizes.
|
||||
var tiles := GameState.visible_tiles
|
||||
if tiles.size() > 0:
|
||||
var new_bounds := _grow_bounds(tiles)
|
||||
# Use visible_positions (always populated from server snapshots) instead of
|
||||
# visible_tiles, which stays empty in live server mode because the server
|
||||
# sends tile_kind but game_state.gd's population check expects "type".
|
||||
var positions: Dictionary = GameState.visible_positions
|
||||
if positions.size() > 0:
|
||||
var new_bounds := _grow_bounds_from_positions(positions)
|
||||
if new_bounds != map_bounds:
|
||||
_resize(new_bounds)
|
||||
|
||||
var ox: int = map_bounds.position.x
|
||||
var oy: int = map_bounds.position.y
|
||||
var positions: Dictionary = GameState.visible_positions
|
||||
|
||||
# TODO(v0.2): gradual decay over game-time instead of immediate EXP_VISIBLE→EXP_EXPLORED
|
||||
|
||||
@@ -130,6 +137,27 @@ func update_from_state() -> void:
|
||||
_prev_visible = positions.duplicate()
|
||||
|
||||
|
||||
func _grow_bounds_from_positions(positions: Dictionary) -> Rect2i:
|
||||
## Compute bounds from visible_positions (Dictionary[Vector2i, bool]).
|
||||
## Equivalent to _grow_bounds() but reads from the always-populated
|
||||
## positions dict instead of the legacy visible_tiles Array.
|
||||
var min_x := 999999
|
||||
var min_y := 999999
|
||||
var max_x := -999999
|
||||
var max_y := -999999
|
||||
for pos in positions:
|
||||
min_x = mini(min_x, pos.x)
|
||||
min_y = mini(min_y, pos.y)
|
||||
max_x = maxi(max_x, pos.x)
|
||||
max_y = maxi(max_y, pos.y)
|
||||
if min_x > max_x:
|
||||
return map_bounds
|
||||
var tile_bounds := Rect2i(min_x - 8, min_y - 8, max_x - min_x + 17, max_y - min_y + 17)
|
||||
if map_bounds.size.x <= 1 and map_bounds.size.y <= 1:
|
||||
return tile_bounds
|
||||
return map_bounds.merge(tile_bounds)
|
||||
|
||||
|
||||
func _grow_bounds(tiles: Array) -> Rect2i:
|
||||
## Compute bounds that include all currently visible tiles, merged with
|
||||
## existing bounds so the texture only grows — never shrinks. Explored
|
||||
@@ -148,8 +176,11 @@ func _grow_bounds(tiles: Array) -> Rect2i:
|
||||
# Guard: all tiles invalid (no x/y) — sentinels would produce negative Rect2i
|
||||
if min_x > max_x:
|
||||
return map_bounds
|
||||
# Margin for fog gradient bleed at edges
|
||||
var tile_bounds := Rect2i(min_x - 4, min_y - 4, max_x - min_x + 9, max_y - min_y + 9)
|
||||
# Margin for fog gradient bleed at edges.
|
||||
# The Gaussian blur in fog.gdshader samples at 2-texel intervals across a 7x7 kernel,
|
||||
# reaching ±6 tiles from the fragment position. Margin must be >= 8 to avoid
|
||||
# clamping artifacts at texture boundaries.
|
||||
var tile_bounds := Rect2i(min_x - 8, min_y - 8, max_x - min_x + 17, max_y - min_y + 17)
|
||||
# Merge with existing bounds — grow only
|
||||
if map_bounds.size.x <= 1 and map_bounds.size.y <= 1:
|
||||
return tile_bounds
|
||||
|
||||
@@ -69,3 +69,4 @@ func update_fog() -> void:
|
||||
_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)
|
||||
_shader_mat.set_shader_parameter("debug_exploration", FogState.debug_exploration)
|
||||
|
||||
Reference in New Issue
Block a user