From c22b497a2760c808c2b86e100f7e11d288e45437 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 6 Jun 2026 03:30:58 -0700 Subject: [PATCH] fix(fog): fully hide undiscovered tiles on map + minimap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Undiscovered (never-seen) terrain was leaking through fog on both the main map and minimap; only the lit(visible) vs unlit(seen) distinction worked. Root cause: unexplored overlay was sub-opaque + undersized. - fog_renderer.gd: UNEXPLORED_COLOR alpha 0.85 -> 1.0 (opaque); edge-fade softening now gated to the FOGGED state only — unexplored tiles stay fully opaque even at vertices bordering a visible tile (was revealing undiscovered terrain along the exploration frontier). - design-tokens.json -> regenerated ui_theme.tres: fog.unexplored alpha 0.90 -> 1.0 (fixed at token source, not hand-edited). - minimap.gd: unexplored now drawn as a full tile-pitch opaque cover instead of a 3x3px dot (the dot left ~5px gaps at minimap scale, leaking terrain). fog.explored (seen dimming) left unchanged. Verified on apricot via iter_7q_worldmap_visual_proof with fog ENABLED: undiscovered renders solid black on map + minimap, clean hard frontier, lit tiles unaffected. (GUT cannot prove this — render-verified.) Co-Authored-By: Claude Opus 4.8 --- .project/designs/design-tokens.json | 4 +- public/games/age-of-dwarves/ui_theme.tres | 2 +- src/game/engine/scenes/hud/minimap.gd | 51 ++++++++++++------- src/game/engine/src/rendering/fog_renderer.gd | 14 +++-- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/.project/designs/design-tokens.json b/.project/designs/design-tokens.json index 7269a2e0..45710721 100644 --- a/.project/designs/design-tokens.json +++ b/.project/designs/design-tokens.json @@ -267,9 +267,9 @@ "$description": "Seen but not currently visible tiles — 70% black" }, "unexplored": { - "$value": "#000000e5", + "$value": "#000000ff", "$type": "color", - "$description": "Never-seen tiles — 90% black" + "$description": "Never-seen tiles — fully opaque black (undiscovered terrain must never be visible on map or minimap)" } }, "player": { diff --git a/public/games/age-of-dwarves/ui_theme.tres b/public/games/age-of-dwarves/ui_theme.tres index a33fabc7..76d4a0c1 100644 --- a/public/games/age-of-dwarves/ui_theme.tres +++ b/public/games/age-of-dwarves/ui_theme.tres @@ -102,7 +102,7 @@ corner_radius_bottom_left = 2 corner_radius_bottom_right = 2 [resource] -metadata/tokens = "{\"accent.gold\":\"d9a020\",\"accent.goldBright\":\"d9b33f\",\"accent.goldPress\":\"ffd14d\",\"accent.goldResource\":\"f2d133\",\"accent.ping\":\"ffd973\",\"accent.sage\":\"66b866\",\"accent.science\":\"66bfff\",\"background.base\":\"1a1410\",\"background.deepest\":\"171219\",\"background.happiness\":\"0f0d07\",\"background.hud\":\"00000099\",\"background.list\":\"120e1e\",\"background.listSelected\":\"3f2d0d\",\"background.menu\":\"0e0a17\",\"background.overlay\":\"0000009e\",\"background.panel\":\"17121e\",\"background.raised\":\"2a2018\",\"background.surface\":\"221a14\",\"border.divider\":\"99731f80\",\"border.focus\":\"d9b340ff\",\"border.happiness\":\"b39940d9\",\"border.list\":\"4d4014b2\",\"border.listSelected\":\"d9b340cc\",\"border.panel\":\"73591fcc\",\"button.bgHover\":\"331a0d\",\"button.bgNormal\":\"1f1733\",\"button.bgPressed\":\"472f0f\",\"climate.cold\":\"1a4dff\",\"climate.hot\":\"ff260d\",\"climate.textCold\":\"66b3ff\",\"climate.textNeutral\":\"d9e0d9\",\"climate.textWarming\":\"ff731a\",\"climate.warm\":\"26cc40\",\"fog.explored\":\"000000b2\",\"fog.unexplored\":\"000000e5\",\"guide.bgPrimary\":\"1a1410\",\"guide.bgSecondary\":\"221a14\",\"guide.bgTertiary\":\"2a2018\",\"guide.dwarfAccent\":\"8b6a1a\",\"guide.dwarfPrimary\":\"c07040\",\"guide.dwarfPrimaryDark\":\"8a4a28\",\"guide.dwarfPrimaryLight\":\"e09868\",\"guide.textMuted\":\"7a6048\",\"guide.textPrimary\":\"f0e4d0\",\"guide.textSecondary\":\"b8a078\",\"player.blue\":\"3366ff\",\"player.brown\":\"806659\",\"player.cyan\":\"1accd9\",\"player.gray\":\"999999\",\"player.green\":\"33cc4d\",\"player.magenta\":\"cc4d80\",\"player.navy\":\"4d4d99\",\"player.orange\":\"e6801a\",\"player.purple\":\"b24de6\",\"player.red\":\"e63333\",\"player.sage\":\"66b366\",\"player.yellow\":\"e6cc1a\",\"semantic.diplomacy\":\"e68c73\",\"semantic.goldenAge\":\"ffeb66\",\"semantic.negative\":\"d95940\",\"semantic.positive\":\"66e666\",\"semantic.trade\":\"ccbf73\",\"semantic.warning\":\"e69933\",\"text.button\":\"e0d199\",\"text.buttonHover\":\"ffeb80\",\"text.buttonPressed\":\"ffffb3\",\"text.disabled\":\"80806680\",\"text.muted\":\"b2b2b2\",\"text.primary\":\"e0d8c8\",\"text.secondary\":\"bfb7a6\",\"text.title\":\"f2d973\"}" +metadata/tokens = "{\"accent.gold\":\"d9a020\",\"accent.goldBright\":\"d9b33f\",\"accent.goldPress\":\"ffd14d\",\"accent.goldResource\":\"f2d133\",\"accent.ping\":\"ffd973\",\"accent.sage\":\"66b866\",\"accent.science\":\"66bfff\",\"background.base\":\"1a1410\",\"background.deepest\":\"171219\",\"background.happiness\":\"0f0d07\",\"background.hud\":\"00000099\",\"background.list\":\"120e1e\",\"background.listSelected\":\"3f2d0d\",\"background.menu\":\"0e0a17\",\"background.overlay\":\"0000009e\",\"background.panel\":\"17121e\",\"background.raised\":\"2a2018\",\"background.surface\":\"221a14\",\"border.divider\":\"99731f80\",\"border.focus\":\"d9b340ff\",\"border.happiness\":\"b39940d9\",\"border.list\":\"4d4014b2\",\"border.listSelected\":\"d9b340cc\",\"border.panel\":\"73591fcc\",\"button.bgHover\":\"331a0d\",\"button.bgNormal\":\"1f1733\",\"button.bgPressed\":\"472f0f\",\"climate.cold\":\"1a4dff\",\"climate.hot\":\"ff260d\",\"climate.textCold\":\"66b3ff\",\"climate.textNeutral\":\"d9e0d9\",\"climate.textWarming\":\"ff731a\",\"climate.warm\":\"26cc40\",\"fog.explored\":\"000000b2\",\"fog.unexplored\":\"000000ff\",\"guide.bgPrimary\":\"1a1410\",\"guide.bgSecondary\":\"221a14\",\"guide.bgTertiary\":\"2a2018\",\"guide.dwarfAccent\":\"8b6a1a\",\"guide.dwarfPrimary\":\"c07040\",\"guide.dwarfPrimaryDark\":\"8a4a28\",\"guide.dwarfPrimaryLight\":\"e09868\",\"guide.textMuted\":\"7a6048\",\"guide.textPrimary\":\"f0e4d0\",\"guide.textSecondary\":\"b8a078\",\"player.blue\":\"3366ff\",\"player.brown\":\"806659\",\"player.cyan\":\"1accd9\",\"player.gray\":\"999999\",\"player.green\":\"33cc4d\",\"player.magenta\":\"cc4d80\",\"player.navy\":\"4d4d99\",\"player.orange\":\"e6801a\",\"player.purple\":\"b24de6\",\"player.red\":\"e63333\",\"player.sage\":\"66b366\",\"player.yellow\":\"e6cc1a\",\"semantic.diplomacy\":\"e68c73\",\"semantic.goldenAge\":\"ffeb66\",\"semantic.negative\":\"d95940\",\"semantic.positive\":\"66e666\",\"semantic.trade\":\"ccbf73\",\"semantic.warning\":\"e69933\",\"text.button\":\"e0d199\",\"text.buttonHover\":\"ffeb80\",\"text.buttonPressed\":\"ffffb3\",\"text.disabled\":\"80806680\",\"text.muted\":\"b2b2b2\",\"text.primary\":\"e0d8c8\",\"text.secondary\":\"bfb7a6\",\"text.title\":\"f2d973\"}" Button/colors/font_color = Color(0.878431, 0.819608, 0.6, 1) Button/colors/font_hover_color = Color(1, 0.921569, 0.501961, 1) Button/colors/font_pressed_color = Color(1, 1, 0.701961, 1) diff --git a/src/game/engine/scenes/hud/minimap.gd b/src/game/engine/scenes/hud/minimap.gd index 914a05c0..29668341 100644 --- a/src/game/engine/scenes/hud/minimap.gd +++ b/src/game/engine/scenes/hud/minimap.gd @@ -222,8 +222,9 @@ func _draw_ping() -> void: var color: Color = _ping_ring_color color.a = 1.0 - t var pixel_pos: Vector2 = _world_to_mini( - HexUtilsScript.axial_to_pixel(_ping_axial) + Vector2( - HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5 + ( + HexUtilsScript.axial_to_pixel(_ping_axial) + + Vector2(HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5) ) ) _overlay_rect.draw_arc(pixel_pos, radius, 0.0, TAU, 24, color, PING_RING_WIDTH) @@ -235,14 +236,29 @@ func _draw_fog(game_map: RefCounted, player_index: int) -> void: if vis == 2: continue var pixel_pos: Vector2 = _world_to_mini( - HexUtilsScript.axial_to_pixel(axial) + Vector2( - HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5 + ( + HexUtilsScript.axial_to_pixel(axial) + + Vector2(HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5) ) ) - var fog_col: Color = _fog_color if vis == 1 else _unexplored_color - _overlay_rect.draw_rect( - Rect2(pixel_pos - Vector2(1.0, 1.0), Vector2(3.0, 3.0)), fog_col - ) + if vis == 1: + # Seen-but-stale: light dot overlay — terrain stays partly visible. + _overlay_rect.draw_rect( + Rect2(pixel_pos - Vector2(1.0, 1.0), Vector2(3.0, 3.0)), _fog_color + ) + else: + # Unexplored: cover the tile's full minimap footprint with opaque + # black. The 3px dot left ~5px gaps at this scale, leaking + # undiscovered terrain; size the rect to the tile pitch (+2px overlap + # to kill seams) so never-seen territory is fully hidden. + var cover: Vector2 = ( + Vector2( + HexUtilsScript.HEX_HORIZ_SPACING * _minimap_scale.x, + HexUtilsScript.HEX_VERT_SPACING * _minimap_scale.y + ) + + Vector2(2.0, 2.0) + ) + _overlay_rect.draw_rect(Rect2(pixel_pos - cover * 0.5, cover), _unexplored_color) func _draw_owner_tiles(game_map: RefCounted) -> void: @@ -261,8 +277,9 @@ func _draw_owner_tiles(game_map: RefCounted) -> void: var tint: Color = player_colors[owner_idx] tint.a = OWNER_TINT_ALPHA var pixel_pos: Vector2 = _world_to_mini( - HexUtilsScript.axial_to_pixel(axial) + Vector2( - HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5 + ( + HexUtilsScript.axial_to_pixel(axial) + + Vector2(HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5) ) ) _overlay_rect.draw_rect(Rect2(pixel_pos - OWNER_TILE_SIZE * 0.5, OWNER_TILE_SIZE), tint) @@ -281,8 +298,9 @@ func _draw_city_dots(player: Variant) -> void: for city: Variant in player.cities: var city_pos: Vector2i = _entity_position(city) var pixel_pos: Vector2 = _world_to_mini( - HexUtilsScript.axial_to_pixel(city_pos) + Vector2( - HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5 + ( + HexUtilsScript.axial_to_pixel(city_pos) + + Vector2(HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5) ) ) _overlay_rect.draw_circle(pixel_pos, CITY_DOT_RADIUS, player.color) @@ -293,8 +311,9 @@ func _draw_unit_dots(player: Variant) -> void: for unit: Variant in player.units: var unit_pos: Vector2i = _entity_position(unit) var pixel_pos: Vector2 = _world_to_mini( - HexUtilsScript.axial_to_pixel(unit_pos) + Vector2( - HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5 + ( + HexUtilsScript.axial_to_pixel(unit_pos) + + Vector2(HexUtilsScript.HEX_WIDTH * 0.5, HexUtilsScript.HEX_HEIGHT * 0.5) ) ) _overlay_rect.draw_circle(pixel_pos, UNIT_DOT_RADIUS, player.color) @@ -331,9 +350,7 @@ func _draw_viewport_rect() -> void: if rect.size.x < 2.0 or rect.size.y < 2.0: return - var vp_color: Color = _parse_color( - EnvConfig.get_var("MINIMAP_VIEWPORT_COLOR", "#FFFFFFB3") - ) + var vp_color: Color = _parse_color(EnvConfig.get_var("MINIMAP_VIEWPORT_COLOR", "#FFFFFFB3")) var vp_width: float = EnvConfig.get_var("MINIMAP_VIEWPORT_WIDTH", "1.5").to_float() _overlay_rect.draw_rect(rect, vp_color, false, vp_width) diff --git a/src/game/engine/src/rendering/fog_renderer.gd b/src/game/engine/src/rendering/fog_renderer.gd index b6b01bb0..38034a96 100644 --- a/src/game/engine/src/rendering/fog_renderer.gd +++ b/src/game/engine/src/rendering/fog_renderer.gd @@ -16,7 +16,7 @@ const FOG_UNEXPLORED: int = 0 const FOG_FOGGED: int = 1 const FOG_VISIBLE: int = 2 -const UNEXPLORED_COLOR: Color = Color(0.0, 0.0, 0.0, 0.85) +const UNEXPLORED_COLOR: Color = Color(0.0, 0.0, 0.0, 1.0) const FOGGED_COLOR: Color = Color(0.0, 0.0, 0.0, 0.55) ## Alpha at vertices that border a visible tile (soft edge fade). const EDGE_FADE_ALPHA: float = 0.0 @@ -133,6 +133,14 @@ func _build_vertex_colors(axial: Vector2i, visibility_state: int) -> PackedColor var base: Color = _get_fog_color(visibility_state) var colors: PackedColorArray = PackedColorArray() colors.resize(6) + # Edge-softening only applies to the FOGGED (seen) state — it creates the + # smooth gradient at the visible↔seen boundary. Unexplored tiles must stay + # fully opaque even at vertices bordering a visible tile, otherwise + # undiscovered terrain bleeds through along the exploration frontier. + if visibility_state != FOG_FOGGED: + for i: int in 6: + colors[i] = base + return colors for i: int in 6: var softened: bool = false for dir_idx: int in VERTEX_NEIGHBOR_DIRS[i]: @@ -192,9 +200,7 @@ func _get_axial_visibility(axial: Vector2i) -> int: return _get_tile_visibility(tile) -func _on_tile_visibility_changed( - tile_pos: Vector2i, player_index: int, state: int -) -> void: +func _on_tile_visibility_changed(tile_pos: Vector2i, player_index: int, state: int) -> void: if player_index != _player_index: return update_tile_fog(tile_pos, state)