feat(ui): atlas overlay toggle bar — 9 MVP overlays + 2 locked (#836)

AtlasOverlayBar is an HBoxContainer docked top-right of the heightmap viewer
with 11 short-label buttons mapping to D-191 §7 / D-181 signal visibility:

  always-on (5)  TER INF NAM GAT POL — terrain, infrastructure, named
                 features, gate markers, political zones. Pinned on; clicks
                 are swallowed so the layers can't accidentally be disabled.
  toggleable (4) POP PRD SHD CRP — population density, production zones,
                 shadow economy, corporate presence. Reflect and mutate
                 viewer overlay state.
  locked (2)    STK BSL — stockpile_weeks, production_vs_baseline. Disabled
                 and greyed out with unlock-requirement tooltips, per D-181
                 semi-private/private tiers; kept in the bar so players see
                 that deeper data exists and is gated.

Each button writes through AtlasViewer.set_overlay_visible(), which is the
single entry point into the viewer's _overlay_visibility dict consumed by
AtlasMarkerOverlay._draw(). Locked overlays short-circuit in that setter.

Per D-191 criterion 6.
This commit is contained in:
2026-04-15 08:27:30 +02:00
parent c33ac8921f
commit 9987eaad8a
2 changed files with 126 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
class_name AtlasOverlayBar
extends HBoxContainer
## Overlay toggle bar for #835 AtlasViewer — nine MVP overlays plus two locked
## layers (#836, D-191 §7, D-181 signal visibility ladder).
##
## Layout:
## [ALWAYS-ON] terrain · infrastructure · named · gate · political
## [TOGGLEABLE] pop density · production · shadow · corp presence
## [LOCKED] stockpile weeks · production vs baseline
##
## Always-on buttons are visible but non-interactive (they render as pinned).
## Toggleable buttons reflect viewer visibility state and flip on click.
## Locked buttons are disabled, greyed, and carry a tooltip describing the
## unlock requirement from D-181 — they are not removed from the bar so the
## player can see that richer data exists and is gated.
const OVERLAY_DEFS: Array = [
# id, label, group ("always" | "toggle" | "locked"), tooltip
{"id": "terrain", "label": "TER", "group": "always", "tooltip": "Terrain — base heightmap. Always visible."},
{"id": "infrastructure", "label": "INF", "group": "always", "tooltip": "Infrastructure — roads + rail. Always visible."},
{"id": "named_features", "label": "NAM", "group": "always", "tooltip": "Named features — rivers, oceans, mountain ranges. Always visible."},
{"id": "gate_markers", "label": "GAT", "group": "always", "tooltip": "Gate + spaceport markers. Always visible."},
{"id": "political_zones", "label": "POL", "group": "always", "tooltip": "Political zones — currency-zone bands. Always visible."},
{"id": "population_density", "label": "POP", "group": "toggle", "tooltip": "Population density (D-181 public)."},
{"id": "production_zones", "label": "PRD", "group": "toggle", "tooltip": "Production zones (D-181 observable — requires presence)."},
{"id": "shadow_economy", "label": "SHD", "group": "toggle", "tooltip": "Shadow economy zones (D-181 observable)."},
{"id": "corp_presence", "label": "CRP", "group": "toggle", "tooltip": "Corporate presence — Tier 1 only (D-181 observable)."},
{"id": "stockpile_weeks", "label": "STK", "group": "locked", "tooltip": "Stockpile weeks — LOCKED. Requires corporate contact (D-181 semi-private)."},
{"id": "production_vs_baseline", "label": "BSL", "group": "locked", "tooltip": "Production vs baseline — LOCKED. Requires insider access (D-181 private)."},
]
const COLOR_PINNED: Color = Color("#c8d0e0")
const COLOR_ACTIVE: Color = Color("#f0d060")
const COLOR_INACTIVE: Color = Color("#556677")
const COLOR_LOCKED: Color = Color("#3a4a55")
var _viewer = null
var _buttons: Dictionary = {} # overlay_id -> Button
func _init(viewer_ref) -> void:
_viewer = viewer_ref
add_theme_constant_override("separation", 4)
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_STOP
for def: Dictionary in OVERLAY_DEFS:
var b: Button = Button.new()
b.text = def["label"]
b.tooltip_text = def["tooltip"]
b.toggle_mode = (def["group"] != "always")
b.focus_mode = Control.FOCUS_NONE
b.custom_minimum_size = Vector2(42.0, 24.0)
b.add_theme_font_size_override("font_size", 10)
match def["group"]:
"always":
b.disabled = false
b.button_pressed = true
b.add_theme_color_override("font_color", COLOR_PINNED)
b.add_theme_color_override("font_hover_color", COLOR_PINNED)
b.add_theme_color_override("font_pressed_color", COLOR_PINNED)
b.add_theme_color_override("font_disabled_color", COLOR_PINNED)
# Swallow clicks so the player can't accidentally turn terrain off,
# but keep it visible + tooltipped.
b.pressed.connect(_on_always_pressed.bind(def["id"]))
"toggle":
b.button_pressed = _viewer.is_overlay_visible(def["id"]) if _viewer else false
b.add_theme_color_override("font_color", COLOR_INACTIVE)
b.add_theme_color_override("font_pressed_color", COLOR_ACTIVE)
b.toggled.connect(_on_toggle_changed.bind(def["id"]))
"locked":
b.disabled = true
b.button_pressed = false
b.add_theme_color_override("font_disabled_color", COLOR_LOCKED)
add_child(b)
_buttons[def["id"]] = b
func _on_always_pressed(overlay_id: String) -> void:
# Always-on overlays stay on. Keep the button visually pressed.
if _buttons.has(overlay_id):
_buttons[overlay_id].button_pressed = true
func _on_toggle_changed(pressed: bool, overlay_id: String) -> void:
if _viewer:
_viewer.set_overlay_visible(overlay_id, pressed)
func sync_from_viewer() -> void:
if _viewer == null:
return
for def: Dictionary in OVERLAY_DEFS:
if def["group"] != "toggle":
continue
var b: Button = _buttons.get(def["id"])
if b:
b.set_pressed_no_signal(_viewer.is_overlay_visible(def["id"]))
+25
View File
@@ -101,6 +101,7 @@ var _canvas: Node2D = null # transformed node holding heightmap
var _overlay_node: AtlasMarkerOverlay = null # draws on top of heightmap
var _city_panel = null # ImplantPanel sidebar (city data)
var _empty_notice = null # ImplantPanel shown when heightmap missing
var _overlay_bar: AtlasOverlayBar = null # #836 overlay toggle bar (top-right)
func _ready() -> void:
@@ -125,6 +126,7 @@ func _ready() -> void:
_build_city_panel()
_build_empty_notice()
_build_overlay_bar()
## Called by AtlasPanel when entering the viewer for a specific body.
@@ -524,9 +526,32 @@ func _position_empty_notice() -> void:
_empty_notice.position = Vector2((sz.x - 400.0) * 0.5, sz.y * 0.35)
# =============================================================================
# Overlay toggle bar (#836)
# =============================================================================
func _build_overlay_bar() -> void:
_overlay_bar = AtlasOverlayBar.new(self)
_overlay_bar.name = "OverlayBar"
add_child(_overlay_bar)
_position_overlay_bar()
func _position_overlay_bar() -> void:
var sz: Vector2 = get_rect().size
if sz == Vector2.ZERO:
sz = Vector2(1280.0, 720.0)
# Top-right, leaving room for the header text
var bar_w: float = _overlay_bar.size.x if _overlay_bar.size.x > 0.0 else 520.0
_overlay_bar.position = Vector2(sz.x - bar_w - PANEL_MARGIN, PANEL_MARGIN)
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED:
if _city_panel:
_position_city_panel()
if _empty_notice:
_position_empty_notice()
if _overlay_bar:
_position_overlay_bar()