refactor(ui): atlas review warnings — headers, public API, KEY_N, overlay source of truth (#128)
4. AtlasPanel and AtlasViewer now compose their title + hint from an
ImplantHeader child rather than hand-rolling them via draw_string, so the
D-169 "theme swap changes the implant hardware appearance" invariant
holds end-to-end. _refresh_screen_header() drives content per level and
on system navigation.
5. AtlasViewer exposes city_canvas_pos(), get_hovered_city(),
get_selected_city(), and get_overlay_defs() as public API — the marker
overlay no longer reaches into underscore-prefixed state, which is
especially important because viewer is an untyped var in the overlay.
6. KEY_N now consumes unconditionally while the viewer is visible, and
main.gd's global economics-monitor toggle is gated on
!HudGroups.is_app_active("implant/map/atlas"). Previously pressing N
without a selected city fell through and closed the fullscreen atlas as
a side effect.
7. OVERLAY_DEFS lives in AtlasViewer as the single source of truth.
AtlasOverlayBar reads the list via viewer.get_overlay_defs(), and
AtlasViewer derives _overlay_visibility / _overlay_locked from the same
table at _ready() — no more hand-maintained parallel lists, so the bar
and the guard in set_overlay_visible can't drift.
8. AtlasOverlayBar drops `class_name`: it now loads via
load("res://ui/implant/atlas_overlay_bar.gd") from AtlasViewer, the same
pattern AtlasPanel uses for AtlasViewer. _init(viewer_ref = null) keeps
the required-arg footgun off the editor's introspection path.
9. `star-map-data` make target added to regenerate
client/data/star_map_data.json from systems.db + wiki, and
`check-star-map` wired into pre-pr-validate + pre-pr-client so any
commit that touches the generator (or any downstream systems.db change
like server #839) fails pre-pr until the JSON is regenerated. The
terrain_reference data-availability dependency is no longer tribal
knowledge.
Also addresses review #15 (push_warning on unknown overlay id in
set_overlay_visible) and #16 (disabled always-on buttons drop handler
churn) as part of the same refactor.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user