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:
@@ -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"]))
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user