fix(fog): fully hide undiscovered tiles on map + minimap

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 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-06 03:30:58 -07:00
parent 26bf44f077
commit c22b497a27
4 changed files with 47 additions and 24 deletions

View file

@ -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": {

View file

@ -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)

View file

@ -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)

View file

@ -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)