feat(ui): implant component library + star map refactor (D-169)
New component library at client/ui/implant/: - ImplantTheme: shared Resource with colors, spacing, font sizes - ImplantPanel: PanelContainer root with themed background/border - ImplantHeader: title + subtitle - ImplantSeparator: themed horizontal rule with above/below spacing - ImplantDataRow: single-line text with optional color override - ImplantTextBlock: RichTextLabel for wrapping narrative text - default_implant.tres: default theme resource Star map info panel refactored from 140 lines of manual draw_string calls to 50 lines of component composition via _rebuild_info_panel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+44
-109
@@ -35,12 +35,9 @@ const HIT_RADIUS: float = 10.0 # click tolerance
|
||||
const EDGE_WIDTH: float = 0.8
|
||||
const EDGE_SELECTED_ALPHA: float = 0.55
|
||||
|
||||
# Info popup — expanded with wiki/GTTR content (#780)
|
||||
# Info popup — uses ImplantPanel component library (D-169)
|
||||
const POPUP_WIDTH: float = 300.0
|
||||
const POPUP_MARGIN: float = 16.0
|
||||
const POPUP_PADDING: float = 12.0
|
||||
const POPUP_LINE_H: float = 17.0
|
||||
const POPUP_GTTR_FONT_SIZE: int = 10
|
||||
const POPUP_GTTR_MAX_LINES: int = 7
|
||||
|
||||
# Colors — sector palette from wireframe
|
||||
@@ -51,7 +48,6 @@ const COLOR_GATEWAY: Color = Color("#f0d060")
|
||||
const COLOR_SELECTION: Color = Color("#f0d060")
|
||||
const COLOR_TEXT: Color = Color("#c8d0e0")
|
||||
const COLOR_TEXT_DIM: Color = Color("#667788")
|
||||
const COLOR_INFO_BG: Color = Color(0.05, 0.08, 0.14, 0.92)
|
||||
|
||||
const SECTOR_COLORS: Dictionary = {
|
||||
"core": Color("#c8d0e0"),
|
||||
@@ -104,10 +100,23 @@ var _pan_start_offset: Vector2 = Vector2.ZERO
|
||||
var _data_loaded: bool = false
|
||||
var _insert_active: bool = true
|
||||
var _dirty: bool = true # redraw needed — set by state changes, cleared after _draw
|
||||
var _info_panel: ImplantPanel # D-169: component-based info panel
|
||||
var _implant_theme: ImplantTheme
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
# D-169: Load implant theme and create info panel
|
||||
_implant_theme = load("res://ui/implant/default_implant.tres") as ImplantTheme
|
||||
_info_panel = ImplantPanel.new()
|
||||
_info_panel.name = "InfoPanel"
|
||||
_info_panel.theme_resource = _implant_theme
|
||||
_info_panel.custom_minimum_size.x = POPUP_WIDTH
|
||||
_info_panel.visible = false
|
||||
_info_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_info_panel)
|
||||
|
||||
_load_data()
|
||||
if _data_loaded:
|
||||
_compute_layout()
|
||||
@@ -307,9 +316,12 @@ func _draw() -> void:
|
||||
if _selected_system != "":
|
||||
_draw_selection(center)
|
||||
|
||||
# Info panel for selected system
|
||||
if _selected_system != "":
|
||||
_draw_info_panel(sz)
|
||||
# Info panel positioned in top-right (D-169 component panel)
|
||||
if _info_panel:
|
||||
_info_panel.visible = _selected_system != ""
|
||||
if _info_panel.visible:
|
||||
_info_panel.position = Vector2(
|
||||
sz.x - POPUP_WIDTH - POPUP_MARGIN, POPUP_MARGIN)
|
||||
|
||||
# Title
|
||||
_draw_title()
|
||||
@@ -395,25 +407,26 @@ func _draw_selection(center: Vector2) -> void:
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, COLOR_SELECTION)
|
||||
|
||||
|
||||
func _draw_info_panel(sz: Vector2) -> void:
|
||||
## Rebuild the info panel with components for the selected system (D-169).
|
||||
func _rebuild_info_panel() -> void:
|
||||
if not _info_panel:
|
||||
return
|
||||
_info_panel.clear()
|
||||
|
||||
var node: Dictionary = _node_lookup.get(_selected_system, {})
|
||||
if node.is_empty():
|
||||
_info_panel.visible = false
|
||||
return
|
||||
|
||||
var font := get_theme_default_font()
|
||||
var panel_w: float = POPUP_WIDTH
|
||||
var pad: float = POPUP_PADDING
|
||||
var line_h: float = 18.0 # line height for single-line text (baseline to baseline)
|
||||
var sep_above: float = 6.0 # space from last text baseline to separator line
|
||||
var sep_below: float = 12.0 # space from separator line to next text baseline
|
||||
var inner_w: float = panel_w - pad * 2.0
|
||||
var sep_color := Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, 0.12)
|
||||
|
||||
# ── Prepare all text content before measuring ────────────────────────────
|
||||
# ── Header ───────────────────────────────────────────────────────────────
|
||||
var sys_name: String = node.get("proper_name", "")
|
||||
if sys_name.is_empty():
|
||||
sys_name = node.get("system_id", "Unknown")
|
||||
_info_panel.add_component(ImplantHeader.new(sys_name, node.get("system_id", "")))
|
||||
|
||||
_info_panel.add_component(ImplantSeparator.new())
|
||||
|
||||
# ── Stats ────────────────────────────────────────────────────────────────
|
||||
var star_type: String = node.get("star_type", "")
|
||||
var hop: int = int(node.get("hop_distance", 0))
|
||||
var stat_line_1: String
|
||||
@@ -421,9 +434,11 @@ func _draw_info_panel(sz: Vector2) -> void:
|
||||
stat_line_1 = "Hop %d from Gateway" % hop
|
||||
else:
|
||||
stat_line_1 = "%s star · hop %d" % [star_type, hop]
|
||||
_info_panel.add_component(ImplantDataRow.new(stat_line_1))
|
||||
|
||||
var sector_str: String = node.get("geographic_sector", "unknown").replace("_", " ").to_upper()
|
||||
var sector_color: Color = SECTOR_COLORS.get(node.get("geographic_sector", ""), COLOR_TEXT_DIM)
|
||||
_info_panel.add_component(ImplantDataRow.new(sector_str + " corridor", sector_color))
|
||||
|
||||
var bodies: String = node.get("bodies", "")
|
||||
var population: String = node.get("population", "")
|
||||
@@ -438,105 +453,24 @@ func _draw_info_panel(sz: Vector2) -> void:
|
||||
stat_line_3 = "%d aperture%s" % [
|
||||
int(node.get("aperture_count", 0)),
|
||||
"s" if int(node.get("aperture_count", 0)) != 1 else ""]
|
||||
_info_panel.add_component(ImplantDataRow.new(stat_line_3))
|
||||
|
||||
var gttr: String = node.get("gttr_excerpt", "").replace("**", "")
|
||||
# ── GTTR excerpt ─────────────────────────────────────────────────────────
|
||||
var gttr: String = node.get("gttr_excerpt", "")
|
||||
if not gttr.is_empty():
|
||||
_info_panel.add_component(ImplantSeparator.new())
|
||||
_info_panel.add_component(ImplantTextBlock.new(gttr, POPUP_GTTR_MAX_LINES))
|
||||
|
||||
# ── Adjacent systems ─────────────────────────────────────────────────────
|
||||
var adj: Array = node.get("adjacent_systems", [])
|
||||
var adj_line: String = ""
|
||||
if not adj.is_empty():
|
||||
_info_panel.add_component(ImplantSeparator.new())
|
||||
var adj_names: Array = []
|
||||
for neighbor_id: String in adj:
|
||||
var neighbor: Dictionary = _node_lookup.get(neighbor_id, {})
|
||||
var n_name: String = neighbor.get("proper_name", "")
|
||||
adj_names.append(n_name if not n_name.is_empty() else neighbor_id)
|
||||
adj_line = " · ".join(adj_names)
|
||||
|
||||
# ── Measure variable-height blocks ───────────────────────────────────────
|
||||
var gttr_h: float = 0.0
|
||||
if not gttr.is_empty():
|
||||
gttr_h = font.get_multiline_string_size(
|
||||
gttr, HORIZONTAL_ALIGNMENT_LEFT, inner_w,
|
||||
POPUP_GTTR_FONT_SIZE, POPUP_GTTR_MAX_LINES).y
|
||||
|
||||
var adj_h: float = POPUP_LINE_H # minimum one line
|
||||
if not adj_line.is_empty():
|
||||
adj_h = font.get_multiline_string_size(
|
||||
adj_line, HORIZONTAL_ALIGNMENT_LEFT, inner_w, 10, 3).y
|
||||
|
||||
# ── Calculate total panel height ─────────────────────────────────────────
|
||||
var sep_total: float = sep_above + sep_below # full separator block height
|
||||
var panel_h: float = pad # top padding
|
||||
panel_h += line_h # system name (15px font)
|
||||
panel_h += line_h # system id (10px font)
|
||||
panel_h += sep_total # separator
|
||||
panel_h += line_h # star type + hop
|
||||
panel_h += line_h # corridor
|
||||
panel_h += line_h # bodies + pop
|
||||
if not gttr.is_empty():
|
||||
panel_h += sep_total + gttr_h
|
||||
panel_h += sep_total + adj_h # adjacent systems
|
||||
panel_h += pad # bottom padding
|
||||
|
||||
# ── Draw background ──────────────────────────────────────────────────────
|
||||
var panel_pos := Vector2(sz.x - panel_w - POPUP_MARGIN, POPUP_MARGIN)
|
||||
var x: float = panel_pos.x + pad
|
||||
var x_right: float = panel_pos.x + panel_w - pad
|
||||
var y: float = panel_pos.y + pad + line_h # first baseline
|
||||
|
||||
draw_rect(Rect2(panel_pos, Vector2(panel_w, panel_h)), COLOR_INFO_BG)
|
||||
draw_rect(Rect2(panel_pos, Vector2(panel_w, panel_h)), sep_color, false, 1.0)
|
||||
|
||||
# ── Draw content ─────────────────────────────────────────────────────────
|
||||
|
||||
# System name
|
||||
draw_string(font, Vector2(x, y), sys_name,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, inner_w, 15, COLOR_TEXT)
|
||||
y += line_h
|
||||
|
||||
# System ID
|
||||
draw_string(font, Vector2(x, y), node.get("system_id", ""),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, inner_w, 10, COLOR_TEXT_DIM)
|
||||
|
||||
# Separator — space above from baseline, line, space below to next baseline
|
||||
y += sep_above
|
||||
draw_line(Vector2(x, y), Vector2(x_right, y), sep_color, 1.0)
|
||||
y += sep_below
|
||||
|
||||
# Star type + hop
|
||||
draw_string(font, Vector2(x, y), stat_line_1,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, inner_w, 11, COLOR_TEXT_DIM)
|
||||
y += line_h
|
||||
|
||||
# Corridor
|
||||
draw_string(font, Vector2(x, y), sector_str + " corridor",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, inner_w, 11, sector_color)
|
||||
y += line_h
|
||||
|
||||
# Bodies + population
|
||||
draw_string(font, Vector2(x, y), stat_line_3,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, inner_w, 11, COLOR_TEXT_DIM)
|
||||
|
||||
# GTTR excerpt
|
||||
if not gttr.is_empty():
|
||||
y += sep_above
|
||||
draw_line(Vector2(x, y), Vector2(x_right, y), sep_color, 1.0)
|
||||
y += sep_below
|
||||
draw_multiline_string(font, Vector2(x, y), gttr,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, inner_w, POPUP_GTTR_FONT_SIZE,
|
||||
POPUP_GTTR_MAX_LINES,
|
||||
Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, 0.75))
|
||||
y += gttr_h - line_h # multiline size includes first baseline
|
||||
|
||||
# Adjacent systems
|
||||
y += sep_above
|
||||
draw_line(Vector2(x, y), Vector2(x_right, y), sep_color, 1.0)
|
||||
y += sep_below
|
||||
if adj_line.is_empty():
|
||||
draw_string(font, Vector2(x, y), "No gate connections",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, inner_w, 10, COLOR_TEXT_DIM)
|
||||
else:
|
||||
draw_multiline_string(font, Vector2(x, y), adj_line,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, inner_w, 10, 3, COLOR_TEXT_DIM)
|
||||
_info_panel.add_component(ImplantDataRow.new(" · ".join(adj_names)))
|
||||
|
||||
|
||||
func _draw_title() -> void:
|
||||
@@ -613,6 +547,7 @@ func _find_nearest_system(pos: Vector2) -> String:
|
||||
func _handle_click(pos: Vector2) -> void:
|
||||
var nearest := _find_nearest_system(pos)
|
||||
_selected_system = nearest
|
||||
_rebuild_info_panel()
|
||||
_dirty = true
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user