Every screen-targeted intent now routes through one _require_current_screen() check and returns the structured error shape instead of silently mutating an off-screen viewer (hoshe's finding: scroll_rung from the reach screen fired real IPC and reported ok). The reference driver's fixed 4-frame settle becomes is_pending()-aware with a 600-frame bound, the keep-waiting decision extracted as a pure testable function — restoring the proven eyeball-driver discipline. The terrain_reference guard moves into AtlasApp._on_body_selected(), the shared tail for double-click, Enter, AND the intent path — closing a pre-existing click/Enter divergence hoshe caught this PR formalizing; the intent layer pre-checks via the new SystemScreen.find_body() and reports structured errors for unknown ids and terrain-less bodies. after_test() resets AtlasAgentBridge.current_app (tyre's freed-pending footgun). Suites 58/58 + 14/14; full suite 3,638. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
729 lines
24 KiB
GDScript
729 lines
24 KiB
GDScript
class_name SystemScreen
|
|
extends Control
|
|
## System picker and orbital diagram screen for AtlasApp (#844, D-191).
|
|
## Manages alphabetic system picker and orbital diagram for the current system.
|
|
## Emits body_selected when the player clicks a body in orbital view.
|
|
|
|
signal body_selected(body: Dictionary)
|
|
|
|
const AtlasFormat := preload("res://ui/implant/apps/atlas/atlas_format.gd")
|
|
|
|
const STAR_RADIUS: float = 140.0
|
|
const STAR_X_OFFSET: float = 0.0
|
|
const BODY_LEFT_MARGIN: float = 300.0
|
|
const BODY_RIGHT_MARGIN: float = 60.0
|
|
const MOON_CLEARANCE: float = 12.0
|
|
const STATION_SIZE: float = 8.0
|
|
const STATION_OFFSET_Y: float = -26.0
|
|
const BODY_HIT_RADIUS: float = 20.0
|
|
const LABEL_OFFSET: float = 14.0
|
|
const PANEL_WIDTH: float = 320.0
|
|
const PANEL_MARGIN: float = 16.0
|
|
|
|
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")
|
|
|
|
var _systems: Array = []
|
|
var _selected_idx: int = 0
|
|
var _orbital_bodies: Array = []
|
|
var _orbital_stations: Array = []
|
|
var _body_positions: Dictionary = {}
|
|
var _station_positions: Dictionary = {}
|
|
var _body_textures: Dictionary = {} # body_id -> ImageTexture
|
|
var _hovered_body: String = ""
|
|
var _hovered_station: String = ""
|
|
var _selected_station: Dictionary = {}
|
|
var _selected_body: Dictionary = {}
|
|
var _dirty: bool = true
|
|
var _in_orbital: bool = false
|
|
|
|
var _picker_panel = null # ImplantPanel
|
|
var _picker_nav_row = null # ImplantDataRow
|
|
var _station_panel = null # ImplantPanel
|
|
var _body_panel = null # ImplantPanel
|
|
|
|
|
|
func _ready() -> void:
|
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if not visible:
|
|
return
|
|
if _dirty:
|
|
queue_redraw()
|
|
_dirty = false
|
|
|
|
|
|
func setup(implant_theme) -> void:
|
|
_build_picker_panel(implant_theme)
|
|
_build_station_panel(implant_theme)
|
|
_build_body_panel(implant_theme)
|
|
|
|
|
|
func set_systems(systems: Array) -> void:
|
|
_systems = systems
|
|
|
|
|
|
func set_selected_idx(idx: int) -> void:
|
|
_selected_idx = idx
|
|
|
|
|
|
func get_selected_idx() -> int:
|
|
return _selected_idx
|
|
|
|
|
|
func current_system() -> Dictionary:
|
|
if _systems.is_empty():
|
|
return {}
|
|
_selected_idx = clampi(_selected_idx, 0, _systems.size() - 1)
|
|
return _systems[_selected_idx]
|
|
|
|
|
|
func is_in_orbital() -> bool:
|
|
return _in_orbital
|
|
|
|
|
|
func get_orbital_body_count() -> int:
|
|
var count: int = 0
|
|
for b: Dictionary in _orbital_bodies:
|
|
if b.get("parent_body_id") == null:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func get_station_count() -> int:
|
|
return _orbital_stations.size()
|
|
|
|
|
|
func show_picker_mode() -> void:
|
|
_in_orbital = false
|
|
if _picker_panel:
|
|
_picker_panel.visible = true
|
|
_rebuild_picker_panel()
|
|
if _station_panel:
|
|
_station_panel.visible = false
|
|
_dirty = true
|
|
|
|
|
|
func load_and_show_orbital() -> void:
|
|
var sys: Dictionary = current_system()
|
|
_orbital_bodies = sys.get("orbit_bodies", [])
|
|
_orbital_stations = sys.get("stations", [])
|
|
_hovered_body = ""
|
|
_hovered_station = ""
|
|
_selected_station = {}
|
|
_selected_body = {}
|
|
_load_body_textures(sys.get("system_id", ""))
|
|
_compute_body_positions()
|
|
if _picker_panel:
|
|
_picker_panel.visible = false
|
|
if _station_panel:
|
|
_station_panel.visible = false
|
|
if _body_panel:
|
|
_body_panel.visible = false
|
|
_in_orbital = true
|
|
_dirty = true
|
|
|
|
|
|
func navigate_system(delta: int) -> void:
|
|
if _systems.is_empty():
|
|
return
|
|
_selected_idx = wrapi(_selected_idx + delta, 0, _systems.size())
|
|
_rebuild_picker_panel()
|
|
|
|
|
|
func enter(payload: Dictionary) -> void:
|
|
if payload.get("mode") == "orbital":
|
|
load_and_show_orbital()
|
|
else:
|
|
show_picker_mode()
|
|
|
|
|
|
func leave() -> void:
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# Drawing
|
|
# =============================================================================
|
|
|
|
|
|
func _draw() -> void:
|
|
if _in_orbital:
|
|
_draw_orbital()
|
|
else:
|
|
draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG)
|
|
|
|
|
|
func _draw_orbital() -> void:
|
|
var sz: Vector2 = get_rect().size
|
|
var mid_y: float = sz.y * 0.5
|
|
var star_center := Vector2(STAR_X_OFFSET, mid_y)
|
|
|
|
draw_rect(Rect2(Vector2.ZERO, sz), COLOR_BG)
|
|
draw_circle(star_center, STAR_RADIUS + 40.0, Color(COLOR_STAR.r, COLOR_STAR.g, COLOR_STAR.b, 0.04))
|
|
draw_circle(star_center, STAR_RADIUS + 20.0, Color(COLOR_STAR.r, COLOR_STAR.g, COLOR_STAR.b, 0.08))
|
|
draw_circle(star_center, STAR_RADIUS, COLOR_STAR)
|
|
_draw_orbital_arcs(star_center)
|
|
_draw_stations()
|
|
_draw_bodies()
|
|
|
|
|
|
func _draw_orbital_arcs(star_center: Vector2) -> void:
|
|
for b: Dictionary in _orbital_bodies:
|
|
if b.get("parent_body_id") != null:
|
|
continue
|
|
var bid: String = str(b.get("body_id", ""))
|
|
if not _body_positions.has(bid):
|
|
continue
|
|
var body_pos: Vector2 = _body_positions[bid]
|
|
var orbit_r: float = body_pos.x - star_center.x
|
|
if orbit_r <= 0.0:
|
|
continue
|
|
draw_arc(star_center, orbit_r, -PI * 0.7, PI * 0.7, 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 radius_km: float = float(b.get("body_radius_km", 0))
|
|
var radius: float = _km_to_draw_radius(radius_km, body_type)
|
|
|
|
if bid == _hovered_body:
|
|
draw_arc(pos, radius + 5.0, 0.0, TAU, 20, Color(1.0, 1.0, 1.0, 0.25), 1.5, true)
|
|
|
|
var tex: ImageTexture = _body_textures.get(bid)
|
|
if tex:
|
|
var draw_size := Vector2(radius * 2.0, radius * 2.0)
|
|
draw_texture_rect(tex, Rect2(pos - draw_size / 2.0, draw_size), false)
|
|
else:
|
|
var atmo: String = b.get("atmosphere", "none")
|
|
var inhabited: bool = bool(b.get("inhabited", false))
|
|
var color: Color
|
|
match body_type:
|
|
"moon": color = COLOR_MOON
|
|
"oort_cloud": color = COLOR_OORT
|
|
_:
|
|
if inhabited: color = COLOR_PLANET_INHABITED
|
|
elif atmo in ["breathable", "standard"]: color = COLOR_PLANET_HABITABLE
|
|
else: color = COLOR_PLANET_BARE
|
|
draw_circle(pos, radius, color)
|
|
|
|
var label: String = b.get("proper_name", bid) if b.get("proper_name") else bid
|
|
var show_label: bool = body_type != "oort_cloud" 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)
|
|
|
|
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
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Orbital geometry
|
|
# =============================================================================
|
|
|
|
|
|
func _compute_body_positions() -> void:
|
|
_body_positions.clear()
|
|
_station_positions.clear()
|
|
|
|
var sz: Vector2 = get_rect().size
|
|
if sz == Vector2.ZERO:
|
|
sz = Vector2(1280.0, 720.0)
|
|
var mid_y: float = sz.y * 0.5
|
|
var usable_width: float = sz.x - BODY_LEFT_MARGIN - BODY_RIGHT_MARGIN
|
|
|
|
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))
|
|
)
|
|
|
|
var count: int = top_bodies.size()
|
|
for i: int in range(count):
|
|
var b: Dictionary = top_bodies[i]
|
|
var bid: String = str(b.get("body_id", ""))
|
|
if bid.is_empty():
|
|
continue
|
|
var t: float = float(i) / float(maxi(count - 1, 1))
|
|
var x: float = BODY_LEFT_MARGIN + t * usable_width
|
|
_body_positions[bid] = Vector2(x, mid_y)
|
|
|
|
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)
|
|
var body_dict_by_id: Dictionary = {}
|
|
for b: Dictionary in _orbital_bodies:
|
|
body_dict_by_id[str(b.get("body_id", ""))] = 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 parent_data: Dictionary = body_dict_by_id.get(parent_key, {})
|
|
var parent_draw_r: float = _km_to_draw_radius(
|
|
float(parent_data.get("body_radius_km", 0)),
|
|
parent_data.get("body_type", "")
|
|
)
|
|
var moon_count: int = siblings.size()
|
|
for j: int in range(moon_count):
|
|
var moon: Dictionary = siblings[j]
|
|
var moon_id: String = str(moon.get("body_id", ""))
|
|
if moon_id.is_empty():
|
|
continue
|
|
var moon_draw_r: float = _km_to_draw_radius(
|
|
float(moon.get("body_radius_km", 0)), "moon"
|
|
)
|
|
var label_space: float = LABEL_OFFSET + 12.0
|
|
var y_off: float = parent_draw_r + label_space + MOON_CLEARANCE + (moon_draw_r * 2.0 + MOON_CLEARANCE) * float(j)
|
|
_body_positions[moon_id] = parent_pos + Vector2(0.0, y_off)
|
|
|
|
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
|
|
var parent_r: float = 0.0
|
|
if parent_id != null and _body_positions.has(str(parent_id)):
|
|
station_parent_pos = _body_positions[str(parent_id)]
|
|
var pd: Dictionary = body_dict_by_id.get(str(parent_id), {})
|
|
parent_r = _km_to_draw_radius(float(pd.get("body_radius_km", 0)), pd.get("body_type", ""))
|
|
else:
|
|
station_parent_pos = Vector2(sz.x - BODY_RIGHT_MARGIN, mid_y)
|
|
var station_y: float = -(parent_r + LABEL_OFFSET + 12.0 + STATION_SIZE + MOON_CLEARANCE)
|
|
_station_positions[station_id] = station_parent_pos + Vector2(0.0, station_y)
|
|
|
|
|
|
# =============================================================================
|
|
# Input
|
|
# =============================================================================
|
|
|
|
|
|
func _gui_input(event: InputEvent) -> void:
|
|
if not _in_orbital:
|
|
return
|
|
if event is InputEventMouseButton and (event as InputEventMouseButton).pressed:
|
|
var mb := event as InputEventMouseButton
|
|
if mb.button_index == MOUSE_BUTTON_LEFT and mb.double_click:
|
|
_handle_orbital_double_click(mb.position)
|
|
elif mb.button_index == MOUSE_BUTTON_LEFT:
|
|
_handle_orbital_click(mb.position)
|
|
elif event is InputEventMouseMotion:
|
|
_handle_orbital_hover((event as InputEventMouseMotion).position)
|
|
|
|
|
|
func _handle_orbital_double_click(pos: Vector2) -> void:
|
|
var bid: String = _find_nearest_body(pos)
|
|
open_body_by_id(bid)
|
|
|
|
|
|
func _handle_orbital_click(pos: Vector2) -> void:
|
|
var bid: String = _find_nearest_body(pos)
|
|
if not bid.is_empty():
|
|
select_body_by_id(bid)
|
|
return
|
|
|
|
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
|
|
_selected_body = {}
|
|
if _body_panel:
|
|
_body_panel.visible = false
|
|
_rebuild_station_panel()
|
|
if _station_panel:
|
|
_station_panel.visible = true
|
|
return
|
|
|
|
_close_detail_panels()
|
|
_dirty = true
|
|
|
|
|
|
## T-971 (AtlasAgentInterface `select_body` intent) — the SAME state
|
|
## mutation the body branch of _handle_orbital_click() performs (opens the
|
|
## body info panel, does not navigate), reachable by id directly. A no-op for
|
|
## an unrecognized id (mirrors the click handler's own "no match under the
|
|
## cursor" fall-through to _close_detail_panels(), except an agent-driven
|
|
## select never had a "click landed on empty space" case to fall through to —
|
|
## an unrecognized id is a caller error, not a deselect gesture, so it is
|
|
## simply ignored here rather than clearing an unrelated selection).
|
|
func select_body_by_id(body_id: String) -> void:
|
|
for b: Dictionary in _orbital_bodies:
|
|
if str(b.get("body_id", "")) == body_id:
|
|
_selected_body = b
|
|
_selected_station = {}
|
|
if _station_panel:
|
|
_station_panel.visible = false
|
|
_rebuild_body_panel()
|
|
if _body_panel:
|
|
_body_panel.visible = true
|
|
_dirty = true
|
|
return
|
|
|
|
|
|
## T-971 (AtlasAgentInterface `open_body` intent) — the SAME state mutation
|
|
## _handle_orbital_double_click() performs (double-click equivalent: emits
|
|
## body_selected, which AtlasApp._on_body_selected() now gates on
|
|
## terrain_reference — PR #209 review, Hoshe finding 3 — before pushing
|
|
## "regional"). A no-op for an unrecognized id. Emits unconditionally for a
|
|
## RECOGNIZED body regardless of terrain_reference, same as the double-click
|
|
## handler always has — the guard lives in the shared _on_body_selected()
|
|
## tail, not here, so this stays a pure "found it, emit" lookup identical to
|
|
## the click path (AtlasAgentInterface does its OWN pre-check via
|
|
## find_body() to report a structured error instead of a silent no-op).
|
|
func open_body_by_id(body_id: String) -> void:
|
|
for b: Dictionary in _orbital_bodies:
|
|
if str(b.get("body_id", "")) == body_id:
|
|
body_selected.emit(b)
|
|
return
|
|
|
|
|
|
## Generic body lookup by id — {} if not found. Shared read used by both
|
|
## select_body_by_id()/open_body_by_id()'s own linear scans (kept inline in
|
|
## each for now, matching this file's existing per-method scan style) and by
|
|
## AtlasAgentInterface's open_body pre-check (PR #209 review, Hoshe finding
|
|
## 3) so the intent layer can build a structured "no terrain reference"
|
|
## error without re-deriving the emit-vs-no-op logic already owned by
|
|
## open_body_by_id() above.
|
|
func find_body(body_id: String) -> Dictionary:
|
|
for b: Dictionary in _orbital_bodies:
|
|
if str(b.get("body_id", "")) == body_id:
|
|
return b
|
|
return {}
|
|
|
|
|
|
func _close_detail_panels() -> void:
|
|
_selected_body = {}
|
|
_selected_station = {}
|
|
if _body_panel:
|
|
_body_panel.visible = false
|
|
if _station_panel:
|
|
_station_panel.visible = false
|
|
|
|
|
|
func has_body_panel_open() -> bool:
|
|
return not _selected_body.is_empty()
|
|
|
|
|
|
func has_station_panel_open() -> bool:
|
|
return not _selected_station.is_empty()
|
|
|
|
|
|
func close_panels() -> void:
|
|
_close_detail_panels()
|
|
_dirty = true
|
|
|
|
|
|
func get_selected_body() -> Dictionary:
|
|
return _selected_body
|
|
|
|
|
|
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
|
|
|
|
|
|
# =============================================================================
|
|
# Globe textures
|
|
# =============================================================================
|
|
|
|
|
|
func _load_body_textures(system_id: String) -> void:
|
|
_body_textures.clear()
|
|
var sys_dir: String = system_id.replace(" ", "-")
|
|
var wiki_root: String = ProjectSettings.globalize_path("res://").get_base_dir().get_base_dir() + "/wiki/star-systems"
|
|
for b: Dictionary in _orbital_bodies:
|
|
var bid: String = str(b.get("body_id", ""))
|
|
if bid.is_empty():
|
|
continue
|
|
var bt: String = b.get("body_type", "")
|
|
var globe_path: String
|
|
if bt in ["oort_cloud", "asteroid_belt"]:
|
|
globe_path = wiki_root + "/_generics/" + bt + "/globe.png"
|
|
else:
|
|
globe_path = wiki_root + "/" + sys_dir + "/bodies/" + bid + "/globe.png"
|
|
if not FileAccess.file_exists(globe_path):
|
|
continue
|
|
var img := Image.load_from_file(globe_path)
|
|
if img == null:
|
|
continue
|
|
var tex := ImageTexture.create_from_image(img)
|
|
_body_textures[bid] = tex
|
|
|
|
|
|
# =============================================================================
|
|
# Sizing
|
|
# =============================================================================
|
|
|
|
|
|
const DRAW_RADIUS_MIN: float = 4.0
|
|
const DRAW_RADIUS_MAX: float = 48.0
|
|
const DRAW_RADIUS_REF_KM: float = 6371.0
|
|
const DRAW_RADIUS_REF_PX: float = 24.0
|
|
|
|
|
|
static func _km_to_draw_radius(radius_km: float, _body_type: String) -> float:
|
|
if radius_km <= 0.0:
|
|
return DRAW_RADIUS_MIN
|
|
var ratio: float = radius_km / DRAW_RADIUS_REF_KM
|
|
var px: float = DRAW_RADIUS_REF_PX * pow(ratio, 0.5)
|
|
return maxf(px, DRAW_RADIUS_MIN)
|
|
|
|
|
|
# =============================================================================
|
|
# Panels
|
|
# =============================================================================
|
|
|
|
|
|
func _build_picker_panel(implant_theme) -> 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_station_panel(implant_theme) -> 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.visible = false
|
|
add_child(_station_panel)
|
|
|
|
|
|
func _rebuild_station_panel() -> void:
|
|
if not _station_panel:
|
|
return
|
|
_station_panel.clear()
|
|
|
|
if _selected_station.is_empty():
|
|
return
|
|
|
|
var sz: Vector2 = get_rect().size
|
|
_station_panel.position = Vector2(sz.x - PANEL_WIDTH - 60.0, 60.0)
|
|
|
|
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"))
|
|
|
|
|
|
func _build_body_panel(implant_theme) -> 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.visible = false
|
|
add_child(_body_panel)
|
|
|
|
|
|
func _rebuild_body_panel() -> void:
|
|
if not _body_panel:
|
|
return
|
|
_body_panel.clear()
|
|
|
|
if _selected_body.is_empty():
|
|
return
|
|
|
|
var sz: Vector2 = get_rect().size
|
|
_body_panel.position = Vector2(sz.x - PANEL_WIDTH - 60.0, 60.0)
|
|
|
|
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 radius_km: float = float(b.get("body_radius_km", 0))
|
|
|
|
var sys: Dictionary = current_system()
|
|
var sys_name: String = sys.get("proper_name", sys.get("system_id", "—"))
|
|
|
|
# Globe artwork at top
|
|
var tex: ImageTexture = _body_textures.get(bid)
|
|
if tex:
|
|
var globe_rect := TextureRect.new()
|
|
globe_rect.texture = tex
|
|
globe_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
|
globe_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
globe_rect.custom_minimum_size = Vector2(PANEL_WIDTH, PANEL_WIDTH)
|
|
_body_panel.add_component(globe_rect)
|
|
|
|
_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 radius_km > 0:
|
|
_body_panel.add_component(
|
|
ImplantDataRow.new("radius " + AtlasFormat.format_radius(radius_km))
|
|
)
|
|
|
|
if inhabited:
|
|
_body_panel.add_component(ImplantSeparator.new())
|
|
_body_panel.add_component(
|
|
ImplantDataRow.new("population " + AtlasFormat.format_pop(pop))
|
|
)
|
|
|
|
_body_panel.add_component(ImplantSeparator.new())
|
|
var has_heightmap: bool = b.get("terrain_reference") != null
|
|
if has_heightmap:
|
|
_body_panel.add_component(ImplantTextBlock.new("enter view heightmap atlas"))
|
|
_body_panel.add_component(ImplantTextBlock.new("esc close"))
|
|
|
|
_dirty = true
|