diff --git a/Makefile b/Makefile index c88251275..ec7003daa 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null) pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \ pre-pr-server pre-pr-client pre-pr-content \ fixtures-client fixtures-gauntlet golden-diff golden-update \ - checklist-validate checklist-generate check-star-map \ + checklist-validate checklist-generate check-star-map star-map-data \ build-sr-voice run-sr-voice test-voice-mock test-voice-real \ perf-baseline debug-schedule \ test-ipc-fixtures test-ipc-protocol test-ipc-integration test-ipc-benchmark \ @@ -53,6 +53,8 @@ help: @echo " make validate-content Validate content YAML against schemas" @echo " make check-fact-ids Check fact_id references against knowledge catalogs" @echo " make atlas-verify Verify atlas proposal JSONs (all in docs/atlas/proposals/)" + @echo " make star-map-data Regenerate client/data/star_map_data.json from systems.db + wiki" + @echo " make check-star-map Assert star_map_data.json is up to date (part of pre-pr-client)" @echo " make economy-db Import economics data into systems.db (TOML/JSON → SQLite)" @echo " make fixtures-client Generate GDScript->Rust cross-encoder fixtures (#475)" @echo " make golden-diff Show diff if golden file output has changed" @@ -260,7 +262,7 @@ pre-pr-build: build-server build-client pre-pr-test: test-server test-client @echo "--- Tests: PASS ---" -pre-pr-validate: validate-content check-fact-ids +pre-pr-validate: validate-content check-fact-ids check-star-map @echo "--- Content validation: PASS ---" pre-pr-fixtures: @@ -302,7 +304,7 @@ pre-pr-fixtures: pre-pr-server: lint-server build-server test-server pre-pr-fixtures audit @echo "=== Server pre-PR: PASSED ===" -pre-pr-client: lint-client build-client test-client +pre-pr-client: lint-client build-client test-client check-star-map @echo "=== Client pre-PR: PASSED ===" pre-pr-content: validate-content check-fact-ids checklist-validate atlas-verify @@ -375,6 +377,14 @@ checklist-generate: check-star-map: @python3 tooling/generate-star-map-data.py --check +# Regenerate client/data/star_map_data.json from systems.db + wiki. Depends on +# nothing — call this after any systems.db change (e.g. the server atlas +# pipeline populating terrain_reference in #839) so the atlas viewer picks up +# the new fields. pre-pr-client / pre-pr-validate assert staleness via +# check-star-map and will fail if this step is skipped. +star-map-data: + @python3 tooling/generate-star-map-data.py + perf-baseline: @tooling/perf-baseline diff --git a/client/scripts/main.gd b/client/scripts/main.gd index ab818403a..0db82afab 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -185,8 +185,10 @@ func _unhandled_key_input(event: InputEvent) -> void: if atlas_panel: atlas_panel.toggle_visible() elif event is InputEventKey and event.keycode == KEY_N: - # #824: N — toggle Economics Monitor implant panel (E is bound to interact) - if economics_panel: + # #824: N — toggle Economics Monitor implant panel (E is bound to interact). + # Gated on the atlas being inactive so the viewer's N → city-economics + # cross-link isn't shadowed by this global toggle (review #6). + if economics_panel and not HudGroups.is_app_active("implant/map/atlas"): economics_panel.toggle_visible() elif event is InputEventKey and event.keycode == KEY_BRACKETLEFT: # #824: [ — cycle economics panel system selector backward diff --git a/client/ui/implant/atlas_marker_overlay.gd b/client/ui/implant/atlas_marker_overlay.gd index d5d46b195..d5a8bbc4f 100644 --- a/client/ui/implant/atlas_marker_overlay.gd +++ b/client/ui/implant/atlas_marker_overlay.gd @@ -163,7 +163,7 @@ func _draw_gate_markers(markers: Dictionary) -> void: for c: Dictionary in markers.get("cities", []): if not bool(c.get("gate_terminal", false)): continue - var pos: Vector2 = viewer._city_canvas_pos(c) + var pos: Vector2 = viewer.city_canvas_pos(c) draw_arc(pos, 9.0, 0.0, TAU, 18, COLOR_GATE, 1.2, true) @@ -172,10 +172,10 @@ func _draw_cities(markers: Dictionary) -> void: if cities.is_empty(): return var font := ThemeDB.fallback_font - var hovered: Dictionary = viewer._hovered_city - var selected: Dictionary = viewer._selected_city + var hovered: Dictionary = viewer.get_hovered_city() + var selected: Dictionary = viewer.get_selected_city() for c: Dictionary in cities: - var pos: Vector2 = viewer._city_canvas_pos(c) + var pos: Vector2 = viewer.city_canvas_pos(c) var tier: int = int(c.get("population_tier", 1)) var r: float = 2.5 + float(tier) * 0.8 var col: Color = COLOR_CITY @@ -199,7 +199,7 @@ func _draw_cities(markers: Dictionary) -> void: func _draw_population_density(markers: Dictionary) -> void: # Placeholder: soft blobs at city positions scaled by population_tier for c: Dictionary in markers.get("cities", []): - var pos: Vector2 = viewer._city_canvas_pos(c) + var pos: Vector2 = viewer.city_canvas_pos(c) var tier: int = int(c.get("population_tier", 1)) var r: float = 12.0 + float(tier) * 6.0 draw_circle(pos, r, COLOR_POP_HEAT) @@ -215,7 +215,7 @@ func _draw_production_zones(markers: Dictionary) -> void: var func_id: String = str(city.get("primary_function", "")).to_lower() if not PRODUCTION_FUNCTIONS.has(func_id): continue - var pos: Vector2 = viewer._city_canvas_pos(city) + var pos: Vector2 = viewer.city_canvas_pos(city) var tier: int = int(city.get("population_tier", 1)) var radius: float = 10.0 + float(tier) * 4.0 draw_circle(pos, radius, COLOR_PRODUCTION) @@ -229,7 +229,7 @@ func _draw_shadow_economy(markers: Dictionary) -> void: var commission: bool = bool(city.get("commission_presence", false)) if commission: continue - var pos: Vector2 = viewer._city_canvas_pos(city) + var pos: Vector2 = viewer.city_canvas_pos(city) var tier: int = int(city.get("population_tier", 1)) var radius: float = 18.0 + float(tier) * 6.0 draw_circle(pos, radius, COLOR_SHADOW) @@ -241,7 +241,7 @@ func _draw_corp_presence(markers: Dictionary) -> void: for city: Dictionary in markers.get("cities", []): if not bool(city.get("commission_presence", false)): continue - var pos: Vector2 = viewer._city_canvas_pos(city) + var pos: Vector2 = viewer.city_canvas_pos(city) draw_rect(Rect2(pos - Vector2(3, 3), Vector2(6, 6)), COLOR_CORP) diff --git a/client/ui/implant/atlas_overlay_bar.gd b/client/ui/implant/atlas_overlay_bar.gd index 50bce34df..01f97534e 100644 --- a/client/ui/implant/atlas_overlay_bar.gd +++ b/client/ui/implant/atlas_overlay_bar.gd @@ -1,45 +1,17 @@ -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 +## This script has no `class_name` on purpose: the owner (AtlasViewer) needs to +## pass the viewer reference to _init() and a `class_name` + required-arg +## _init() combo is a Godot editor footgun (review #8). Instance it via +## load("res://ui/implant/atlas_overlay_bar.gd").new(self) like AtlasViewer +## itself is instanced from AtlasPanel. ## -## 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, 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. Needs corporate contact (D-181 semi-private)."}, - {"id": "production_vs_baseline", "label": "BSL", "group": "locked", - "tooltip": "Production vs baseline — LOCKED. Needs insider access (D-181 private)."}, -] +## Layout: [ALWAYS-ON] [TOGGLEABLE] [LOCKED]. Each row in viewer.get_overlay_defs() +## produces exactly one button, so adding or retiring an overlay is a one-file +## change — the bar and marker renderer stay in lockstep (review #7). const COLOR_PINNED: Color = Color("#c8d0e0") const COLOR_ACTIVE: Color = Color("#f0d060") @@ -50,14 +22,16 @@ var _viewer = null var _buttons: Dictionary = {} # overlay_id -> Button -func _init(viewer_ref) -> void: +func _init(viewer_ref = null) -> 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: + if _viewer == null: + return + for def: Dictionary in _viewer.get_overlay_defs(): var b: Button = Button.new() b.text = def["label"] b.tooltip_text = def["tooltip"] @@ -68,17 +42,14 @@ func _ready() -> void: match def["group"]: "always": - b.disabled = false + # Disabled + pressed = visually pinned, no click handler churn + # (review #16). Theme font_disabled_color is overridden so the + # button still reads as "on" rather than greyed-out. + b.disabled = true 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.button_pressed = _viewer.is_overlay_visible(def["id"]) 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"])) @@ -90,12 +61,6 @@ func _ready() -> void: _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) @@ -104,7 +69,7 @@ func _on_toggle_changed(pressed: bool, overlay_id: String) -> void: func sync_from_viewer() -> void: if _viewer == null: return - for def: Dictionary in OVERLAY_DEFS: + for def: Dictionary in _viewer.get_overlay_defs(): if def["group"] != "toggle": continue var b: Button = _buttons.get(def["id"]) diff --git a/client/ui/implant/atlas_panel.gd b/client/ui/implant/atlas_panel.gd index eaff87e13..5dfa901bd 100644 --- a/client/ui/implant/atlas_panel.gd +++ b/client/ui/implant/atlas_panel.gd @@ -69,6 +69,7 @@ var _picker_panel = null # ImplantPanel — level 0 system selector var _picker_nav_row = null # ImplantDataRow — nav hint text, updated on navigate var _body_panel = null # ImplantPanel — level 2 body entry var _station_panel = null # ImplantPanel — station mini, shown in level 1 on click +var _screen_header: ImplantHeader = null # D-169-composed title/hint row (top-left) var _viewer = null # AtlasViewer — level 3 heightmap viewer (#835) @@ -88,6 +89,7 @@ func _ready() -> void: _implant_theme = load("res://ui/implant/default_implant.tres") _load_system_list() + _build_screen_header() _build_picker_panel() _build_body_panel() _build_station_panel() @@ -289,22 +291,10 @@ func _draw() -> void: func _draw_picker_bg() -> void: draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG) - _draw_header("ATLAS — SYSTEM SELECTION", "select a system · ◄ ► cycle · enter open orbital view · esc close") func _draw_body_bg() -> void: draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG) - var sys: Dictionary = _current_system() - _draw_header( - "ATLAS — BODY ENTRY · " + sys.get("proper_name", sys.get("system_id", "—")).to_upper(), - "enter view atlas · esc back to orbital" - ) - - -func _draw_header(title: String, hint: String) -> void: - var font := get_theme_default_font() - draw_string(font, Vector2(16, 28), title, HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_TEXT) - draw_string(font, Vector2(16, 44), hint, HORIZONTAL_ALIGNMENT_LEFT, -1, 9, COLOR_TEXT_DIM) func _draw_orbital() -> void: @@ -326,22 +316,53 @@ func _draw_orbital() -> void: # Body dots + labels _draw_bodies() - # Header - var sys: Dictionary = _current_system() - var sys_name: String = sys.get("proper_name", sys.get("system_id", "—")) - var star_type: String = sys.get("star_type", "") - var top_count: int = 0 - for b: Dictionary in _orbital_bodies: - if b.get("parent_body_id") == null: - top_count += 1 - var subtitle: String = "" - if not star_type.is_empty(): - subtitle = star_type + " · " - subtitle += "%d orbital bodies · %d stations" % [top_count, _orbital_stations.size()] - _draw_header( - "ATLAS — ORBITAL VIEW · " + sys_name.to_upper(), - subtitle + " · click body → atlas entry · esc back" - ) + +func _build_screen_header() -> void: + # D-169: the top-of-screen title / hint composes from ImplantHeader so the + # implant theme drives its fonts and semantic colors. Review #4 flagged the + # original draw_string() approach as a theme-swap invariant violation. + _screen_header = ImplantHeader.new() + _screen_header.position = Vector2(PANEL_MARGIN, 16.0) + _screen_header.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(_screen_header) + if _implant_theme: + _screen_header.apply_implant_theme(_implant_theme) + + +func _refresh_screen_header() -> void: + if _screen_header == null: + return + var title: String = "" + var hint: String = "" + match _level: + Level.SYSTEM_PICKER: + title = "ATLAS — SYSTEM SELECTION" + hint = "select a system · ◄ ► cycle · enter open orbital view · esc close" + Level.ORBITAL_DIAGRAM: + var sys: Dictionary = _current_system() + var sys_name: String = str(sys.get("proper_name", sys.get("system_id", "—"))) + title = "ATLAS — ORBITAL VIEW · " + sys_name.to_upper() + var star_type: String = str(sys.get("star_type", "")) + var top_count: int = 0 + for b: Dictionary in _orbital_bodies: + if b.get("parent_body_id") == null: + top_count += 1 + var subtitle: String = "" + if not star_type.is_empty(): + subtitle = star_type + " · " + subtitle += "%d orbital bodies · %d stations" % [top_count, _orbital_stations.size()] + hint = subtitle + " · click body → atlas entry · esc back" + Level.BODY_ENTRY: + var sys2: Dictionary = _current_system() + var sys2_name: String = str(sys2.get("proper_name", sys2.get("system_id", "—"))) + title = "ATLAS — BODY ENTRY · " + sys2_name.to_upper() + hint = "enter view atlas · esc back to orbital" + Level.HEIGHTMAP_VIEWER: + # Viewer owns its own header while active. + title = "" + hint = "" + _screen_header.set_content(title, hint) + _screen_header.visible = (_level != Level.HEIGHTMAP_VIEWER) func _draw_orbit_rings(center: Vector2) -> void: @@ -482,6 +503,7 @@ func _navigate_system(delta: int) -> void: return _selected_idx = wrapi(_selected_idx + delta, 0, _systems.size()) _rebuild_picker_panel() + _refresh_screen_header() func _handle_orbital_click(pos: Vector2) -> void: @@ -561,6 +583,7 @@ func _show_level(level: Level) -> void: if _viewer: _viewer.visible = (level == Level.HEIGHTMAP_VIEWER) + _refresh_screen_header() queue_redraw() diff --git a/client/ui/implant/atlas_viewer.gd b/client/ui/implant/atlas_viewer.gd index f19d3f0a6..3eccf1d0d 100644 --- a/client/ui/implant/atlas_viewer.gd +++ b/client/ui/implant/atlas_viewer.gd @@ -50,6 +50,36 @@ const COLOR_TEXT: Color = Color("#c8d0e0") const COLOR_TEXT_DIM: Color = Color("#667788") const COLOR_EMPTY_NOTICE: Color = Color("#445566") +# ── Overlay definitions (single source of truth, review #7) ───────────────── +## Overlay catalogue consumed by both AtlasMarkerOverlay (renders) and +## AtlasOverlayBar (exposes as toggle buttons). Groups map to the D-181 signal +## visibility ladder: always = public, toggle = observable, locked = semi- +## private / private. Keep this table in sync with D-191 §7. +const OVERLAY_DEFS: Array = [ + {"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, 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. Needs corporate contact (D-181 semi-private)."}, + {"id": "production_vs_baseline", "label": "BSL", "group": "locked", + "tooltip": "Production vs baseline — LOCKED. Needs insider access (D-181 private)."}, +] + # ── Context (set by AtlasPanel.show_body) ───────────────────────────────────── var _body: Dictionary = {} var _system: Dictionary = {} @@ -75,33 +105,19 @@ var _selected_city: Dictionary = {} var _hovered_city: Dictionary = {} # ── Overlay visibility (#836 plugs in here) ─────────────────────────────────── -## Map of overlay id → bool. Always-on overlays are true by default; deferred -## overlays are locked (see _overlay_locked). AtlasOverlayBar in #836 writes -## into this dict via set_overlay_visible(). -var _overlay_visibility: Dictionary = { - "terrain": true, - "infrastructure": true, - "named_features": true, - "gate_markers": true, - "political_zones": true, - "population_density": false, - "production_zones": false, - "shadow_economy": false, - "corp_presence": false, - "stockpile_weeks": false, - "production_vs_baseline": false, -} -var _overlay_locked: Dictionary = { - "stockpile_weeks": true, - "production_vs_baseline": true, -} +## Runtime state derived from OVERLAY_DEFS in _ready() — always-on overlays +## start true, toggleables false, locked entries are tracked separately and +## can't be flipped by set_overlay_visible(). +var _overlay_visibility: Dictionary = {} +var _overlay_locked: Dictionary = {} # ── Child nodes ─────────────────────────────────────────────────────────────── var _canvas: Node2D = null # transformed node holding heightmap + markers 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) +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) func _ready() -> void: @@ -114,6 +130,12 @@ func _ready() -> void: _implant_theme = load("res://ui/implant/default_implant.tres") + for def: Dictionary in OVERLAY_DEFS: + var group: String = def["group"] + _overlay_visibility[def["id"]] = (group == "always") + if group == "locked": + _overlay_locked[def["id"]] = true + # Build transformed canvas that holds heightmap + marker overlay _canvas = Node2D.new() _canvas.name = "AtlasCanvas" @@ -124,6 +146,7 @@ func _ready() -> void: _overlay_node.viewer = self _canvas.add_child(_overlay_node) + _build_screen_header() _build_city_panel() _build_empty_notice() _build_overlay_bar() @@ -138,6 +161,7 @@ func show_body(body: Dictionary, system: Dictionary) -> void: _load_heightmap() _load_markers() _fit_to_view() + _refresh_screen_header() _city_panel.visible = false _empty_notice.visible = (_heightmap_texture == null) grab_focus() @@ -150,6 +174,7 @@ func set_overlay_visible(overlay_id: String, visible_state: bool) -> void: if _overlay_locked.get(overlay_id, false): return if not _overlay_visibility.has(overlay_id): + push_warning("AtlasViewer: unknown overlay id '%s'" % overlay_id) return _overlay_visibility[overlay_id] = visible_state _overlay_node.queue_redraw() @@ -171,6 +196,18 @@ func get_markers() -> Dictionary: return _markers +func get_hovered_city() -> Dictionary: + return _hovered_city + + +func get_selected_city() -> Dictionary: + return _selected_city + + +func get_overlay_defs() -> Array: + return OVERLAY_DEFS + + # ============================================================================= # Data loading # ============================================================================= @@ -300,20 +337,28 @@ func screen_to_canvas(screen_point: Vector2) -> Vector2: func _draw() -> void: draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG) - _draw_header() - if _heightmap_texture != null: - _canvas.position = _view_offset - _canvas.scale = Vector2(_view_zoom, _view_zoom) -func _draw_header() -> void: - var font := get_theme_default_font() +func _build_screen_header() -> void: + # D-169: compose the title + hint from ImplantHeader so the implant theme + # drives fonts/colors. Review #4 flagged the draw_string() approach as a + # theme-swap invariant violation. + _screen_header = ImplantHeader.new() + _screen_header.position = Vector2(PANEL_MARGIN, 16.0) + _screen_header.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(_screen_header) + if _implant_theme: + _screen_header.apply_implant_theme(_implant_theme) + + +func _refresh_screen_header() -> void: + if _screen_header == null: + return var body_name: String = _dict_str(_body, "proper_name", _dict_str(_body, "body_id", "—")) var sys_name: String = _dict_str(_system, "proper_name", _dict_str(_system, "system_id", "—")) var title: String = "ATLAS — %s · %s" % [body_name.to_upper(), sys_name.to_upper()] - draw_string(font, Vector2(16, 28), title, HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_TEXT) var hint: String = "drag pan · wheel zoom · r reset · click city data · esc back" - draw_string(font, Vector2(16, 44), hint, HORIZONTAL_ALIGNMENT_LEFT, -1, 9, COLOR_TEXT_DIM) + _screen_header.set_content(title, hint) ## Safely extract a string field from a dict, falling back when missing or null. @@ -400,7 +445,7 @@ func _find_city_at(screen_pos: Vector2) -> Dictionary: return {} var hit_radius: float = 12.0 for c: Dictionary in cities: - var canvas_pt: Vector2 = _city_canvas_pos(c) + var canvas_pt: Vector2 = city_canvas_pos(c) var screen_pt: Vector2 = canvas_to_screen(canvas_pt) if screen_pos.distance_to(screen_pt) <= hit_radius: return c @@ -408,8 +453,9 @@ func _find_city_at(screen_pos: Vector2) -> Dictionary: ## Extract canvas-space position for a city marker. Supports `pos: [x,y]`, -## `lat`/`lon` (equirectangular), and legacy `center: [row,col]`. -func _city_canvas_pos(city: Dictionary) -> Vector2: +## `lat`/`lon` (equirectangular), and legacy `center: [row,col]`. Public so +## AtlasMarkerOverlay doesn't need to reach into private state (review #5). +func city_canvas_pos(city: Dictionary) -> Vector2: if city.has("pos") and city["pos"] is Array and city["pos"].size() >= 2: return grid_to_canvas(Vector2(float(city["pos"][0]), float(city["pos"][1]))) if city.has("lat") and city.has("lon"): @@ -499,11 +545,16 @@ func _unhandled_key_input(event: InputEvent) -> void: if not (event is InputEventKey) or not event.pressed or event.is_echo(): return var ek := event as InputEventKey - if ek.keycode == KEY_N and _selected_city.size() > 0: + if ek.keycode != KEY_N: + return + # Always consume KEY_N while the viewer is visible — otherwise it would + # fall through to main.gd's global economics monitor toggle and close the + # fullscreen atlas as a side effect (review #6). + get_viewport().set_input_as_handled() + if _selected_city.size() > 0: var sys_id: String = _system.get("system_id", "") if not sys_id.is_empty(): economics_link_requested.emit(sys_id) - get_viewport().set_input_as_handled() # ============================================================================= @@ -544,7 +595,8 @@ func _position_empty_notice() -> void: func _build_overlay_bar() -> void: - _overlay_bar = AtlasOverlayBar.new(self) + var BarScript := load("res://ui/implant/atlas_overlay_bar.gd") + _overlay_bar = BarScript.new(self) _overlay_bar.name = "OverlayBar" add_child(_overlay_bar) _position_overlay_bar()