feat(ui): inline body detail panel on system orbital view
Body info panel with globe artwork renders on the right side of the system map instead of navigating to a separate screen. Single-click shows/swaps panel, double-click opens regional heightmap view, ESC closes panel. Station panel also moved to right side for consistency. Moon and station positioning clears parent body radius + label space. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -72,7 +72,9 @@ func _handle_key(event: InputEventKey) -> void:
|
||||
KEY_M:
|
||||
HudGroups.close_app()
|
||||
KEY_ESCAPE:
|
||||
if current_screen_id() == "reach":
|
||||
if current_screen_id() == "system" and _system_screen and (_system_screen.has_body_panel_open() or _system_screen.has_station_panel_open()):
|
||||
_system_screen.close_panels()
|
||||
elif current_screen_id() == "reach":
|
||||
HudGroups.close_app()
|
||||
else:
|
||||
nav.pop()
|
||||
@@ -95,18 +97,13 @@ func _handle_enter() -> void:
|
||||
if _system_screen and not _system_screen.is_in_orbital():
|
||||
var sys: Dictionary = _system_screen.current_system()
|
||||
nav.replace("system", {"mode": "orbital", "system": sys})
|
||||
"planet":
|
||||
if _planet_screen and _planet_screen.has_heightmap():
|
||||
(
|
||||
nav
|
||||
. push(
|
||||
"regional",
|
||||
{
|
||||
"body": _planet_screen.current_body(),
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
}
|
||||
)
|
||||
)
|
||||
elif _system_screen and _system_screen.has_body_panel_open():
|
||||
var body: Dictionary = _system_screen.get_selected_body()
|
||||
if body.get("terrain_reference") != null:
|
||||
nav.push("regional", {
|
||||
"body": body,
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -123,16 +120,10 @@ func _on_system_selected(system_id: String) -> void:
|
||||
|
||||
|
||||
func _on_body_selected(body: Dictionary) -> void:
|
||||
(
|
||||
nav
|
||||
. push(
|
||||
"planet",
|
||||
{
|
||||
"body": body,
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
}
|
||||
)
|
||||
)
|
||||
nav.push("regional", {
|
||||
"body": body,
|
||||
"system": nav.current_payload().get("system", {}),
|
||||
})
|
||||
|
||||
|
||||
func _on_regional_back() -> void:
|
||||
|
||||
@@ -40,12 +40,14 @@ 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:
|
||||
@@ -64,6 +66,7 @@ func _process(_delta: float) -> void:
|
||||
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:
|
||||
@@ -118,12 +121,15 @@ func load_and_show_orbital() -> void:
|
||||
_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
|
||||
|
||||
@@ -338,11 +344,15 @@ func _compute_body_positions() -> void:
|
||||
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)
|
||||
_station_positions[station_id] = station_parent_pos + Vector2(0.0, STATION_OFFSET_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)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -354,12 +364,16 @@ func _gui_input(event: InputEvent) -> void:
|
||||
if not _in_orbital:
|
||||
return
|
||||
if event is InputEventMouseButton and (event as InputEventMouseButton).pressed:
|
||||
_handle_orbital_click((event as InputEventMouseButton).position)
|
||||
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_click(pos: Vector2) -> void:
|
||||
func _handle_orbital_double_click(pos: Vector2) -> void:
|
||||
var bid: String = _find_nearest_body(pos)
|
||||
if not bid.is_empty():
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
@@ -367,22 +381,65 @@ func _handle_orbital_click(pos: Vector2) -> void:
|
||||
body_selected.emit(b)
|
||||
return
|
||||
|
||||
|
||||
func _handle_orbital_click(pos: Vector2) -> void:
|
||||
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:
|
||||
_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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
@@ -517,7 +574,6 @@ func _build_station_panel(implant_theme) -> void:
|
||||
_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)
|
||||
|
||||
@@ -530,6 +586,9 @@ func _rebuild_station_panel() -> void:
|
||||
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", "—")
|
||||
@@ -553,3 +612,91 @@ func _rebuild_station_panel() -> void:
|
||||
_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 " + _format_radius(radius_km)))
|
||||
|
||||
if inhabited:
|
||||
_body_panel.add_component(ImplantSeparator.new())
|
||||
_body_panel.add_component(ImplantDataRow.new("population " + _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
|
||||
|
||||
|
||||
static 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
|
||||
|
||||
|
||||
static func _format_radius(km: float) -> String:
|
||||
if km >= 10000.0:
|
||||
return "%s km" % _format_pop(int(km))
|
||||
return "%.0f km" % km
|
||||
|
||||
Reference in New Issue
Block a user