diff --git a/client/ui/implant/atlas_overlay_bar.gd b/client/ui/implant/atlas_overlay_bar.gd new file mode 100644 index 000000000..acf14d955 --- /dev/null +++ b/client/ui/implant/atlas_overlay_bar.gd @@ -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"])) diff --git a/client/ui/implant/atlas_viewer.gd b/client/ui/implant/atlas_viewer.gd index b68b39b77..d509445b8 100644 --- a/client/ui/implant/atlas_viewer.gd +++ b/client/ui/implant/atlas_viewer.gd @@ -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()