fix(ui): render LOS boundary wall tiles through fog without marking explored (#585)

- game_state.gd: add boundary_positions Dictionary field; BoundaryWall tiles from
  visible_tiles go to boundary_positions instead of visible_positions — rendered by
  tile_renderer but not tracked as explored fog memory
- fog_state.gd: update_from_state() writes VIS_FORWARD for boundary_positions so fog
  lifts over margin wall content; boundary tiles excluded from exploration step so they
  don't persist as EXP_EXPLORED when player turns away
- tile_renderer.gd: no changes needed — renders all visible_tiles by type, sector-agnostic
- test_fog_shader.gd: 4 new tests — boundary excluded from visible_positions, tracked in
  boundary_positions, cleared each snapshot, fog lifts to VIS_FORWARD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 20:32:32 +01:00
co-authored by Claude Sonnet 4.6
parent c3abf32185
commit 475280191c
3 changed files with 93 additions and 4 deletions
+73
View File
@@ -268,6 +268,79 @@ func test_game_state_visible_positions_cleared_on_new_snapshot() -> void:
assert_that(GameState.visible_positions.has(Vector2i(10, 10))).is_true()
# -- #585: BoundaryWall tiles — visible in fog, not persistently explored ------
func test_boundary_wall_tiles_not_in_visible_positions() -> void:
## #585: BoundaryWall margin tiles must NOT enter visible_positions.
## They are rendered via tile_renderer (from visible_tiles) but must not
## update the player's fog exploration memory.
GameState.apply_snapshot({
"tick": 1,
"visible_tiles": [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward"},
{"x": 6, "y": 5, "z": 0, "visibility": "BoundaryWall"},
],
})
assert_that(GameState.visible_positions.has(Vector2i(5, 5))).is_true()
assert_that(GameState.visible_positions.has(Vector2i(6, 5))).is_false()
assert_that(GameState.boundary_positions.has(Vector2i(6, 5))).is_true()
func test_boundary_wall_tiles_in_visibility_sectors() -> void:
## BoundaryWall visibility sector is still tracked in visibility_sectors
## (for potential future use — wall coloring, etc.)
GameState.apply_snapshot({
"tick": 1,
"visible_tiles": [
{"x": 3, "y": 3, "z": 0, "visibility": "BoundaryWall"},
],
})
assert_that(GameState.visibility_sectors.has(Vector2i(3, 3))).is_true()
assert_that(GameState.visibility_sectors[Vector2i(3, 3)]).is_equal("BoundaryWall")
func test_boundary_positions_cleared_on_new_snapshot() -> void:
## BoundaryWall positions are cleared each snapshot so stale walls don't persist.
GameState.apply_snapshot({
"tick": 1,
"visible_tiles": [{"x": 7, "y": 7, "z": 0, "visibility": "BoundaryWall"}],
})
assert_that(GameState.boundary_positions.has(Vector2i(7, 7))).is_true()
GameState.apply_snapshot({
"tick": 2,
"visible_tiles": [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}],
})
assert_that(GameState.boundary_positions.has(Vector2i(7, 7))).is_false()
assert_that(GameState.boundary_positions.size()).is_equal(0)
func test_boundary_wall_fog_vis_forward() -> void:
## #585: BoundaryWall tiles must lift fog (VIS_FORWARD = 255) so wall content composites.
## visible_positions excludes boundary tiles; fog_state writes vis bytes for them separately.
## Reads _vis_bytes directly (packed byte array) to avoid ImageTexture.get_image() lag.
var fog_state = _get_fog_state()
if fog_state == null:
return
GameState.apply_snapshot({
"tick": 1,
"visible_tiles": [
{"x": 0, "y": 0, "z": 0, "visibility": "Forward"}, # normal LOS tile
{"x": 1, "y": 0, "z": 0, "visibility": "BoundaryWall"}, # margin tile
],
})
fog_state.update_from_state()
var ox: int = fog_state.map_bounds.position.x
var oy: int = fog_state.map_bounds.position.y
var w: int = fog_state.map_bounds.size.x
var vis: PackedByteArray = fog_state._vis_bytes
var normal_idx: int = (0 - oy) * w + (0 - ox)
var boundary_idx: int = (0 - oy) * w + (1 - ox)
assert_int(vis[normal_idx]).is_equal(FogState.VIS_FORWARD) # normal tile: VIS_FORWARD
assert_int(vis[boundary_idx]).is_equal(FogState.VIS_FORWARD) # boundary also fog-lifted
GameState.visible_positions.clear()
GameState.boundary_positions.clear()
# -- Z-layer compliance (D-049) -----------------------------------------------
func test_fog_overlay_z_layer() -> void: