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"
|
||||
},
|
||||
"unexplored": {
|
||||
"$value": "#000000ff",
|
||||
"$value": "#1a160fff",
|
||||
"$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": {
|
||||
|
|
|
|||
|
|
@ -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\":\"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_hover_color = Color(1, 0.921569, 0.501961, 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.
|
||||
##
|
||||
## Three visibility states per tile:
|
||||
## 0 = Unexplored: fully black overlay (terrain hidden)
|
||||
## 1 = Fog: semi-transparent dark overlay (terrain visible but dimmed)
|
||||
## 0 = Unexplored: opaque dwarven-vellum "unmapped" overlay (terrain hidden).
|
||||
## 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)
|
||||
|
||||
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_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)
|
||||
## Alpha at vertices that border a visible tile (soft edge fade).
|
||||
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
|
||||
## flanking that vertex. Computed from the hex polygon vertex order:
|
||||
## 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().
|
||||
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:
|
||||
if EnvConfig.get_bool("FORCE_DISABLE_FOGOFWAR"):
|
||||
|
|
@ -63,6 +77,11 @@ func initialize(game_map: GameMapScript, player_index: int) -> void:
|
|||
if fog_disabled:
|
||||
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
|
||||
for axial: Vector2i in tiles:
|
||||
var tile: TileScript = tiles[axial] as TileScript
|
||||
|
|
@ -93,8 +112,12 @@ func update_tile_fog(axial: Vector2i, visibility_state: int) -> void:
|
|||
node.visible = false
|
||||
# Neighbors on the fog side need their edge vertices re-softened.
|
||||
_refresh_neighbor_edges(axial)
|
||||
elif visibility_state == FOG_UNEXPLORED and _unexplored_tex != null:
|
||||
node.visible = true
|
||||
_apply_unexplored_texture(node)
|
||||
else:
|
||||
node.visible = true
|
||||
node.texture = null
|
||||
node.vertex_colors = _build_vertex_colors(axial, visibility_state)
|
||||
else:
|
||||
_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:
|
||||
polygon.visible = false
|
||||
elif visibility_state == FOG_UNEXPLORED and _unexplored_tex != null:
|
||||
_apply_unexplored_texture(polygon)
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
## 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
|
||||
## that border at least one visible neighbor tile.
|
||||
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:
|
||||
continue
|
||||
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:
|
||||
|
|
@ -204,3 +243,62 @@ func _on_tile_visibility_changed(tile_pos: Vector2i, player_index: int, state: i
|
|||
if player_index != _player_index:
|
||||
return
|
||||
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