fix(ui): atlas overlay scale, opener robustness, rivers match water (#960)

- Generation overlays map positions from Layer1Output.grid_w/grid_h onto the
  displayed texture (not the markers.json grid), fixing the off-by-2 scale.
- AtlasViewer loads ImplantPending by path instead of referencing the new
  class_name, so a stale global-class cache (a session that hasn't re-imported
  after the class was added) can't fail to parse AtlasViewer and break the
  atlas from opening.
- Rivers and mouths use the reliefmap's own water colour (sampled median ocean
  blue) so river lines blend into surface water instead of reading as a
  distinct-coloured line flowing onto the sea.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 10:03:47 +02:00
co-authored by Claude Opus 4.7
parent adbd7f6744
commit 6134296840
2 changed files with 37 additions and 13 deletions
@@ -32,8 +32,11 @@ const COLOR_PRODUCTION: Color = Color(0.40, 0.80, 0.55, 0.18)
const COLOR_SHADOW: Color = Color(0.35, 0.20, 0.50, 0.22)
const COLOR_CORP: Color = Color(0.85, 0.65, 0.20, 0.75)
# Generation overlays (#960, D-225, Araminta's encoding).
const COLOR_GEN_RIVER: Color = Color(0.35, 0.60, 0.90, 0.75)
const COLOR_GEN_MOUTH: Color = Color(0.50, 0.80, 1.0, 0.90)
# Rivers/mouths use the reliefmap's own water colour (sampled median ocean blue)
# so river lines blend seamlessly into the surface water bodies instead of
# reading as a distinct-coloured line flowing onto the sea.
const COLOR_GEN_RIVER: Color = Color(0.353, 0.647, 0.776, 1.0)
const COLOR_GEN_MOUTH: Color = Color(0.353, 0.647, 0.776, 1.0)
const COLOR_GEN_BASIN_FILL: Color = Color(0.20, 0.35, 0.55, 0.06)
const COLOR_GEN_BASIN_LINE: Color = Color(0.45, 0.65, 0.85, 0.45)
const GEN_ATTRACTOR_MIN_STRENGTH: float = 0.15
@@ -55,6 +58,14 @@ const PRODUCTION_FUNCTIONS: Array = [
var viewer = null # AtlasViewer (untyped to avoid cyclic ref)
# Generation-overlay coordinate mapping (#960): set in _draw before the gen
# layers are drawn. Positions are [row, col] in the Layer-1 working grid
# (_gen_grid_*), mapped onto the displayed heightmap texture (_gen_tex_*).
var _gen_grid_w: float = 0.0
var _gen_grid_h: float = 0.0
var _gen_tex_w: float = 0.0
var _gen_tex_h: float = 0.0
func _draw() -> void:
if viewer == null:
@@ -102,12 +113,19 @@ func _draw() -> void:
# Draw order: basins (area) under rivers under attractors (point anchors).
var layer1: Variant = viewer.get_generation_layer1()
if layer1 is Dictionary:
if viewer.is_overlay_visible("gen_l1_basins"):
_draw_gen_basins(layer1)
if viewer.is_overlay_visible("gen_l1_rivers"):
_draw_gen_rivers(layer1)
if viewer.is_overlay_visible("gen_l1_attractors"):
_draw_gen_attractors(layer1)
# The Layer1Output carries the working-grid dims its positions live in
# (#960). Map from those, NOT the markers.json grid (_grid_w/_grid_h).
_gen_grid_w = float(layer1.get("grid_w", 0))
_gen_grid_h = float(layer1.get("grid_h", 0))
_gen_tex_w = tex_w
_gen_tex_h = tex_h
if _gen_grid_w > 0.0 and _gen_grid_h > 0.0:
if viewer.is_overlay_visible("gen_l1_basins"):
_draw_gen_basins(layer1)
if viewer.is_overlay_visible("gen_l1_rivers"):
_draw_gen_rivers(layer1)
if viewer.is_overlay_visible("gen_l1_attractors"):
_draw_gen_attractors(layer1)
# POIs (non-gate first, then gates on top if enabled)
_draw_pois(markers)
@@ -359,9 +377,12 @@ func _draw_corp_presence(markers: Dictionary) -> void:
# =============================================================================
## Map a Layer-1 [row, col] grid position to canvas space.
## Map a Layer-1 [row, col] position (in the Layer1Output working grid) onto the
## displayed heightmap texture. Uses the grid dims FROM THE DATA — NOT
## viewer.grid_to_canvas, whose _grid_w/_grid_h is the markers.json coordinate
## space. Conflating the two scaled the overlays off by the downsample ratio (#960).
func _gen_pos(rc: Variant) -> Vector2:
return viewer.grid_to_canvas(Vector2(float(rc[1]), float(rc[0])))
return Vector2(float(rc[1]) / _gen_grid_w * _gen_tex_w, float(rc[0]) / _gen_grid_h * _gen_tex_h)
func _draw_gen_rivers(layer1: Dictionary) -> void:
+6 -3
View File
@@ -182,7 +182,7 @@ var _city_panel = null # ImplantPanel sidebar (city data)
var _empty_notice = null # ImplantPanel shown when heightmap missing
var _overlay_bar = null # #836 overlay toggle bar (no class_name, review #8)
var _screen_header: ImplantHeader = null # top-left title/hint (D-169 composition)
var _gen_pending_indicator: ImplantPending = null # #960 Layer-1 "generating" overlay
var _gen_pending_indicator = null # ImplantPending — loaded by path, not a class_name dep
func _ready() -> void:
@@ -220,7 +220,10 @@ func _ready() -> void:
SimBridge.atlas_layers_received.connect(_on_atlas_layers_received)
# #960: diegetic "generating" indicator, shown only while the proxy is Pending.
_gen_pending_indicator = ImplantPending.new()
# Loaded by path (not `ImplantPending.new()`) so a stale global-class cache —
# e.g. a running session that hasn't re-imported after this class was added —
# can't fail to parse AtlasViewer and break the atlas from opening (#960).
_gen_pending_indicator = load("res://ui/implant/implant_pending.gd").new()
_gen_pending_indicator.name = "GenPending"
add_child(_gen_pending_indicator)
_gen_pending_indicator.apply_implant_theme(_implant_theme)
@@ -359,7 +362,7 @@ func _position_pending_indicator() -> void:
if _gen_pending_indicator == null:
return
_gen_pending_indicator.position = Vector2(
(size.x - ImplantPending.DEFAULT_WIDTH) * 0.5, size.y * 0.45
(size.x - _gen_pending_indicator.DEFAULT_WIDTH) * 0.5, size.y * 0.45
)