test(client): add BoundaryWall fog tests and document tile_renderer behavior (#585)

4 new tests in test_fog_sprint22.gd verify BoundaryWall tiles populate
boundary_positions (not visible_positions), get VIS_FORWARD without
EXP_VISIBLE, stay EXP_UNEXPLORED after leaving LOS, and clear on new
snapshot. Comment in tile_renderer.gd documents implicit rendering path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 20:33:08 +01:00
co-authored by Claude Opus 4.6
parent 475280191c
commit 33b26c1a15
2 changed files with 142 additions and 1 deletions
+7 -1
View File
@@ -64,11 +64,17 @@ func _setup_tileset() -> void:
tile_set = ts
# Update tiles from snapshot data
# tiles: Array of {x: int, y: int, z: int, type: String}
# tiles: Array of {x: int, y: int, z: int, type: String, visibility: String (optional)}
# z here is the server-side FLOOR LEVEL (0 = ground, 1 = first floor, etc.),
# NOT the Godot scene z_index (which controls render order within a floor).
# This node only renders floor-level 0. Higher floor levels will be handled
# by separate TileMapLayer nodes when multi-floor rendering is implemented.
#
# BoundaryWall tiles (#585): visibility="BoundaryWall" tiles (wall tiles 1 step beyond
# LOS boundary) are rendered normally here — they have a "type" field from protocol.gd
# so they composite correctly under the fog shader. The fog/exploration exemption is
# handled in fog_state.gd (VIS_FORWARD without EXP_VISIBLE) and game_state.gd
# (boundary_positions not visible_positions). No special handling needed in this method.
func update_tiles(tiles: Array) -> void:
if not _initialized:
return
+135
View File
@@ -24,12 +24,14 @@ func before_test() -> void:
GameState.visible_positions.clear()
GameState.visible_tiles.clear()
GameState.visibility_sectors.clear()
GameState.boundary_positions.clear()
func after_test() -> void:
GameState.visible_positions.clear()
GameState.visible_tiles.clear()
GameState.visibility_sectors.clear()
GameState.boundary_positions.clear()
# -- Spec constants (D-059) ---------------------------------------------------
@@ -489,6 +491,139 @@ func test_visible_positions_cleared_on_new_snapshot() -> void:
assert_bool(GameState.visible_positions.has(Vector2i(10, 10))).is_true()
# -- Sprint 23: BoundaryWall handling (#585) ----------------------------------
func test_boundary_positions_populated_from_snapshot() -> void:
# #585: BoundaryWall tiles go to boundary_positions (not visible_positions).
# Fog lifts for boundary wall tiles so wall content composites correctly.
GameState.apply_snapshot({
"tick": 20,
"visible_tiles": [
{"x": 10, "y": 10, "z": 0, "visibility": "Forward", "type": "floor"},
{"x": 11, "y": 10, "z": 0, "visibility": "BoundaryWall", "type": "wall"},
],
})
assert_bool(GameState.visible_positions.has(Vector2i(10, 10))).override_failure_message(
"Forward tile must be in visible_positions"
).is_true()
assert_bool(GameState.visible_positions.has(Vector2i(11, 10))).override_failure_message(
"BoundaryWall tile must NOT be in visible_positions (#585)"
).is_false()
assert_bool(GameState.boundary_positions.has(Vector2i(11, 10))).override_failure_message(
"BoundaryWall tile must be in boundary_positions (#585)"
).is_true()
func test_boundary_wall_vis_forward_not_exp_visible() -> void:
# #585: BoundaryWall tiles get VIS_FORWARD (fog lifted) but NOT EXP_VISIBLE.
# They render through fog but are not stored as exploration memory.
var fog_state = _get_fog_state()
if fog_state == null:
return
if not fog_state.has_method("update_from_state"):
return
GameState.visible_positions = {Vector2i(5, 5): true}
GameState.boundary_positions = {Vector2i(6, 5): true}
GameState.visible_tiles = [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward", "type": "floor"},
{"x": 6, "y": 5, "z": 0, "visibility": "BoundaryWall", "type": "wall"},
]
fog_state.update_from_state()
var vis_bytes = fog_state.get("_vis_bytes")
var exp_bytes = fog_state.get("_exp_bytes")
if vis_bytes == null or exp_bytes == null:
push_warning("TestFogSprint22: byte arrays not accessible — skipped")
return
var ox: int = fog_state.map_bounds.position.x
var oy: int = fog_state.map_bounds.position.y
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
if w <= 0:
return
var px := 6 - ox
var py := 5 - oy
if px < 0 or py < 0 or px >= w:
push_warning("TestFogSprint22: boundary tile (6,5) out of bounds — skipped")
return
var idx := py * w + px
if idx < 0 or idx >= vis_bytes.size():
return
assert_int(vis_bytes[idx]).override_failure_message(
"BoundaryWall tile must have VIS_FORWARD — fog must lift to composite wall content (#585)"
).is_equal(fog_state.VIS_FORWARD)
assert_int(exp_bytes[idx]).override_failure_message(
"BoundaryWall tile must NOT be EXP_VISIBLE — it is not explored memory (#585)"
).is_not_equal(fog_state.EXP_VISIBLE)
func test_boundary_wall_stays_unexplored_after_leaving_los() -> void:
# #585: When BoundaryWall tile leaves LOS, it must NOT decay to EXP_EXPLORED.
# Normal LOS tiles decay to EXP_EXPLORED when they leave LOS.
# Boundary tiles must stay EXP_UNEXPLORED — they were never explored.
var fog_state = _get_fog_state()
if fog_state == null:
return
if not fog_state.has_method("update_from_state"):
return
# Frame 1: BoundaryWall at (6,5) is visible
GameState.visible_positions = {Vector2i(5, 5): true}
GameState.boundary_positions = {Vector2i(6, 5): true}
GameState.visible_tiles = [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward", "type": "floor"},
{"x": 6, "y": 5, "z": 0, "visibility": "BoundaryWall", "type": "wall"},
]
fog_state.update_from_state()
# Frame 2: both leave LOS
GameState.visible_positions.clear()
GameState.boundary_positions.clear()
GameState.visible_tiles = []
fog_state.update_from_state()
var exp_bytes = fog_state.get("_exp_bytes")
if exp_bytes == null:
return
var ox: int = fog_state.map_bounds.position.x
var oy: int = fog_state.map_bounds.position.y
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
if w <= 0:
return
var px := 6 - ox
var py := 5 - oy
if px >= 0 and py >= 0 and px < w:
var idx := py * w + px
if idx >= 0 and idx < exp_bytes.size():
assert_int(exp_bytes[idx]).override_failure_message(
"BoundaryWall tile must stay EXP_UNEXPLORED after leaving LOS (#585 — not explored memory)"
).is_equal(fog_state.EXP_UNEXPLORED)
func test_boundary_wall_cleared_on_new_snapshot() -> void:
# #585: boundary_positions must be cleared each tick — old walls must not persist.
# BoundaryWall positions shift as the player moves; stale positions would lift fog
# where no wall exists.
GameState.apply_snapshot({
"tick": 30,
"visible_tiles": [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward", "type": "floor"},
{"x": 6, "y": 5, "z": 0, "visibility": "BoundaryWall", "type": "wall"},
],
})
assert_bool(GameState.boundary_positions.has(Vector2i(6, 5))).is_true()
GameState.apply_snapshot({
"tick": 31,
"visible_tiles": [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward", "type": "floor"},
],
})
assert_bool(GameState.boundary_positions.has(Vector2i(6, 5))).override_failure_message(
"Stale BoundaryWall position must be cleared on next snapshot (#585)"
).is_false()
# -- Performance (D-059) -------------------------------------------------------
func test_fog_state_update_under_2ms_for_400_tiles() -> void: