refactor(client): simplify fog shader to 3-layer model

Reduce from 5-layer to 3-layer fog: clear (forward cone), explored
(light overlay preserving art), and unexplored (solid near-black).
Remove peripheral sector handling from fog_state.gd.

Also preserve exploration data across texture resizes — previously,
resizing the fog texture lost all explored-tile state, causing tiles
behind the player to render as unexplored black instead of light fog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 12:19:44 +01:00
co-authored by Claude Opus 4.6
parent 2016e9a725
commit 1edc8bb1ec
2 changed files with 66 additions and 68 deletions
+36 -9
View File
@@ -35,6 +35,11 @@ func _ready() -> void:
func _resize(bounds: Rect2i) -> void:
var old_bounds := map_bounds
var old_exp := _exp_bytes
var old_w := _width
var old_h := _height
map_bounds = bounds
_width = maxi(bounds.size.x, 1)
_height = maxi(bounds.size.y, 1)
@@ -49,6 +54,21 @@ func _resize(bounds: Rect2i) -> void:
_exp_bytes = PackedByteArray()
_exp_bytes.resize(sz)
_exp_bytes.fill(EXP_UNEXPLORED)
# Preserve exploration data from old bounds into new bounds
if old_exp.size() > 0 and old_w > 0 and old_h > 0:
var dx: int = old_bounds.position.x - bounds.position.x
var dy: int = old_bounds.position.y - bounds.position.y
for oy in range(old_h):
var ny: int = oy + dy
if ny < 0 or ny >= _height:
continue
for ox in range(old_w):
var nx: int = ox + dx
if nx < 0 or nx >= _width:
continue
var old_val: int = old_exp[oy * old_w + ox]
if old_val > EXP_UNEXPLORED:
_exp_bytes[ny * _width + nx] = old_val
_exp_image = Image.create_from_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
exploration_texture = ImageTexture.create_from_image(_exp_image)
@@ -61,29 +81,29 @@ func _resize(bounds: Rect2i) -> void:
func update_from_state() -> void:
# Resize if map bounds changed
# 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 := _compute_bounds(tiles)
var new_bounds := _grow_bounds(tiles)
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
var sectors: Dictionary = GameState.visibility_sectors
# TODO(v0.2): gradual decay over game-time instead of immediate EXP_VISIBLE→EXP_EXPLORED
# 1. Clear visibility, then write current LOS
# 1. Clear visibility, then write current LOS (all tiles are Forward)
_vis_bytes.fill(VIS_HIDDEN)
for pos in positions:
var px: int = pos.x - ox
var py: int = pos.y - oy
if px < 0 or py < 0 or px >= _width or py >= _height:
continue
var sector: String = sectors.get(pos, "Forward")
_vis_bytes[py * _width + px] = VIS_FORWARD if sector == "Forward" else VIS_PERIPHERAL
_vis_bytes[py * _width + px] = VIS_FORWARD
_vis_image.set_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
visibility_texture.update(_vis_image)
@@ -110,7 +130,10 @@ func update_from_state() -> void:
_prev_visible = positions.duplicate()
func _compute_bounds(tiles: Array) -> Rect2i:
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
## tiles always stay within bounds and render as deep fog, not black.
var min_x := 999999
var min_y := 999999
var max_x := -999999
@@ -124,6 +147,10 @@ func _compute_bounds(tiles: Array) -> Rect2i:
max_y = maxi(max_y, int(tile.y))
# Guard: all tiles invalid (no x/y) — sentinels would produce negative Rect2i
if min_x > max_x:
return Rect2i(0, 0, 1, 1)
return map_bounds
# Margin for fog gradient bleed at edges
return Rect2i(min_x - 4, min_y - 4, max_x - min_x + 9, max_y - min_y + 9)
var tile_bounds := Rect2i(min_x - 4, min_y - 4, max_x - min_x + 9, max_y - min_y + 9)
# Merge with existing bounds — grow only
if map_bounds.size.x <= 1 and map_bounds.size.y <= 1:
return tile_bounds
return map_bounds.merge(tile_bounds)