feat(fog): dwarven cartographer's-fog for unexplored tiles
Replace flat-black unexplored fog with an antique-cartography treatment (Civ-VI style), themed as dwarven 'unmapped vellum/slate' — reveals no real terrain, only a stylized stone surface. - fog_renderer.gd: unexplored tiles painted with a procedurally generated opaque vellum texture (FastNoiseLite domain-warped FBM → warm dark slate→parchment gradient), generated synchronously at init so there is no reveal-before-ready leak. No binary art asset required. Visible/seen paths unchanged; frontier stays a clean opaque edge. - design-tokens.json -> ui_theme.tres: fog.unexplored 000000ff -> 1a160fff (warm 'unmapped vellum' tone) so the minimap unexplored cover matches. Render-verified on apricot (iter_7q proof, fog enabled): undiscovered renders as warm stone/vellum on map + minimap, no terrain bleed, clean frontier, lit tiles unaffected. gdlint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c22b497a27
commit
30bcde26d5
3 changed files with 105 additions and 7 deletions
|
|
@ -267,9 +267,9 @@
|
||||||
"$description": "Seen but not currently visible tiles — 70% black"
|
"$description": "Seen but not currently visible tiles — 70% black"
|
||||||
},
|
},
|
||||||
"unexplored": {
|
"unexplored": {
|
||||||
"$value": "#000000ff",
|
"$value": "#1a160fff",
|
||||||
"$type": "color",
|
"$type": "color",
|
||||||
"$description": "Never-seen tiles — fully opaque black (undiscovered terrain must never be visible on map or minimap)"
|
"$description": "Never-seen tiles — opaque warm 'unmapped vellum' slate (matches the dwarven-cartography fog texture on the main map). Must never reveal real terrain on map or minimap."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ corner_radius_bottom_left = 2
|
||||||
corner_radius_bottom_right = 2
|
corner_radius_bottom_right = 2
|
||||||
|
|
||||||
[resource]
|
[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\":\"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\"}"
|
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\":\"1a160fff\",\"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_color = Color(0.878431, 0.819608, 0.6, 1)
|
||||||
Button/colors/font_hover_color = Color(1, 0.921569, 0.501961, 1)
|
Button/colors/font_hover_color = Color(1, 0.921569, 0.501961, 1)
|
||||||
Button/colors/font_pressed_color = Color(1, 1, 0.701961, 1)
|
Button/colors/font_pressed_color = Color(1, 1, 0.701961, 1)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ extends Node2D
|
||||||
## creating a smooth gradient at the fog boundary instead of a hard cutoff.
|
## creating a smooth gradient at the fog boundary instead of a hard cutoff.
|
||||||
##
|
##
|
||||||
## Three visibility states per tile:
|
## Three visibility states per tile:
|
||||||
## 0 = Unexplored: fully black overlay (terrain hidden)
|
## 0 = Unexplored: opaque dwarven-vellum "unmapped" overlay (terrain hidden).
|
||||||
## 1 = Fog: semi-transparent dark overlay (terrain visible but dimmed)
|
## The antique-cartography treatment — a stylized stone/parchment surface
|
||||||
|
## that reveals NO real terrain, resources, or units (see _build_unexplored_texture).
|
||||||
|
## 1 = Fog: semi-transparent dark overlay (last-seen terrain visible but dimmed)
|
||||||
## 2 = Visible: no overlay (full brightness)
|
## 2 = Visible: no overlay (full brightness)
|
||||||
|
|
||||||
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
|
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
|
||||||
|
|
@ -16,11 +18,17 @@ const FOG_UNEXPLORED: int = 0
|
||||||
const FOG_FOGGED: int = 1
|
const FOG_FOGGED: int = 1
|
||||||
const FOG_VISIBLE: int = 2
|
const FOG_VISIBLE: int = 2
|
||||||
|
|
||||||
const UNEXPLORED_COLOR: Color = Color(0.0, 0.0, 0.0, 1.0)
|
## Fallback flat tone if the procedural vellum texture fails to generate — kept
|
||||||
|
## opaque + on-theme so undiscovered terrain is never revealed regardless.
|
||||||
|
const UNEXPLORED_COLOR: Color = Color(0.10, 0.085, 0.06, 1.0)
|
||||||
const FOGGED_COLOR: Color = Color(0.0, 0.0, 0.0, 0.55)
|
const FOGGED_COLOR: Color = Color(0.0, 0.0, 0.0, 0.55)
|
||||||
## Alpha at vertices that border a visible tile (soft edge fade).
|
## Alpha at vertices that border a visible tile (soft edge fade).
|
||||||
const EDGE_FADE_ALPHA: float = 0.0
|
const EDGE_FADE_ALPHA: float = 0.0
|
||||||
|
|
||||||
|
## Procedural dwarven-vellum texture for unexplored ("unmapped") tiles — the
|
||||||
|
## antique-cartography fog treatment. Generated once at init.
|
||||||
|
const VELLUM_TEX_SIZE: int = 128
|
||||||
|
|
||||||
## Each flat-top hex vertex i is the corner shared by the two neighbors
|
## Each flat-top hex vertex i is the corner shared by the two neighbors
|
||||||
## flanking that vertex. Computed from the hex polygon vertex order:
|
## flanking that vertex. Computed from the hex polygon vertex order:
|
||||||
## vertex 0 (96,0) top-left → NW (dir 2) and NE (dir 1)
|
## vertex 0 (96,0) top-left → NW (dir 2) and NE (dir 1)
|
||||||
|
|
@ -48,6 +56,12 @@ var _player_index: int = 0
|
||||||
## Nullable — cleared on clear(), assigned on initialize().
|
## Nullable — cleared on clear(), assigned on initialize().
|
||||||
var _game_map: GameMapScript = null
|
var _game_map: GameMapScript = null
|
||||||
|
|
||||||
|
## Procedural vellum texture + cached per-hex UVs / opaque vertex colors,
|
||||||
|
## built once on first initialize().
|
||||||
|
var _unexplored_tex: ImageTexture = null
|
||||||
|
var _hex_uv: PackedVector2Array = PackedVector2Array()
|
||||||
|
var _white6: PackedColorArray = PackedColorArray()
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if EnvConfig.get_bool("FORCE_DISABLE_FOGOFWAR"):
|
if EnvConfig.get_bool("FORCE_DISABLE_FOGOFWAR"):
|
||||||
|
|
@ -63,6 +77,11 @@ func initialize(game_map: GameMapScript, player_index: int) -> void:
|
||||||
if fog_disabled:
|
if fog_disabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if _unexplored_tex == null:
|
||||||
|
_unexplored_tex = _build_unexplored_texture()
|
||||||
|
_hex_uv = _build_hex_uv()
|
||||||
|
_white6 = _build_opaque_white()
|
||||||
|
|
||||||
var tiles: Dictionary = game_map.tiles
|
var tiles: Dictionary = game_map.tiles
|
||||||
for axial: Vector2i in tiles:
|
for axial: Vector2i in tiles:
|
||||||
var tile: TileScript = tiles[axial] as TileScript
|
var tile: TileScript = tiles[axial] as TileScript
|
||||||
|
|
@ -93,8 +112,12 @@ func update_tile_fog(axial: Vector2i, visibility_state: int) -> void:
|
||||||
node.visible = false
|
node.visible = false
|
||||||
# Neighbors on the fog side need their edge vertices re-softened.
|
# Neighbors on the fog side need their edge vertices re-softened.
|
||||||
_refresh_neighbor_edges(axial)
|
_refresh_neighbor_edges(axial)
|
||||||
|
elif visibility_state == FOG_UNEXPLORED and _unexplored_tex != null:
|
||||||
|
node.visible = true
|
||||||
|
_apply_unexplored_texture(node)
|
||||||
else:
|
else:
|
||||||
node.visible = true
|
node.visible = true
|
||||||
|
node.texture = null
|
||||||
node.vertex_colors = _build_vertex_colors(axial, visibility_state)
|
node.vertex_colors = _build_vertex_colors(axial, visibility_state)
|
||||||
else:
|
else:
|
||||||
_create_fog_polygon(axial, visibility_state)
|
_create_fog_polygon(axial, visibility_state)
|
||||||
|
|
@ -120,6 +143,8 @@ func _create_fog_polygon(axial: Vector2i, visibility_state: int) -> void:
|
||||||
|
|
||||||
if visibility_state == FOG_VISIBLE:
|
if visibility_state == FOG_VISIBLE:
|
||||||
polygon.visible = false
|
polygon.visible = false
|
||||||
|
elif visibility_state == FOG_UNEXPLORED and _unexplored_tex != null:
|
||||||
|
_apply_unexplored_texture(polygon)
|
||||||
else:
|
else:
|
||||||
polygon.vertex_colors = _build_vertex_colors(axial, visibility_state)
|
polygon.vertex_colors = _build_vertex_colors(axial, visibility_state)
|
||||||
|
|
||||||
|
|
@ -127,6 +152,14 @@ func _create_fog_polygon(axial: Vector2i, visibility_state: int) -> void:
|
||||||
_fog_nodes[axial] = polygon
|
_fog_nodes[axial] = polygon
|
||||||
|
|
||||||
|
|
||||||
|
## Paint an unexplored tile with the opaque dwarven-vellum texture. Reveals no
|
||||||
|
## real terrain — the texture is a stylized "unmapped" stone/parchment surface.
|
||||||
|
func _apply_unexplored_texture(polygon: Polygon2D) -> void:
|
||||||
|
polygon.texture = _unexplored_tex
|
||||||
|
polygon.uv = _hex_uv
|
||||||
|
polygon.vertex_colors = _white6
|
||||||
|
|
||||||
|
|
||||||
## Build per-vertex colors for a fog polygon at axial, softening vertices
|
## Build per-vertex colors for a fog polygon at axial, softening vertices
|
||||||
## that border at least one visible neighbor tile.
|
## that border at least one visible neighbor tile.
|
||||||
func _build_vertex_colors(axial: Vector2i, visibility_state: int) -> PackedColorArray:
|
func _build_vertex_colors(axial: Vector2i, visibility_state: int) -> PackedColorArray:
|
||||||
|
|
@ -171,7 +204,13 @@ func _refresh_neighbor_edges(visible_axial: Vector2i) -> void:
|
||||||
if nb_tile == null:
|
if nb_tile == null:
|
||||||
continue
|
continue
|
||||||
var vis: int = _get_tile_visibility(nb_tile)
|
var vis: int = _get_tile_visibility(nb_tile)
|
||||||
nb_node.vertex_colors = _build_vertex_colors(nb_axial, vis)
|
# Unexplored neighbors keep the opaque vellum texture — only fogged tiles
|
||||||
|
# participate in edge softening.
|
||||||
|
if vis == FOG_UNEXPLORED and _unexplored_tex != null:
|
||||||
|
_apply_unexplored_texture(nb_node)
|
||||||
|
else:
|
||||||
|
nb_node.texture = null
|
||||||
|
nb_node.vertex_colors = _build_vertex_colors(nb_axial, vis)
|
||||||
|
|
||||||
|
|
||||||
func _get_fog_color(visibility_state: int) -> Color:
|
func _get_fog_color(visibility_state: int) -> Color:
|
||||||
|
|
@ -204,3 +243,62 @@ func _on_tile_visibility_changed(tile_pos: Vector2i, player_index: int, state: i
|
||||||
if player_index != _player_index:
|
if player_index != _player_index:
|
||||||
return
|
return
|
||||||
update_tile_fog(tile_pos, state)
|
update_tile_fog(tile_pos, state)
|
||||||
|
|
||||||
|
|
||||||
|
## Cache an opaque-white 6-vertex color array (texture shown as-authored).
|
||||||
|
func _build_opaque_white() -> PackedColorArray:
|
||||||
|
var c: PackedColorArray = PackedColorArray()
|
||||||
|
c.resize(6)
|
||||||
|
for i: int in 6:
|
||||||
|
c[i] = Color.WHITE
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
## Map each hex-polygon vertex (local pixel coords) onto the full vellum texture
|
||||||
|
## so every unexplored tile shows the complete stone/parchment surface.
|
||||||
|
func _build_hex_uv() -> PackedVector2Array:
|
||||||
|
var poly: PackedVector2Array = HexUtilsScript.hex_polygon
|
||||||
|
var min_v: Vector2 = Vector2(INF, INF)
|
||||||
|
var max_v: Vector2 = Vector2(-INF, -INF)
|
||||||
|
for v: Vector2 in poly:
|
||||||
|
min_v = min_v.min(v)
|
||||||
|
max_v = max_v.max(v)
|
||||||
|
var span: Vector2 = (max_v - min_v).max(Vector2.ONE)
|
||||||
|
var uv: PackedVector2Array = PackedVector2Array()
|
||||||
|
uv.resize(poly.size())
|
||||||
|
for i: int in poly.size():
|
||||||
|
uv[i] = (poly[i] - min_v) / span * float(VELLUM_TEX_SIZE)
|
||||||
|
return uv
|
||||||
|
|
||||||
|
|
||||||
|
## Generate the opaque dwarven-vellum "unmapped" texture synchronously (so there
|
||||||
|
## is no reveal-before-ready leak). Domain-warped fractal noise mapped through a
|
||||||
|
## warm dark slate→parchment gradient: reads as aged, unread stone — never as
|
||||||
|
## real terrain. Self-contained — no binary art asset required.
|
||||||
|
func _build_unexplored_texture() -> ImageTexture:
|
||||||
|
var noise: FastNoiseLite = FastNoiseLite.new()
|
||||||
|
noise.seed = 1337
|
||||||
|
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||||
|
noise.frequency = 0.035
|
||||||
|
noise.fractal_type = FastNoiseLite.FRACTAL_FBM
|
||||||
|
noise.fractal_octaves = 4
|
||||||
|
noise.domain_warp_enabled = true
|
||||||
|
noise.domain_warp_amplitude = 35.0
|
||||||
|
var n_img: Image = noise.get_image(VELLUM_TEX_SIZE, VELLUM_TEX_SIZE, false, false, true)
|
||||||
|
|
||||||
|
var ramp: Gradient = Gradient.new()
|
||||||
|
ramp.offsets = PackedFloat32Array([0.0, 0.45, 0.75, 1.0])
|
||||||
|
ramp.colors = PackedColorArray(
|
||||||
|
[
|
||||||
|
Color(0.05, 0.045, 0.035), # deep shadow between veins
|
||||||
|
Color(0.10, 0.085, 0.06), # base unmapped slate
|
||||||
|
Color(0.155, 0.12, 0.075), # aged parchment
|
||||||
|
Color(0.24, 0.185, 0.105), # gold-brown vein highlight (ties to UI gold)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
var out: Image = Image.create(VELLUM_TEX_SIZE, VELLUM_TEX_SIZE, false, Image.FORMAT_RGB8)
|
||||||
|
for y: int in VELLUM_TEX_SIZE:
|
||||||
|
for x: int in VELLUM_TEX_SIZE:
|
||||||
|
out.set_pixel(x, y, ramp.sample(n_img.get_pixel(x, y).r))
|
||||||
|
return ImageTexture.create_from_image(out)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue