Critical fixes: - fog_state: guard _compute_bounds() against all-invalid tiles (negative Rect2i crash) - fog_shader: read Camera2D zoom dynamically instead of hardcoded Vector2(2,2) - world_radial: set custom_minimum_size in _ready() from spoke geometry Warnings: - fog.gdshader: tighten PERIPHERAL_LOW 0.15→0.55 to match D-059 peripheral band - Extract color_for_entity_kind() to Constants.gd, decouple CursorRenderer from EntityRenderer Documentation: shallow copy assumption, gradual decay TODO, tween guard rationale, fade timing rationale, ToggleInsert TODO, monologue consume-once, hover offset safety, v6 fixture gap TODO. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
119 lines
3.9 KiB
GDScript
119 lines
3.9 KiB
GDScript
extends Node
|
|
|
|
## Fog texture state — visibility/exploration/zone tint images updated from GameState.
|
|
## Read by fog_shader.gd for shader uniforms. Not a renderer — pure data.
|
|
## Architecture: docs/architecture/fog-shader-spec.md | D-059
|
|
|
|
var map_bounds: Rect2i = Rect2i(0, 0, 1, 1)
|
|
var visibility_texture: ImageTexture
|
|
var exploration_texture: ImageTexture
|
|
var zone_tint_texture: ImageTexture
|
|
|
|
var _vis_bytes: PackedByteArray
|
|
var _exp_bytes: PackedByteArray
|
|
var _vis_image: Image
|
|
var _exp_image: Image
|
|
var _tint_image: Image
|
|
var _width: int = 1
|
|
var _height: int = 1
|
|
var _prev_visible: Dictionary = {} # Tiles visible last frame (for incremental decay)
|
|
|
|
|
|
func _ready() -> void:
|
|
_resize(Rect2i(0, 0, 64, 64))
|
|
|
|
|
|
func _resize(bounds: Rect2i) -> void:
|
|
map_bounds = bounds
|
|
_width = maxi(bounds.size.x, 1)
|
|
_height = maxi(bounds.size.y, 1)
|
|
var sz := _width * _height
|
|
|
|
_vis_bytes = PackedByteArray()
|
|
_vis_bytes.resize(sz)
|
|
_vis_bytes.fill(0)
|
|
_vis_image = Image.create_from_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
|
|
visibility_texture = ImageTexture.create_from_image(_vis_image)
|
|
|
|
_exp_bytes = PackedByteArray()
|
|
_exp_bytes.resize(sz)
|
|
_exp_bytes.fill(0)
|
|
_exp_image = Image.create_from_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
|
exploration_texture = ImageTexture.create_from_image(_exp_image)
|
|
|
|
# Zone tint — neutral dark for Sprint 6 (zone metadata deferred)
|
|
_tint_image = Image.create(_width, _height, false, Image.FORMAT_RGB8)
|
|
_tint_image.fill(Color(0.05, 0.05, 0.08))
|
|
zone_tint_texture = ImageTexture.create_from_image(_tint_image)
|
|
|
|
_prev_visible.clear()
|
|
|
|
|
|
func update_from_state() -> void:
|
|
# Resize if map bounds changed
|
|
var tiles := GameState.visible_tiles
|
|
if tiles.size() > 0:
|
|
var new_bounds := _compute_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 255→128
|
|
|
|
# 1. Clear visibility, then write current LOS
|
|
_vis_bytes.fill(0)
|
|
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] = 255 if sector == "Forward" else 180
|
|
_vis_image.set_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
|
|
visibility_texture.update(_vis_image)
|
|
|
|
# 2. Exploration: tiles leaving LOS decay to 128, visible tiles stay 255
|
|
# Only touch tiles that changed (O(visible) not O(map_size))
|
|
for pos in _prev_visible:
|
|
if not positions.has(pos):
|
|
var px: int = pos.x - ox
|
|
var py: int = pos.y - oy
|
|
if px >= 0 and py >= 0 and px < _width and py < _height:
|
|
var idx: int = py * _width + px
|
|
if _exp_bytes[idx] > 128:
|
|
_exp_bytes[idx] = 128
|
|
|
|
for pos in positions:
|
|
var px: int = pos.x - ox
|
|
var py: int = pos.y - oy
|
|
if px >= 0 and py >= 0 and px < _width and py < _height:
|
|
_exp_bytes[py * _width + px] = 255
|
|
_exp_image.set_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
|
exploration_texture.update(_exp_image)
|
|
|
|
# Shallow copy — correct for Dictionary<Vector2i, bool/String> values
|
|
_prev_visible = positions.duplicate()
|
|
|
|
|
|
func _compute_bounds(tiles: Array) -> Rect2i:
|
|
var min_x := 999999
|
|
var min_y := 999999
|
|
var max_x := -999999
|
|
var max_y := -999999
|
|
for tile in tiles:
|
|
if not tile is Dictionary or not tile.has("x") or not tile.has("y"):
|
|
continue
|
|
min_x = mini(min_x, int(tile.x))
|
|
min_y = mini(min_y, int(tile.y))
|
|
max_x = maxi(max_x, int(tile.x))
|
|
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)
|
|
# 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)
|