Files
settled-reach/client/scripts/rendering/fog_renderer.gd
T
jpmschweitzerandClaude Opus 4.6 aa3a3fea7d fix(client): address PR review — invalidation, constants, tests
Review feedback from Hoshe + Tyre on PR #8:

- Extract shared TILE_SIZE to Constants class_name (Tyre #5, Hoshe #7)
- Fix tile invalidation: detect tile/visibility count changes instead
  of one-shot dirty flag, supports chunk loading (Tyre #3, #4)
- Fog dirty tracking: only re-render when visible_positions changes
- Add bounds warning for unknown tile types (Hoshe #1)
- Add player-not-found warning in GameState (Hoshe #5)
- Use Image.fill_rect() instead of pixel loops (Tyre #10)
- Document _player_pos as reserved for fog decay #113 (Tyre #6)
- Add TODO(#130) for D-033 relationship colors (Hoshe #3, Tyre #8)
- Add class_name to EntityRenderer, FogRenderer, TileRenderer
- 20 new rendering tests (D-030 Layer 1): entity lifecycle, fog
  registration, tile type mapping, snapshot completeness, constants
- 65 total tests passing, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:15:42 +01:00

89 lines
2.9 KiB
GDScript

class_name FogRenderer
extends TileMapLayer
# Fog renderer — draws fog overlay on non-visible tiles (D-011)
# Three visibility states per tile:
# visible = no fog tile (clear)
# fog-edge = semi-transparent dark overlay (adjacent to visible)
# hidden = opaque black overlay
#
# Atlas layout:
# (0,0) = full fog (opaque black)
# (1,0) = fog edge (semi-transparent)
#
# Note: fog-edge uses 8-directional neighbors for visual smoothness.
# Actual visibility boundaries come from the server's shadowcasting (D-011).
# Fog-returns-over-time (D-011 decay) is tracked in #113, not here.
const TILE_SIZE: int = Constants.TILE_SIZE
var _initialized: bool = false
var _all_tile_positions: Dictionary = {} # Vector2i -> true, all known map tiles
func _ready() -> void:
_setup_tileset()
_initialized = true
print("FogRenderer: Initialized")
func _setup_tileset() -> void:
var ts := TileSet.new()
ts.tile_size = Vector2i(TILE_SIZE, TILE_SIZE)
var source := TileSetAtlasSource.new()
var img := Image.create(TILE_SIZE * 2, TILE_SIZE, false, Image.FORMAT_RGBA8)
# Full fog (0,0) — opaque black
img.fill_rect(Rect2i(0, 0, TILE_SIZE, TILE_SIZE), Color(0.02, 0.02, 0.05, 1.0))
# Fog edge (1,0) — semi-transparent dark
img.fill_rect(Rect2i(TILE_SIZE, 0, TILE_SIZE, TILE_SIZE), Color(0.02, 0.02, 0.05, 0.6))
var tex := ImageTexture.create_from_image(img)
source.texture = tex
source.texture_region_size = Vector2i(TILE_SIZE, TILE_SIZE)
source.create_tile(Vector2i(0, 0))
source.create_tile(Vector2i(1, 0))
ts.add_source(source)
tile_set = ts
# Register all known tile positions (called when tile data arrives)
func register_tile_positions(tiles: Array) -> void:
_all_tile_positions.clear()
for tile_data in tiles:
if tile_data.has("x") and tile_data.has("y"):
_all_tile_positions[Vector2i(tile_data.x, tile_data.y)] = true
# Update fog based on visible positions.
# player_pos reserved for future fog-decay tracking (#113).
func update_fog(visible_positions: Dictionary, _player_pos: Vector2) -> void:
if not _initialized:
return
clear()
if _all_tile_positions.is_empty() or visible_positions.is_empty():
return
# Build set of fog-edge positions (8-directional neighbors of visible tiles)
var fog_edge: Dictionary = {}
var neighbors := [
Vector2i(-1, 0), Vector2i(1, 0), Vector2i(0, -1), Vector2i(0, 1),
Vector2i(-1, -1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(1, 1),
]
for pos in visible_positions:
for offset in neighbors:
var neighbor_pos: Vector2i = pos + offset
if not visible_positions.has(neighbor_pos) and _all_tile_positions.has(neighbor_pos):
fog_edge[neighbor_pos] = true
# Place fog tiles on all known positions that aren't visible
for pos in _all_tile_positions:
if visible_positions.has(pos):
continue # Visible — no fog
elif fog_edge.has(pos):
set_cell(pos, 0, Vector2i(1, 0)) # Fog edge — semi-transparent
else:
set_cell(pos, 0, Vector2i(0, 0)) # Full fog — opaque