diff --git a/src/game/engine/tests/integration/test_fog_renderer_consumes_vision.gd b/src/game/engine/tests/integration/test_fog_renderer_consumes_vision.gd new file mode 100644 index 00000000..8af5964a --- /dev/null +++ b/src/game/engine/tests/integration/test_fog_renderer_consumes_vision.gd @@ -0,0 +1,219 @@ +extends GutTest +## p1-60 workstream G: Integration test proving fog_renderer.gd correctly +## consumes per-player tile visibility and produces the expected fog +## polygon state for each of VIS_VISIBLE / VIS_SEEN_STALE / VIS_UNSEEN. +## +## Headless-compatible: fog_renderer.gd is a Node2D that creates Polygon2D +## children under itself — no display server is required to instantiate or +## inspect them. We add the renderer to the test scene tree via +## add_child_autofree so its _ready() runs and EventBus connects normally. + +const FogRendererScript: GDScript = preload( + "res://engine/src/rendering/fog_renderer.gd" +) +const GameMapScript: GDScript = preload("res://engine/src/map/game_map.gd") +const TileScript: GDScript = preload("res://engine/src/map/tile.gd") +const WorldMapVisionScript: GDScript = preload( + "res://engine/scenes/world_map/world_map_vision.gd" +) + +## Mirror world_map_vision.gd constants (asserted below). +const VIS_UNSEEN: int = 0 +const VIS_SEEN_STALE: int = 1 +const VIS_VISIBLE: int = 2 + +const PLAYER_INDEX: int = 0 +const MAP_W: int = 6 +const MAP_H: int = 6 + +## Hand-picked axial coordinates inside the 6x6 grid for each visibility band. +const VISIBLE_CELLS: Array[Vector2i] = [Vector2i(2, 2), Vector2i(3, 2)] +const STALE_CELLS: Array[Vector2i] = [Vector2i(0, 0), Vector2i(1, 0)] + +var _renderer: Node2D = null +var _game_map: RefCounted = null + + +# --------------------------------------------------------------------------- +# Setup +# --------------------------------------------------------------------------- + +func before_each() -> void: + _game_map = _build_grid_map(MAP_W, MAP_H) + _assign_visibilities(_game_map) + + _renderer = FogRendererScript.new() + add_child_autofree(_renderer) + await get_tree().process_frame + _renderer.initialize(_game_map, PLAYER_INDEX) + + +func after_each() -> void: + if is_instance_valid(_renderer): + _renderer.clear() + _renderer = null + _game_map = null + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +func _build_grid_map(w: int, h: int) -> RefCounted: + var gm: RefCounted = GameMapScript.new() + gm.initialize(w, h, 0) # WrapMode.NONE — bounded for predictability + for x: int in w: + for y: int in h: + var pos: Vector2i = Vector2i(x, y) + var tile: TileScript = TileScript.new(pos, "grass") + gm.set_tile(pos, tile) + return gm + + +func _assign_visibilities(gm: RefCounted) -> void: + for pos: Vector2i in VISIBLE_CELLS: + (gm.tiles[pos] as TileScript).set_visibility(PLAYER_INDEX, VIS_VISIBLE) + for pos: Vector2i in STALE_CELLS: + (gm.tiles[pos] as TileScript).set_visibility(PLAYER_INDEX, VIS_SEEN_STALE) + # All other tiles remain default VIS_UNSEEN (0). + + +func _fog_node_for(axial: Vector2i) -> Polygon2D: + var nodes: Dictionary = _renderer._fog_nodes + if not nodes.has(axial): + return null + return nodes[axial] as Polygon2D + + +# --------------------------------------------------------------------------- +# Constant alignment +# --------------------------------------------------------------------------- + +func test_renderer_constants_match_vision_layer() -> void: + assert_eq(FogRendererScript.FOG_UNEXPLORED, WorldMapVisionScript.VIS_UNSEEN, + "fog_renderer.FOG_UNEXPLORED must equal world_map_vision.VIS_UNSEEN") + assert_eq(FogRendererScript.FOG_FOGGED, WorldMapVisionScript.VIS_SEEN_STALE, + "fog_renderer.FOG_FOGGED must equal world_map_vision.VIS_SEEN_STALE") + assert_eq(FogRendererScript.FOG_VISIBLE, WorldMapVisionScript.VIS_VISIBLE, + "fog_renderer.FOG_VISIBLE must equal world_map_vision.VIS_VISIBLE") + + +# --------------------------------------------------------------------------- +# Polygon presence & visibility flag per state +# --------------------------------------------------------------------------- + +func test_initialize_creates_polygon_per_tile() -> void: + ## Every tile in the map gets a fog Polygon2D child (visible ones are + ## hidden via .visible=false, not omitted). + assert_eq(_renderer._fog_nodes.size(), MAP_W * MAP_H, + "fog_renderer must create one Polygon2D per tile in the map") + + +func test_visible_tiles_polygon_is_hidden() -> void: + for pos: Vector2i in VISIBLE_CELLS: + var node: Polygon2D = _fog_node_for(pos) + assert_not_null(node, "missing fog polygon for VISIBLE tile %s" % str(pos)) + assert_false(node.visible, + "VIS_VISIBLE tile %s must have fog polygon hidden (no overlay)" % str(pos)) + + +func test_stale_tiles_polygon_is_visible_and_fogged_color() -> void: + for pos: Vector2i in STALE_CELLS: + var node: Polygon2D = _fog_node_for(pos) + assert_not_null(node, "missing fog polygon for STALE tile %s" % str(pos)) + assert_true(node.visible, + "VIS_SEEN_STALE tile %s must have fog polygon visible" % str(pos)) + # All non-softened vertices should carry the FOGGED_COLOR alpha (0.55). + var has_fogged: bool = _polygon_has_color(node, FogRendererScript.FOGGED_COLOR) + assert_true(has_fogged, + "VIS_SEEN_STALE tile %s must paint at least one vertex with FOGGED_COLOR" % str(pos)) + + +func test_unseen_tiles_polygon_is_visible_and_unexplored_color() -> void: + ## Pick an interior unseen tile away from the visible/stale bands so it + ## won't have any softened (alpha=0) vertices. + var unseen_pos: Vector2i = Vector2i(5, 5) + var node: Polygon2D = _fog_node_for(unseen_pos) + assert_not_null(node, "missing fog polygon for UNSEEN tile %s" % str(unseen_pos)) + assert_true(node.visible, + "VIS_UNSEEN tile %s must have fog polygon visible" % str(unseen_pos)) + var colors: PackedColorArray = node.vertex_colors + assert_eq(colors.size(), 6, "fog polygon must have 6 vertex colors") + for i: int in 6: + var c: Color = colors[i] + assert_eq(c, FogRendererScript.UNEXPLORED_COLOR, + "interior UNSEEN tile vertex %d must be UNEXPLORED_COLOR (no neighbor softening)" % i) + + +# --------------------------------------------------------------------------- +# Edge softening: stale tiles bordering a visible tile fade to alpha 0 +# --------------------------------------------------------------------------- + +func test_stale_tile_adjacent_to_visible_has_softened_vertex() -> void: + ## STALE_CELLS[1] = (1,0) is not adjacent to either visible cell, but we + ## can verify softening by switching (1,0)→VIS_SEEN_STALE adjacency + ## directly: place a stale tile next to a visible one. + ## + ## (2,2) is VIS_VISIBLE. (1,2) (axial west of 2,2) is currently UNSEEN. + ## Mark (1,2) as VIS_SEEN_STALE and push an update through the renderer. + var stale_neighbor: Vector2i = Vector2i(1, 2) + (_game_map.tiles[stale_neighbor] as TileScript).set_visibility( + PLAYER_INDEX, VIS_SEEN_STALE + ) + _renderer.update_tile_fog(stale_neighbor, VIS_SEEN_STALE) + + var node: Polygon2D = _fog_node_for(stale_neighbor) + assert_not_null(node) + assert_true(node.visible, "stale neighbor polygon must remain visible") + + var has_softened: bool = _polygon_has_alpha( + node, FogRendererScript.EDGE_FADE_ALPHA + ) + assert_true(has_softened, + "stale tile bordering a VIS_VISIBLE tile must have at least one vertex softened to alpha=%f" + % FogRendererScript.EDGE_FADE_ALPHA) + + +# --------------------------------------------------------------------------- +# Live update path: update_tile_fog flips a tile from unseen → visible +# --------------------------------------------------------------------------- + +func test_update_tile_fog_unseen_to_visible_hides_polygon() -> void: + var pos: Vector2i = Vector2i(4, 4) + var node: Polygon2D = _fog_node_for(pos) + assert_not_null(node) + assert_true(node.visible, "precondition: tile starts with visible fog polygon") + + (_game_map.tiles[pos] as TileScript).set_visibility(PLAYER_INDEX, VIS_VISIBLE) + _renderer.update_tile_fog(pos, VIS_VISIBLE) + assert_false(node.visible, + "after update_tile_fog(VIS_VISIBLE) the fog polygon must be hidden") + + +func test_update_tile_fog_visible_to_stale_reshows_polygon() -> void: + var pos: Vector2i = VISIBLE_CELLS[0] + var node: Polygon2D = _fog_node_for(pos) + assert_false(node.visible, "precondition: VIS_VISIBLE polygon is hidden") + + (_game_map.tiles[pos] as TileScript).set_visibility(PLAYER_INDEX, VIS_SEEN_STALE) + _renderer.update_tile_fog(pos, VIS_SEEN_STALE) + assert_true(node.visible, + "after VIS_VISIBLE→VIS_SEEN_STALE the fog polygon must reappear") + + +# --------------------------------------------------------------------------- +# Color helpers +# --------------------------------------------------------------------------- + +func _polygon_has_color(node: Polygon2D, c: Color) -> bool: + for v: Color in node.vertex_colors: + if v == c: + return true + return false + + +func _polygon_has_alpha(node: Polygon2D, alpha: float) -> bool: + for v: Color in node.vertex_colors: + if is_equal_approx(v.a, alpha): + return true + return false