## Sprint 22 — Fog system acceptance tests (#569) ## ## Validates FogState data management against the Sprint 22 acceptance criteria: ## - Explored tiles never revert to unexplored black (EXP_EXPLORED persistence) ## - Bounds grow-only invariant (explored tiles behind player stay in texture) ## - All visible tiles written as Forward (server simplified to Forward-only) ## - Exploration data survives texture resize (grow-only bounds copy) ## - Shader file present with correct fog_alpha constant ## ## Spec: D-059 (fog shader), D-015 (vision cone), D-066 (dual-scale grid, 6-8 tile gradient) ## Ticket: #569 class_name TestFogSprint22 extends GdUnitTestSuite func _get_fog_state() -> Node: var node = get_node_or_null("/root/FogState") if node == null: push_warning("TestFogSprint22: FogState autoload not found — test skipped (awaiting #569)") return node 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) --------------------------------------------------- func test_exp_explored_constant_is_128() -> void: # EXP_EXPLORED = 128 → shader reads this as ~0.502. # smoothstep(0.0, 0.2, 0.502) = 1.0 → exp_fade fully applied. # If EXP_EXPLORED were 0, explored tiles would render as solid unexplored black. var fog_state = _get_fog_state() if fog_state == null: return assert_int(fog_state.EXP_EXPLORED).override_failure_message( "EXP_EXPLORED must be 128 — shader exp_fade requires explored value > 0.2 to avoid unexplored-black rendering" ).is_equal(128) func test_exp_unexplored_constant_is_0() -> void: var fog_state = _get_fog_state() if fog_state == null: return assert_int(fog_state.EXP_UNEXPLORED).is_equal(0) func test_exp_visible_constant_is_255() -> void: # EXP_VISIBLE = 255 → shader reads 1.0, full art visibility (currently in LOS) var fog_state = _get_fog_state() if fog_state == null: return assert_int(fog_state.EXP_VISIBLE).is_equal(255) func test_vis_forward_constant_is_255() -> void: # D-059: VIS_FORWARD = 255 → clear vision, nearly transparent fog overlay var fog_state = _get_fog_state() if fog_state == null: return assert_int(fog_state.VIS_FORWARD).is_equal(255) func test_vis_hidden_constant_is_0() -> void: # D-059: VIS_HIDDEN = 0 → no vision, fog fully opaque var fog_state = _get_fog_state() if fog_state == null: return assert_int(fog_state.VIS_HIDDEN).is_equal(0) func test_unexplored_color_spec_value() -> void: # D-059: Unexplored = solid near-black #12141a # Verify the hex value decodes to the expected channel values. var c := Color("#12141a") assert_float(c.r).is_equal_approx(18.0 / 255.0, 0.003) assert_float(c.g).is_equal_approx(20.0 / 255.0, 0.003) assert_float(c.b).is_equal_approx(26.0 / 255.0, 0.003) # Sanity: it IS very dark (all channels < 0.12) assert_float(c.r).is_less(0.12) assert_float(c.g).is_less(0.12) assert_float(c.b).is_less(0.12) # -- Acceptance: explored tiles persist after leaving LOS (criterion 3) ------ func test_explored_tile_becomes_exp_explored_after_leaving_los() -> void: # ACCEPTANCE CRITERION: "Explored tiles never revert to unexplored black" # When tile (5,5) was in LOS (frame 1) and then leaves LOS (frame 2), # its exploration byte must be EXP_EXPLORED (128), not EXP_UNEXPLORED (0). var fog_state = _get_fog_state() if fog_state == null: return if not fog_state.has_method("update_from_state"): push_warning("TestFogSprint22: update_from_state missing — skipped") return # Frame 1: tile (5,5) is visible GameState.visible_positions = {Vector2i(5, 5): true} GameState.visible_tiles = [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}] fog_state.update_from_state() # Frame 2: tile (5,5) leaves LOS GameState.visible_positions.clear() GameState.visible_tiles = [] fog_state.update_from_state() # Internal state check: _exp_bytes[tile(5,5)] must be EXP_EXPLORED (128) var exp_bytes = fog_state.get("_exp_bytes") if exp_bytes == null: push_warning("TestFogSprint22: _exp_bytes not accessible — data path untestable headlessly") 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: push_warning("TestFogSprint22: _width inaccessible — data path untestable") return var px := 5 - ox var py := 5 - oy if px < 0 or py < 0 or px >= w: push_warning("TestFogSprint22: tile (5,5) out of bounds after update — check grow_bounds margin") return var idx := py * w + px if idx < 0 or idx >= exp_bytes.size(): push_warning("TestFogSprint22: idx %d out of exp_bytes range %d" % [idx, exp_bytes.size()]) return assert_int(exp_bytes[idx]).override_failure_message( "Tile (5,5) must be EXP_EXPLORED=128 after leaving LOS — not EXP_UNEXPLORED=0 (#569 regression)" ).is_equal(fog_state.EXP_EXPLORED) func test_explored_tile_is_exp_visible_while_in_los() -> void: # While in LOS, tile exploration byte must be EXP_VISIBLE (255) 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(3, 3): true} GameState.visible_tiles = [{"x": 3, "y": 3, "z": 0, "visibility": "Forward"}] 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 := 3 - ox var py := 3 - oy if px < 0 or py < 0 or px >= w: return var idx := py * w + px if idx >= 0 and idx < exp_bytes.size(): assert_int(exp_bytes[idx]).is_equal(fog_state.EXP_VISIBLE) func test_unexplored_tile_stays_exp_unexplored() -> void: # Tile (7, 8) was never seen — must remain EXP_UNEXPLORED (0) var fog_state = _get_fog_state() if fog_state == null: return if not fog_state.has_method("update_from_state"): return # See only (5, 5) — tile (7, 8) is not in LOS GameState.visible_positions = {Vector2i(5, 5): true} GameState.visible_tiles = [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}] 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 := 7 - ox var py := 8 - oy if px < 0 or py < 0 or px >= w: return var idx := py * w + px if idx >= 0 and idx < exp_bytes.size(): assert_int(exp_bytes[idx]).is_equal(fog_state.EXP_UNEXPLORED) # -- Acceptance: bounds grow-only invariant ------------------------------------ func test_bounds_never_shrink() -> void: # ACCEPTANCE CRITERION: "Explored tiles never revert to unexplored black" # Requires grow-only bounds: once a tile is in the texture, it stays there. var fog_state = _get_fog_state() if fog_state == null: return if not fog_state.has_method("update_from_state"): return # Frame 1: see (10, 10) → establishes initial bounds GameState.visible_positions = {Vector2i(10, 10): true} GameState.visible_tiles = [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}] fog_state.update_from_state() var b1: Rect2i = fog_state.map_bounds # Frame 2: see (30, 30) → bounds must expand to include both GameState.visible_positions = {Vector2i(30, 30): true} GameState.visible_tiles = [{"x": 30, "y": 30, "z": 0, "visibility": "Forward"}] fog_state.update_from_state() var b2: Rect2i = fog_state.map_bounds # Frame 3: back to (10, 10) → bounds must NOT shrink GameState.visible_positions = {Vector2i(10, 10): true} GameState.visible_tiles = [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}] fog_state.update_from_state() var b3: Rect2i = fog_state.map_bounds assert_bool(b2.size.x >= b1.size.x).override_failure_message( "Bounds must grow when player moves to larger region" ).is_true() assert_bool(b2.size.y >= b1.size.y).is_true() assert_bool(b3.size.x >= b2.size.x).override_failure_message( "Bounds must not shrink when player returns to previous position (grow-only invariant)" ).is_true() assert_bool(b3.size.y >= b2.size.y).is_true() func test_bounds_include_margin_for_gradient_bleed() -> void: # D-066: 6-8 tile gradient at cone edge requires texture margin. # _grow_bounds adds 8-tile margin on each side (accommodates 7x7 Gaussian # kernel at 2-texel intervals = ±6 tile reach). After seeing (10,10), # bounds should extend at least 4 tiles beyond the visible tile. 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(10, 10): true} GameState.visible_tiles = [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}] fog_state.update_from_state() var b: Rect2i = fog_state.map_bounds # With 4-tile margin: bounds.position.x <= 10 - 4 = 6 assert_bool(b.position.x <= 6).override_failure_message( "FogState bounds must include 4-tile margin for gradient bleed (D-066 gradient spec)" ).is_true() assert_bool(b.position.y <= 6).is_true() # -- Acceptance: Forward-only visibility (Sprint 22 server simplification) ---- func test_visible_tiles_written_as_vis_forward() -> void: # Sprint 22: server sends only Forward tiles (Peripheral sector removed). # FogState writes VIS_FORWARD (255) for all tiles in visible_positions. 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, Vector2i(6, 5): true} GameState.visible_tiles = [ {"x": 5, "y": 5, "z": 0, "visibility": "Forward"}, {"x": 6, "y": 5, "z": 0, "visibility": "Forward"}, ] fog_state.update_from_state() var vis_bytes = fog_state.get("_vis_bytes") if vis_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 for pos in [Vector2i(5, 5), Vector2i(6, 5)]: var px := pos.x - ox var py := pos.y - oy if px < 0 or py < 0 or px >= w: continue var idx := py * w + px if idx >= 0 and idx < vis_bytes.size(): assert_int(vis_bytes[idx]).override_failure_message( "All visible tiles should be VIS_FORWARD=255 — server is Forward-only in Sprint 22" ).is_equal(fog_state.VIS_FORWARD) func test_tiles_outside_los_written_as_vis_hidden() -> void: # Tiles in bounds but not in visible_positions must be VIS_HIDDEN (0) 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.visible_tiles = [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}] fog_state.update_from_state() # (5, 7) is inside the padded bounds but not visible — must be VIS_HIDDEN var vis_bytes = fog_state.get("_vis_bytes") if vis_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 := 5 - ox var py := 7 - oy if px >= 0 and py >= 0 and px < w: var idx := py * w + px if idx >= 0 and idx < vis_bytes.size(): assert_int(vis_bytes[idx]).is_equal(fog_state.VIS_HIDDEN) # -- Acceptance: exploration survives texture resize -------------------------- func test_exploration_data_preserved_across_bounds_growth() -> void: # D-059: Texture resize must copy old exploration bytes into new texture. # Without this, tiles seen before a resize appear as EXP_UNEXPLORED (black). var fog_state = _get_fog_state() if fog_state == null: return if not fog_state.has_method("update_from_state"): return # Frame 1: see (5, 5), then leave GameState.visible_positions = {Vector2i(5, 5): true} GameState.visible_tiles = [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}] fog_state.update_from_state() GameState.visible_positions.clear() GameState.visible_tiles = [] fog_state.update_from_state() # (5,5) → EXP_EXPLORED # Frame 2: move far away — forces bounds growth (resize) GameState.visible_positions = {Vector2i(80, 80): true} GameState.visible_tiles = [{"x": 80, "y": 80, "z": 0, "visibility": "Forward"}] fog_state.update_from_state() # (5,5) must still be EXP_EXPLORED after the resize 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 := 5 - ox var py := 5 - oy if px < 0 or py < 0 or px >= w: push_warning("TestFogSprint22: (5,5) not in bounds after resize — is copy-on-resize working?") return var idx := py * w + px if idx >= 0 and idx < exp_bytes.size(): assert_int(exp_bytes[idx]).override_failure_message( "Exploration data at (5,5) must survive bounds growth — EXP_EXPLORED (128) expected after resize" ).is_greater_equal(fog_state.EXP_EXPLORED) # -- Shader file checks (D-059) ----------------------------------------------- func test_fog_gdshader_exists() -> void: assert_bool(ResourceLoader.exists("res://shaders/fog.gdshader")).override_failure_message( "fog.gdshader must exist — fog rendering requires this shader file (#569)" ).is_true() func test_fog_shader_defines_fog_alpha() -> void: # D-059: explored fog overlay must be ~25-30% opacity. # fog_alpha constant controls this. Verify the shader defines it. if not ResourceLoader.exists("res://shaders/fog.gdshader"): push_warning("TestFogSprint22: fog.gdshader not found — shader check skipped") return var source := FileAccess.get_file_as_string("res://shaders/fog.gdshader") if source.is_empty(): push_warning("TestFogSprint22: fog.gdshader is empty or unreadable") return assert_bool(source.contains("fog_alpha")).override_failure_message( "fog.gdshader must define fog_alpha for the 25-30%% explored-tile overlay (D-059)" ).is_true() func test_fog_shader_defines_smoothstep_clarity_ramp() -> void: # D-059/D-066: smooth gradient requires a clarity ramp (smoothstep). # The blurred visibility → clarity ramp must use smoothstep for smooth gradients. if not ResourceLoader.exists("res://shaders/fog.gdshader"): push_warning("TestFogSprint22: fog.gdshader not found — skipped") return var source := FileAccess.get_file_as_string("res://shaders/fog.gdshader") if source.is_empty(): return assert_bool(source.contains("smoothstep")).override_failure_message( "fog.gdshader must use smoothstep for the clarity ramp — hard steps violate D-066 gradient spec" ).is_true() func test_fog_shader_defines_unexplored_color() -> void: # D-059: unexplored = solid near-black #12141a. if not ResourceLoader.exists("res://shaders/fog.gdshader"): push_warning("TestFogSprint22: fog.gdshader not found — skipped") return var source := FileAccess.get_file_as_string("res://shaders/fog.gdshader") if source.is_empty(): return assert_bool(source.contains("UNEXPLORED_COLOR")).override_failure_message( "fog.gdshader must define UNEXPLORED_COLOR constant (D-059 #12141a spec)" ).is_true() func test_fog_shader_uses_gaussian_blur_for_gradient() -> void: # D-066: 6-8 tile soft gradient requires Gaussian blur on visibility texture. # Current implementation: 7x7 kernel at 2-texel intervals (±6 tiles), sigma 2.0 # in kernel space = 4.0 tiles effective. At 2-sigma (8 tiles), weight drops to 0.14. # This covers the D-066 "6-8 tile" gradient spec. if not ResourceLoader.exists("res://shaders/fog.gdshader"): push_warning("TestFogSprint22: fog.gdshader not found — skipped") return var source := FileAccess.get_file_as_string("res://shaders/fog.gdshader") if source.is_empty(): return # 7x7 Gaussian uses dy from -3 to 3 assert_bool(source.contains("sample_visibility")).override_failure_message( "fog.gdshader must call sample_visibility() for Gaussian-blurred visibility (D-066 gradient)" ).is_true() assert_bool(source.contains("-3.0")).override_failure_message( "fog.gdshader sample_visibility must use 7x7 kernel (±3 tiles) for 6-tile gradient coverage (D-066)" ).is_true() # -- Regression: GameState visible_positions (existing contract) --------------- func test_visible_positions_derived_from_visible_tiles_in_server_mode() -> void: # D-020: In real server mode, visible_positions derives from visible_tiles. # Fog rendering depends on this derivation being correct. GameState.apply_snapshot({ "tick": 10, "visible_tiles": [ {"x": 7, "y": 7, "z": 0, "visibility": "Forward"}, {"x": 8, "y": 7, "z": 0, "visibility": "Forward"}, ], }) assert_bool(GameState.visible_positions.has(Vector2i(7, 7))).override_failure_message( "visible_positions must be derived from visible_tiles when no explicit visible_positions key" ).is_true() assert_bool(GameState.visible_positions.has(Vector2i(8, 7))).is_true() func test_visibility_sectors_populated_forward_only() -> void: # D-015: visibility_sectors must be populated from visible_tiles. # In Forward-only mode, all sectors are "Forward". GameState.apply_snapshot({ "tick": 11, "visible_tiles": [ {"x": 4, "y": 4, "z": 0, "visibility": "Forward"}, ], }) assert_bool(GameState.visibility_sectors.has(Vector2i(4, 4))).is_true() assert_str(GameState.visibility_sectors[Vector2i(4, 4)]).is_equal("Forward") func test_visible_positions_cleared_on_new_snapshot() -> void: # Old positions from tick N must not persist to tick N+1 GameState.apply_snapshot({ "tick": 1, "visible_tiles": [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}], }) assert_int(GameState.visible_positions.size()).is_equal(1) GameState.apply_snapshot({ "tick": 2, "visible_tiles": [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}], }) assert_bool(GameState.visible_positions.has(Vector2i(5, 5))).override_failure_message( "Old visible positions must be cleared when new visible_tiles arrive" ).is_false() 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: # D-059: <1ms/frame CPU budget for fog update. Allow 2x margin for test env. var fog_state = _get_fog_state() if fog_state == null: return if not fog_state.has_method("update_from_state"): return var positions: Dictionary = {} var tiles: Array = [] for x in range(20): for y in range(20): positions[Vector2i(x, y)] = true tiles.append({"x": x, "y": y, "z": 0, "visibility": "Forward"}) GameState.visible_positions = positions GameState.visible_tiles = tiles var start := Time.get_ticks_usec() fog_state.update_from_state() var elapsed_ms := (Time.get_ticks_usec() - start) / 1000.0 assert_float(elapsed_ms).override_failure_message( "FogState.update_from_state() must complete in <2ms for 400 tiles (spec: <1ms D-059)" ).is_less(2.0)