Files
settled-reach/client/ui/implant/atlas_panel.gd
T

791 lines
27 KiB
GDScript

class_name AtlasPanel
extends Control
## Atlas implant panel — 4-level navigation (#834, #835):
## system picker → orbital diagram → body entry → regional viewer.
## FULLSCREEN implant app (z=20) at implant/map/atlas per D-170.
## Uses ImplantPanel component library (D-169). Data from star_map_data.json.
##
## Navigation:
## Level 0 SYSTEM_PICKER — ◄ ► cycle systems, Enter to open orbital view
## Level 1 ORBITAL_DIAGRAM — rendered via _draw(), click body → level 2, click station → mini panel
## Level 2 BODY_ENTRY — body info panel, Enter to open heightmap viewer, Esc back
## Level 3 HEIGHTMAP_VIEWER — AtlasViewer with pan/zoom + markers + city data (#835), Esc back
## Emitted when the viewer's city-data panel requests the economics monitor for
## the current system. main.gd bridges this to EconomicsPanel.select_system()
## + HudGroups.open_app("implant/economics") — D-191 cross-panel integration.
signal economics_link_requested(system_id: String)
enum Level { SYSTEM_PICKER = 0, ORBITAL_DIAGRAM = 1, BODY_ENTRY = 2, HEIGHTMAP_VIEWER = 3 }
const APP_PATH := "implant/map/atlas"
const STAR_MAP_DATA := "res://data/star_map_data.json"
const PANEL_WIDTH: float = 320.0
const PANEL_MARGIN: float = 16.0
# ── Orbital diagram geometry ──────────────────────────────────────────────────
const ORBITAL_CENTER_FRACTION := Vector2(0.5, 0.55)
const STAR_RADIUS: float = 12.0
const ORBITAL_RING_BASE: float = 65.0 # innermost planet ring radius (px)
const ORBITAL_RING_STEP: float = 58.0 # radial gap between orbit rings
const MOON_ORBIT_RADIUS: float = 24.0 # sub-orbit radius for moons around parent
const STATION_SIZE: float = 6.0 # station marker half-size
const BODY_HIT_RADIUS: float = 16.0 # click/hover detection radius
const LABEL_OFFSET: float = 11.0 # px below body dot for label
# ── Colors ────────────────────────────────────────────────────────────────────
const COLOR_BG: Color = Color("#0d1117")
const COLOR_STAR: Color = Color("#f0d060")
const COLOR_PLANET_INHABITED: Color = Color("#44aa66")
const COLOR_PLANET_HABITABLE: Color = Color("#4488aa")
const COLOR_PLANET_BARE: Color = Color("#556677")
const COLOR_MOON: Color = Color("#3a4a55")
const COLOR_OORT: Color = Color("#253040")
const COLOR_STATION: Color = Color("#f0d060")
const COLOR_RING: Color = Color(1.0, 1.0, 1.0, 0.06)
const COLOR_TEXT: Color = Color("#c8d0e0")
const COLOR_TEXT_DIM: Color = Color("#667788")
# ── Navigation state ──────────────────────────────────────────────────────────
var _level: Level = Level.SYSTEM_PICKER
var _systems: Array = [] # Array[Dictionary] from star_map_data.json
var _selected_idx: int = 0 # index into _systems
var _selected_body: Dictionary = {} # body dict at Level.BODY_ENTRY
# ── Orbital diagram runtime state ─────────────────────────────────────────────
var _orbital_bodies: Array = [] # orbit_bodies for current system
var _orbital_stations: Array = [] # stations for current system
var _body_positions: Dictionary = {} # body_id -> Vector2
var _station_positions: Dictionary = {} # station_id -> Vector2
var _hovered_body: String = ""
var _hovered_station: String = ""
var _selected_station: Dictionary = {} # station clicked in orbital view
var _dirty: bool = true
# ── Visual components (D-169 ImplantPanel library) ────────────────────────────
var _implant_theme = null # ImplantTheme — loaded at runtime (autoload parse-order rule)
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)
func _ready() -> void:
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = Control.GROW_DIRECTION_BOTH
grow_vertical = Control.GROW_DIRECTION_BOTH
visible = false
mouse_filter = Control.MOUSE_FILTER_STOP
# D-170: Register with HUD layer groups
HudGroups.register(self, APP_PATH)
HudGroups.app_changed.connect(_on_app_changed)
_implant_theme = load("res://ui/implant/default_implant.tres")
_load_system_list()
_build_screen_header()
_build_picker_panel()
_build_body_panel()
_build_station_panel()
_build_heightmap_viewer()
_show_level(Level.SYSTEM_PICKER)
func _process(_delta: float) -> void:
if not visible:
return
if _dirty:
queue_redraw()
_dirty = false
## Toggle atlas panel. Called from main.gd on KEY_A.
func toggle_visible() -> void:
HudGroups.toggle_app(APP_PATH, HudGroups.Mode.FULLSCREEN)
## Called from main.gd when insert state changes.
func set_insert_active(active: bool) -> void:
if not active and HudGroups.is_app_active(APP_PATH):
HudGroups.close_app()
func _on_app_changed(app_path: String, mode: int) -> void:
if app_path != APP_PATH:
return
if mode == HudGroups.Mode.FULLSCREEN:
visible = true
_dirty = true
else:
visible = false
# =============================================================================
# Data loading
# =============================================================================
func _load_system_list() -> void:
if not FileAccess.file_exists(STAR_MAP_DATA):
push_warning("AtlasPanel: %s not found" % STAR_MAP_DATA)
return
var file := FileAccess.open(STAR_MAP_DATA, FileAccess.READ)
if file == null:
return
var parsed: Variant = JSON.parse_string(file.get_as_text())
file.close()
if not (parsed is Dictionary):
return
for node: Dictionary in parsed.get("nodes", []):
if not node.get("system_id", "").is_empty():
_systems.append(node)
_systems.sort_custom(
func(a: Dictionary, b: Dictionary) -> bool:
var na: String = a.get("proper_name", a.get("system_id", ""))
var nb: String = b.get("proper_name", b.get("system_id", ""))
return na < nb
)
func _current_system() -> Dictionary:
if _systems.is_empty():
return {}
_selected_idx = clampi(_selected_idx, 0, _systems.size() - 1)
return _systems[_selected_idx]
func _enter_orbital_diagram() -> void:
var sys: Dictionary = _current_system()
_orbital_bodies = sys.get("orbit_bodies", [])
_orbital_stations = sys.get("stations", [])
_hovered_body = ""
_hovered_station = ""
_selected_station = {}
_compute_body_positions()
if _station_panel:
_station_panel.visible = false
_show_level(Level.ORBITAL_DIAGRAM)
func _enter_body_entry(body: Dictionary) -> void:
_selected_body = body
_rebuild_body_panel()
_show_level(Level.BODY_ENTRY)
func _open_heightmap_viewer() -> void:
if _viewer == null:
return
_viewer.show_body(_selected_body, _current_system())
_show_level(Level.HEIGHTMAP_VIEWER)
# =============================================================================
# Orbital geometry
# =============================================================================
func _compute_body_positions() -> void:
_body_positions.clear()
_station_positions.clear()
var sz: Vector2 = get_rect().size
# Fall back to design-time size if rect not available yet
if sz == Vector2.ZERO:
sz = Vector2(1280.0, 720.0)
var center: Vector2 = sz * ORBITAL_CENTER_FRACTION
# Separate top-level bodies (no parent) from moons
var top_bodies: Array = []
for b: Dictionary in _orbital_bodies:
if b.get("parent_body_id") == null:
top_bodies.append(b)
top_bodies.sort_custom(
func(a: Dictionary, b: Dictionary) -> bool:
return int(a.get("orbit_index", 0)) < int(b.get("orbit_index", 0))
)
# Group by orbit_index and distribute evenly on each ring
var by_orbit: Dictionary = {}
for b: Dictionary in top_bodies:
var idx: int = int(b.get("orbit_index", 0))
if not by_orbit.has(idx):
by_orbit[idx] = []
by_orbit[idx].append(b)
for orbit_idx: int in by_orbit:
var ring_bodies: Array = by_orbit[orbit_idx]
var ring_r: float = ORBITAL_RING_BASE + (orbit_idx - 1) * ORBITAL_RING_STEP
var count: int = ring_bodies.size()
for i: int in range(count):
var b: Dictionary = ring_bodies[i]
var bid: String = str(b.get("body_id", ""))
if bid.is_empty():
continue
var angle: float
if count == 1:
angle = -PI / 2.0 # top position (12 o'clock)
else:
angle = -PI / 2.0 + TAU * float(i) / float(count)
_body_positions[bid] = center + Vector2(cos(angle), sin(angle)) * ring_r
# Place moons near their parent body. Moons per parent count drives the
# angular spacing — a hard-coded divisor made the 5th+ moon overlap moon 1
# and become unclickable on gas giants with many satellites (review #1).
var moons_by_parent: Dictionary = {}
for b: Dictionary in _orbital_bodies:
var parent_id: Variant = b.get("parent_body_id")
if parent_id == null:
continue
var key: String = str(parent_id)
if not moons_by_parent.has(key):
moons_by_parent[key] = []
moons_by_parent[key].append(b)
for parent_key: String in moons_by_parent:
if not _body_positions.has(parent_key):
continue
var siblings: Array = moons_by_parent[parent_key]
siblings.sort_custom(
func(a: Dictionary, b: Dictionary) -> bool:
return int(a.get("orbit_index", 0)) < int(b.get("orbit_index", 0))
)
var parent_pos: Vector2 = _body_positions[parent_key]
var count: int = siblings.size()
for i: int in range(count):
var moon: Dictionary = siblings[i]
var moon_id: String = str(moon.get("body_id", ""))
if moon_id.is_empty():
continue
var angle: float = -PI / 2.0 + TAU * float(i) / float(count)
_body_positions[moon_id] = (
parent_pos + Vector2(cos(angle), sin(angle)) * MOON_ORBIT_RADIUS
)
# Place stations near their parent body (offset right + slightly up)
for s: Dictionary in _orbital_stations:
var station_id: String = str(s.get("station_id", ""))
if station_id.is_empty():
continue
var parent_id: Variant = s.get("orbits_body_id")
var station_parent_pos: Vector2
if parent_id != null and _body_positions.has(str(parent_id)):
station_parent_pos = _body_positions[str(parent_id)]
else:
station_parent_pos = center # fallback to star position
_station_positions[station_id] = station_parent_pos + Vector2(20.0, -10.0)
# =============================================================================
# Drawing
# =============================================================================
func _draw() -> void:
match _level:
Level.SYSTEM_PICKER:
_draw_picker_bg()
Level.ORBITAL_DIAGRAM:
_draw_orbital()
Level.BODY_ENTRY:
_draw_body_bg()
Level.HEIGHTMAP_VIEWER:
pass # AtlasViewer draws its own background
func _draw_picker_bg() -> void:
draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG)
func _draw_body_bg() -> void:
draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG)
func _draw_orbital() -> void:
var sz: Vector2 = get_rect().size
var center: Vector2 = sz * ORBITAL_CENTER_FRACTION
draw_rect(Rect2(Vector2.ZERO, sz), COLOR_BG)
# Orbit rings for top-level bodies
_draw_orbit_rings(center)
# Star glow + body
draw_circle(center, STAR_RADIUS + 4.0, Color(COLOR_STAR.r, COLOR_STAR.g, COLOR_STAR.b, 0.18))
draw_circle(center, STAR_RADIUS, COLOR_STAR)
# Station markers (behind body dots)
_draw_stations()
# Body dots + labels
_draw_bodies()
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:
# Draw one ring per unique orbit_index of top-level bodies
var seen_orbits: Dictionary = {}
for b: Dictionary in _orbital_bodies:
if b.get("parent_body_id") != null:
continue
var idx: int = int(b.get("orbit_index", 0))
if seen_orbits.has(idx):
continue
seen_orbits[idx] = true
var r: float = ORBITAL_RING_BASE + (idx - 1) * ORBITAL_RING_STEP
draw_arc(center, r, 0.0, TAU, 64, COLOR_RING, 0.5, true)
func _draw_bodies() -> void:
var font := get_theme_default_font()
for b: Dictionary in _orbital_bodies:
var bid: String = str(b.get("body_id", ""))
if bid.is_empty() or not _body_positions.has(bid):
continue
var pos: Vector2 = _body_positions[bid]
var body_type: String = b.get("body_type", "")
var atmo: String = b.get("atmosphere", "none")
var inhabited: bool = bool(b.get("inhabited", false))
var color: Color
var radius: float
match body_type:
"moon":
color = COLOR_MOON
radius = 3.5
"oort_cloud":
# Oort cloud shown as a faint dashed circle suggestion, not a dot
color = COLOR_OORT
radius = 2.0
_:
if inhabited:
color = COLOR_PLANET_INHABITED
radius = 6.0
elif atmo in ["breathable", "standard"]:
color = COLOR_PLANET_HABITABLE
radius = 5.5
else:
color = COLOR_PLANET_BARE
radius = 4.5
# Hover highlight ring
if bid == _hovered_body:
draw_arc(pos, radius + 5.0, 0.0, TAU, 20, Color(1.0, 1.0, 1.0, 0.25), 1.0, true)
draw_circle(pos, radius, color)
# Label: always for inhabited, on hover otherwise
var label: String = b.get("proper_name", bid) if b.get("proper_name") else bid
var show_label: bool = inhabited or bid == _hovered_body
if show_label:
var lcolor: Color = COLOR_TEXT if bid == _hovered_body else COLOR_TEXT_DIM
var lsz: Vector2 = font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, 9)
var lpos: Vector2 = pos + Vector2(-lsz.x / 2.0, radius + LABEL_OFFSET)
draw_string(font, lpos, label, HORIZONTAL_ALIGNMENT_LEFT, -1, 9, lcolor)
func _draw_stations() -> void:
var font := get_theme_default_font()
for s: Dictionary in _orbital_stations:
var sid: String = str(s.get("station_id", ""))
if sid.is_empty() or not _station_positions.has(sid):
continue
var pos: Vector2 = _station_positions[sid]
var half: float = STATION_SIZE / 2.0
var rect: Rect2 = Rect2(pos - Vector2(half, half), Vector2(STATION_SIZE, STATION_SIZE))
if sid == _hovered_station:
draw_rect(
Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)),
Color(1.0, 1.0, 1.0, 0.18)
)
draw_rect(rect, COLOR_STATION)
# Label on hover
if sid == _hovered_station:
var label: String = s.get("proper_name", sid) if s.get("proper_name") else sid
var lsz: Vector2 = font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, 9)
draw_string(
font,
pos + Vector2(-lsz.x / 2.0, half + 8.0),
label,
HORIZONTAL_ALIGNMENT_LEFT,
-1,
9,
COLOR_TEXT_DIM
)
# =============================================================================
# Input
# =============================================================================
func _gui_input(event: InputEvent) -> void:
if event is InputEventKey and (event as InputEventKey).pressed and not event.is_echo():
_handle_key(event as InputEventKey)
elif _level == Level.ORBITAL_DIAGRAM:
if event is InputEventMouseButton and (event as InputEventMouseButton).pressed:
_handle_orbital_click((event as InputEventMouseButton).position)
elif event is InputEventMouseMotion:
_handle_orbital_hover((event as InputEventMouseMotion).position)
func _handle_key(event: InputEventKey) -> void:
if _level == Level.HEIGHTMAP_VIEWER:
# Viewer handles its own input via _gui_input; don't double-process.
return
match event.keycode:
KEY_ESCAPE:
_navigate_back()
KEY_B:
HudGroups.close_app()
KEY_LEFT:
if _level == Level.SYSTEM_PICKER:
_navigate_system(-1)
KEY_RIGHT:
if _level == Level.SYSTEM_PICKER:
_navigate_system(1)
KEY_ENTER, KEY_KP_ENTER:
if _level == Level.SYSTEM_PICKER:
_enter_orbital_diagram()
elif _level == Level.BODY_ENTRY:
_open_heightmap_viewer()
func _navigate_back() -> void:
match _level:
Level.SYSTEM_PICKER:
HudGroups.close_app()
Level.ORBITAL_DIAGRAM:
_show_level(Level.SYSTEM_PICKER)
Level.BODY_ENTRY:
_show_level(Level.ORBITAL_DIAGRAM)
Level.HEIGHTMAP_VIEWER:
_show_level(Level.BODY_ENTRY)
func _navigate_system(delta: int) -> void:
if _systems.is_empty():
return
_selected_idx = wrapi(_selected_idx + delta, 0, _systems.size())
_rebuild_picker_panel()
_refresh_screen_header()
func _handle_orbital_click(pos: Vector2) -> void:
# Bodies take priority over stations
var bid: String = _find_nearest_body(pos)
if not bid.is_empty():
for b: Dictionary in _orbital_bodies:
if str(b.get("body_id", "")) == bid:
_enter_body_entry(b)
return
# Station click → show mini panel (no drill-down per D-191)
var sid: String = _find_nearest_station(pos)
if not sid.is_empty():
for s: Dictionary in _orbital_stations:
if str(s.get("station_id", "")) == sid:
_selected_station = s
_rebuild_station_panel()
if _station_panel:
_station_panel.visible = true
return
# Click on empty space — dismiss station panel
_selected_station = {}
if _station_panel:
_station_panel.visible = false
_dirty = true
func _handle_orbital_hover(pos: Vector2) -> void:
var new_body: String = _find_nearest_body(pos)
var new_station: String = "" if not new_body.is_empty() else _find_nearest_station(pos)
if new_body != _hovered_body or new_station != _hovered_station:
_hovered_body = new_body
_hovered_station = new_station
_dirty = true
func _find_nearest_body(pos: Vector2) -> String:
var best_dist: float = BODY_HIT_RADIUS
var best_id: String = ""
for bid: String in _body_positions:
var d: float = pos.distance_to(_body_positions[bid])
if d < best_dist:
best_dist = d
best_id = bid
return best_id
func _find_nearest_station(pos: Vector2) -> String:
var best_dist: float = BODY_HIT_RADIUS
var best_id: String = ""
for sid: String in _station_positions:
var d: float = pos.distance_to(_station_positions[sid])
if d < best_dist:
best_dist = d
best_id = sid
return best_id
# =============================================================================
# Level switching — show/hide panels
# =============================================================================
func _show_level(level: Level) -> void:
_level = level
_dirty = true
if _picker_panel:
_picker_panel.visible = (level == Level.SYSTEM_PICKER)
if _body_panel:
_body_panel.visible = (level == Level.BODY_ENTRY)
# Station panel managed separately — remains hidden until a station click
if _station_panel and level != Level.ORBITAL_DIAGRAM:
_station_panel.visible = false
if _viewer:
_viewer.visible = (level == Level.HEIGHTMAP_VIEWER)
_refresh_screen_header()
queue_redraw()
# =============================================================================
# Panel construction — D-169 ImplantPanel component library
# =============================================================================
func _build_picker_panel() -> void:
_picker_panel = ImplantPanel.new()
_picker_panel.name = "PickerPanel"
_picker_panel.theme_resource = _implant_theme
_picker_panel.custom_minimum_size.x = PANEL_WIDTH
_picker_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
_picker_panel.position = Vector2(PANEL_MARGIN, 60.0)
add_child(_picker_panel)
_rebuild_picker_panel()
func _rebuild_picker_panel() -> void:
if not _picker_panel:
return
_picker_panel.clear()
var sys: Dictionary = _current_system()
var sys_name: String = sys.get("proper_name", sys.get("system_id", "—"))
var sys_id: String = sys.get("system_id", "")
var sector: String = sys.get("geographic_sector", "").replace("_", " ").to_upper()
var czone: String = sys.get("currency_zone", "").replace("_", " ")
var total: int = _systems.size()
_picker_panel.add_component(ImplantHeader.new("ATLAS", sys_name))
_picker_panel.add_component(ImplantSeparator.new())
_picker_nav_row = ImplantDataRow.new("◄ ► [%d / %d]" % [_selected_idx + 1, total])
_picker_panel.add_component(_picker_nav_row)
_picker_panel.add_component(ImplantSeparator.new())
_picker_panel.add_component(ImplantDataRow.new(sys_id))
if not sector.is_empty():
_picker_panel.add_component(ImplantDataRow.new(sector + " CORRIDOR"))
if not czone.is_empty():
_picker_panel.add_component(ImplantDataRow.new("currency " + czone))
_picker_panel.add_component(ImplantDataRow.new(sys.get("bodies", "—")))
_picker_panel.add_component(ImplantDataRow.new("pop " + sys.get("population", "—")))
_picker_panel.add_component(ImplantSeparator.new())
_picker_panel.add_component(ImplantTextBlock.new("enter open orbital map"))
_picker_panel.add_component(ImplantTextBlock.new("esc close atlas"))
_dirty = true
func _build_body_panel() -> void:
_body_panel = ImplantPanel.new()
_body_panel.name = "BodyPanel"
_body_panel.theme_resource = _implant_theme
_body_panel.custom_minimum_size.x = PANEL_WIDTH
_body_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
_body_panel.position = Vector2(PANEL_MARGIN, 60.0)
_body_panel.visible = false
add_child(_body_panel)
func _rebuild_body_panel() -> void:
if not _body_panel:
return
_body_panel.clear()
var b: Dictionary = _selected_body
var bid: String = b.get("body_id", "")
var name_str: String = b.get("proper_name", "") if b.get("proper_name") else bid
var body_type: String = b.get("body_type", "").replace("_", " ").to_upper()
var mass_class: String = b.get("mass_class", "") if b.get("mass_class") else ""
var atmo: String = b.get("atmosphere", "none") if b.get("atmosphere") else "none"
var inhabited: bool = bool(b.get("inhabited", false))
var pop: int = int(b.get("population", 0))
var has_heightmap: bool = b.get("terrain_reference") != null
var sys: Dictionary = _current_system()
var sys_name: String = sys.get("proper_name", sys.get("system_id", "—"))
_body_panel.add_component(ImplantHeader.new(name_str, sys_name + " system"))
_body_panel.add_component(ImplantSeparator.new())
var type_line: String = body_type
if not mass_class.is_empty():
type_line += " · " + mass_class.replace("_", " ").to_upper()
_body_panel.add_component(ImplantDataRow.new(type_line))
_body_panel.add_component(ImplantDataRow.new("atmosphere " + atmo))
if inhabited:
_body_panel.add_component(ImplantSeparator.new())
_body_panel.add_component(ImplantDataRow.new("population " + _format_pop(pop)))
_body_panel.add_component(ImplantSeparator.new())
if has_heightmap:
_body_panel.add_component(ImplantTextBlock.new("enter view heightmap atlas"))
else:
_body_panel.add_component(ImplantTextBlock.new("atlas data pending (#839)"))
_body_panel.add_component(ImplantTextBlock.new("esc back to orbital view"))
func _build_station_panel() -> void:
_station_panel = ImplantPanel.new()
_station_panel.name = "StationPanel"
_station_panel.theme_resource = _implant_theme
_station_panel.custom_minimum_size.x = PANEL_WIDTH
_station_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
_station_panel.position = Vector2(PANEL_MARGIN, 60.0)
_station_panel.visible = false
add_child(_station_panel)
func _build_heightmap_viewer() -> void:
_viewer = AtlasViewer.new()
_viewer.name = "AtlasViewer"
_viewer.visible = false
add_child(_viewer)
_viewer.back_pressed.connect(_on_viewer_back)
_viewer.economics_link_requested.connect(_on_viewer_economics_link)
func _on_viewer_back() -> void:
_show_level(Level.BODY_ENTRY)
func _on_viewer_economics_link(system_id: String) -> void:
economics_link_requested.emit(system_id)
# Close the atlas; main.gd will open economics monitor in its place.
HudGroups.close_app()
func _rebuild_station_panel() -> void:
if not _station_panel:
return
_station_panel.clear()
if _selected_station.is_empty():
return
var s: Dictionary = _selected_station
var s_name: String = (
s.get("proper_name", "") if s.get("proper_name") else s.get("station_id", "—")
)
var s_type: String = s.get("station_type", "").replace("_", " ").to_upper()
var gov: String = s.get("governance_type", "") if s.get("governance_type") else ""
var role: String = s.get("economic_role", "") if s.get("economic_role") else ""
var sys: Dictionary = _current_system()
var czone: String = sys.get("currency_zone", "") if sys.get("currency_zone") else "—"
czone = czone.replace("_", " ")
_station_panel.add_component(ImplantHeader.new(s_name, s_type + " STATION"))
_station_panel.add_component(ImplantSeparator.new())
if not gov.is_empty():
_station_panel.add_component(
ImplantDataRow.new("operator " + gov.replace("_", " ").to_upper())
)
if not role.is_empty():
_station_panel.add_component(ImplantDataRow.new("function " + role.replace("_", " ")))
_station_panel.add_component(ImplantDataRow.new("currency " + czone))
_station_panel.add_component(ImplantSeparator.new())
_station_panel.add_component(ImplantTextBlock.new("station atlas deferred"))
_station_panel.add_component(ImplantTextBlock.new("esc back"))
# =============================================================================
# Helpers
# =============================================================================
func _format_pop(pop: int) -> String:
if pop <= 0:
return "0"
var s: String = str(pop)
var result: String = ""
var count: int = 0
for i: int in range(s.length() - 1, -1, -1):
if count > 0 and count % 3 == 0:
result = "," + result
result = s[i] + result
count += 1
return result