feat(ui): atlas system orbital redesign — horizontal orrery with globe textures
Half-sun on left edge, planets left-to-right with radius-based sizing (Earth = 24px reference, unclamped — gas giants fill the view). Globe textures loaded from wiki at runtime. Double-click on reach map opens system view. M closes atlas from any screen, ESC navigates back. Fixes Godot 4.6 type inference parse errors in main.gd (var zoom/speed). Uses physical_keycode throughout for layout-independent key handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,13 +62,15 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if not event.is_pressed() or event.is_echo():
|
||||
return
|
||||
if current_screen_id() == "regional":
|
||||
return # AtlasViewer handles its own keyboard input
|
||||
return
|
||||
_handle_key(event as InputEventKey)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _handle_key(event: InputEventKey) -> void:
|
||||
match event.keycode:
|
||||
match event.physical_keycode:
|
||||
KEY_M:
|
||||
HudGroups.close_app()
|
||||
KEY_ESCAPE:
|
||||
if current_screen_id() == "reach":
|
||||
HudGroups.close_app()
|
||||
|
||||
@@ -438,7 +438,10 @@ func _gui_input(event: InputEvent) -> void:
|
||||
if mb.pressed:
|
||||
match mb.button_index:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
_handle_reach_click(mb.position)
|
||||
if mb.double_click:
|
||||
_handle_reach_double_click(mb.position)
|
||||
else:
|
||||
_handle_reach_click(mb.position)
|
||||
MOUSE_BUTTON_MIDDLE:
|
||||
_reach_is_panning = true
|
||||
_reach_pan_start = mb.position
|
||||
@@ -476,6 +479,13 @@ func _handle_reach_click(pos: Vector2) -> void:
|
||||
_dirty = true
|
||||
|
||||
|
||||
func _handle_reach_double_click(pos: Vector2) -> void:
|
||||
var sid := _find_nearest_reach_system(pos)
|
||||
if not sid.is_empty():
|
||||
_reach_selected = sid
|
||||
system_selected.emit(sid)
|
||||
|
||||
|
||||
func _update_reach_hover(pos: Vector2) -> void:
|
||||
var nearest := _find_nearest_reach_system(pos)
|
||||
if nearest != _reach_hovered:
|
||||
|
||||
@@ -6,14 +6,15 @@ extends Control
|
||||
|
||||
signal body_selected(body: Dictionary)
|
||||
|
||||
const ORBITAL_CENTER_FRACTION := Vector2(0.5, 0.55)
|
||||
const STAR_RADIUS: float = 12.0
|
||||
const ORBITAL_RING_BASE: float = 65.0
|
||||
const ORBITAL_RING_STEP: float = 58.0
|
||||
const MOON_ORBIT_RADIUS: float = 24.0
|
||||
const STATION_SIZE: float = 6.0
|
||||
const BODY_HIT_RADIUS: float = 16.0
|
||||
const LABEL_OFFSET: float = 11.0
|
||||
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
|
||||
|
||||
@@ -35,6 +36,7 @@ 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 = {}
|
||||
@@ -116,6 +118,7 @@ func load_and_show_orbital() -> void:
|
||||
_hovered_body = ""
|
||||
_hovered_station = ""
|
||||
_selected_station = {}
|
||||
_load_body_textures(sys.get("system_id", ""))
|
||||
_compute_body_positions()
|
||||
if _picker_panel:
|
||||
_picker_panel.visible = false
|
||||
@@ -157,27 +160,30 @@ func _draw() -> void:
|
||||
|
||||
func _draw_orbital() -> void:
|
||||
var sz: Vector2 = get_rect().size
|
||||
var center: Vector2 = sz * ORBITAL_CENTER_FRACTION
|
||||
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_orbit_rings(center)
|
||||
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)
|
||||
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_orbit_rings(center: Vector2) -> void:
|
||||
var seen_orbits: Dictionary = {}
|
||||
func _draw_orbital_arcs(star_center: Vector2) -> void:
|
||||
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):
|
||||
var bid: String = str(b.get("body_id", ""))
|
||||
if not _body_positions.has(bid):
|
||||
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)
|
||||
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:
|
||||
@@ -188,36 +194,31 @@ func _draw_bodies() -> void:
|
||||
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":
|
||||
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
|
||||
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.0, true)
|
||||
draw_arc(pos, radius + 5.0, 0.0, TAU, 20, Color(1.0, 1.0, 1.0, 0.25), 1.5, true)
|
||||
|
||||
draw_circle(pos, radius, color)
|
||||
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 = inhabited or bid == _hovered_body
|
||||
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)
|
||||
@@ -269,7 +270,8 @@ func _compute_body_positions() -> void:
|
||||
var sz: Vector2 = get_rect().size
|
||||
if sz == Vector2.ZERO:
|
||||
sz = Vector2(1280.0, 720.0)
|
||||
var center: Vector2 = sz * ORBITAL_CENTER_FRACTION
|
||||
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:
|
||||
@@ -280,30 +282,16 @@ func _compute_body_positions() -> void:
|
||||
return int(a.get("orbit_index", 0)) < int(b.get("orbit_index", 0))
|
||||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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
|
||||
else:
|
||||
angle = -PI / 2.0 + TAU * float(i) / float(count)
|
||||
_body_positions[bid] = center + Vector2(cos(angle), sin(angle)) * ring_r
|
||||
|
||||
# Moons per parent count drives spacing — hard-coded divisor caused overlap on gas giants
|
||||
var moons_by_parent: Dictionary = {}
|
||||
for b: Dictionary in _orbital_bodies:
|
||||
var parent_id: Variant = b.get("parent_body_id")
|
||||
@@ -313,6 +301,10 @@ func _compute_body_positions() -> void:
|
||||
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
|
||||
@@ -322,16 +314,23 @@ func _compute_body_positions() -> void:
|
||||
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 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 angle: float = -PI / 2.0 + TAU * float(i) / float(count)
|
||||
_body_positions[moon_id] = (
|
||||
parent_pos + Vector2(cos(angle), sin(angle)) * MOON_ORBIT_RADIUS
|
||||
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", ""))
|
||||
@@ -342,8 +341,8 @@ func _compute_body_positions() -> void:
|
||||
if parent_id != null and _body_positions.has(str(parent_id)):
|
||||
station_parent_pos = _body_positions[str(parent_id)]
|
||||
else:
|
||||
station_parent_pos = center
|
||||
_station_positions[station_id] = station_parent_pos + Vector2(20.0, -10.0)
|
||||
station_parent_pos = Vector2(sz.x - BODY_RIGHT_MARGIN, mid_y)
|
||||
_station_positions[station_id] = station_parent_pos + Vector2(0.0, STATION_OFFSET_Y)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -415,6 +414,53 @@ func _find_nearest_station(pos: Vector2) -> String:
|
||||
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
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user