From cbcfeccd67bd3e8ade736d623f3ae4485d020fdc Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 19 Apr 2026 14:03:25 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20PR=20#131=20review=20round=201=20?= =?UTF-8?q?=E2=80=94=20retire=20starchart,=20harden=20registry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses blocker comments from PR review: - Delete star_map.gd and star_map.tscn — dead implant/map/starchart HudGroups registration that should have landed with the atlas unification (D-191 criterion 1) - Remove test_star_map_is_accessible_from_insert_ui and test_star_map_scene_exists from test_sprint30.gd — D-191 supersedes the insert-UI accessibility pattern - ImplantRegistry._scan() now detects default_key collisions (first-wins with push_warning) and validates default_mode against HudGroups.Mode enum (skip + warn on invalid) star_map_data.json remains — still used by atlas_app and economics_app for system index lookups. Co-Authored-By: Claude Opus 4.6 --- client/tests/test_sprint30.gd | 38 -- client/ui/implant/implant_registry.gd | 26 +- client/ui/star_map.gd | 637 -------------------------- client/ui/star_map.tscn | 18 - 4 files changed, 23 insertions(+), 696 deletions(-) delete mode 100644 client/ui/star_map.gd delete mode 100644 client/ui/star_map.tscn diff --git a/client/tests/test_sprint30.gd b/client/tests/test_sprint30.gd index 4d8b509b4..dff631bae 100644 --- a/client/tests/test_sprint30.gd +++ b/client/tests/test_sprint30.gd @@ -550,41 +550,3 @@ func test_overhead_anchor_offset_constant() -> void: assert_bool(attachment == null).override_failure_message( "_overhead_attachment must be null before skeleton is loaded" ).is_true() - - -# ============================================================================= -# #674 — Star map insert module (test-first) -# ============================================================================= - -func test_star_map_scene_exists() -> void: - ## [ACCEPTANCE #674] The star map scene must exist at the expected path. - ## WILL FAIL until #674 is implemented. - var expected_path := "res://ui/star_map.tscn" - assert_bool(ResourceLoader.exists(expected_path)).override_failure_message( - "[#674] Star map scene must exist at res://ui/star_map.tscn" - ).is_true() - - -func test_star_map_is_accessible_from_insert_ui() -> void: - ## [ACCEPTANCE #674] The star map module must be reachable from the insert UI. - ## Verify via HUD or main scene that a star_map node/scene is connected. - ## WILL FAIL until #674 wires the scene into the insert layer. - var hud_scene_path := "res://ui/hud.tscn" - if not ResourceLoader.exists(hud_scene_path): - push_warning("test_star_map_is_accessible_from_insert_ui: HUD scene not found — skipping") - return - var packed := load(hud_scene_path) as PackedScene - if packed == null: - return - var hud := packed.instantiate() - if hud == null: - return - auto_free(hud) - add_child(hud) - await get_tree().process_frame - - # Star map must be reachable as a named node from the HUD or insert layer - var star_map := hud.get_node_or_null("StarMap") - assert_bool(star_map != null).override_failure_message( - "[#674] HUD must contain a StarMap node accessible from the insert UI" - ).is_true() diff --git a/client/ui/implant/implant_registry.gd b/client/ui/implant/implant_registry.gd index 342df7620..00771997d 100644 --- a/client/ui/implant/implant_registry.gd +++ b/client/ui/implant/implant_registry.gd @@ -17,6 +17,8 @@ func get_manifests() -> Array: func _scan() -> void: _scanned = true _manifests.clear() + var key_owners: Dictionary = {} # default_key -> app_path (collision detection) + var valid_modes: Array = HudGroups.Mode.values() var dir := DirAccess.open("res://ui/implant/apps") if dir == null: push_warning("ImplantRegistry: could not open res://ui/implant/apps") @@ -28,10 +30,28 @@ func _scan() -> void: var tres_path: String = "res://ui/implant/apps/%s/app.tres" % entry if ResourceLoader.exists(tres_path): var m = load(tres_path) - if _is_valid_manifest(m): - _manifests.append(m) - else: + if not _is_valid_manifest(m): push_warning("ImplantRegistry: invalid or missing app_path in %s" % tres_path) + else: + var app_path: String = m.get("app_path") + var default_mode: int = m.get("default_mode", 2) + var default_key: int = m.get("default_key", -1) + if not valid_modes.has(default_mode): + push_warning( + "ImplantRegistry: invalid default_mode %d in %s — skipping" % [ + default_mode, tres_path + ] + ) + elif default_key >= 0 and key_owners.has(default_key): + push_warning( + "ImplantRegistry: key binding collision — key %d already bound to %s, skipping %s" % [ + default_key, key_owners[default_key], app_path + ] + ) + else: + if default_key >= 0: + key_owners[default_key] = app_path + _manifests.append(m) entry = dir.get_next() dir.list_dir_end() diff --git a/client/ui/star_map.gd b/client/ui/star_map.gd deleted file mode 100644 index 0ebb3b462..000000000 --- a/client/ui/star_map.gd +++ /dev/null @@ -1,637 +0,0 @@ -class_name StarMapRenderer -extends Control - -## Star map — concentric hop-ring view of the Settled Reach gate network (#674). -## Renders 301 systems as dots on concentric rings (hop distance from Gateway). -## Sector-colored: core (white-gold), north (blue), south (orange), east (green), west (tan). -## -## Data source: res://data/star_map_data.json (generated from star-map.json + systems.db + wiki). -## Regenerate with: tooling/generate-star-map-data.py -## -## D-013: Diegetic neural insert overlay. Accessible from the insert UI. -## Parent epic: #51 (Diegetic Insert/Minimap), ticket #674. -## Ticket #780: Click-through popup — wiki/GTTR content on system select. - -const DATA_PATH := "res://data/star_map_data.json" - -# Layout -const MAP_CENTER_FRACTION := Vector2(0.5, 0.5) # center of control -const MIN_RING_RADIUS: float = 30.0 # innermost ring (hop 0 = gateway dot only) -const RING_SPACING: float = 22.0 # pixels between hop rings -const MAX_HOP_RINGS: int = 24 # max hop distance we render rings for - -# Dot sizing -const DOT_RADIUS_HUB: float = 4.5 -const DOT_RADIUS_JUNCTION: float = 3.5 -const DOT_RADIUS_DEFAULT: float = 2.5 -const DOT_RADIUS_DEAD_END: float = 2.0 -const GATEWAY_RADIUS: float = 6.0 - -# Selection -const SELECTION_RING_RADIUS: float = 8.0 -const HIT_RADIUS: float = 10.0 # click tolerance - -# Edge rendering — only shown for selected system (ticket #780 UX rule) -const EDGE_WIDTH: float = 0.8 -const EDGE_SELECTED_ALPHA: float = 0.55 - -# Info popup — uses ImplantPanel component library (D-169) -const POPUP_WIDTH: float = 300.0 -const POPUP_MARGIN: float = 16.0 -const POPUP_GTTR_MAX_LINES: int = 7 - -# Colors — sector palette from wireframe -const COLOR_BG: Color = Color("#0d1117") -const COLOR_RING: Color = Color("#1a2030") -const COLOR_RING_MAJOR: Color = Color("#222a3a") -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 SECTOR_COLORS: Dictionary = { - "core": Color("#c8d0e0"), - "north_reach": Color("#4488aa"), - "south_reach": Color("#aa6644"), - "east_reach": Color("#44aa66"), - "west_reach": Color("#aa8844"), - "deep_frontier": Color("#556677"), - "unknown": Color("#445566"), -} - -const SECTOR_LABELS: Dictionary = { - "north_reach": "NORTH REACH", - "south_reach": "SOUTH REACH", - "east_reach": "EAST REACH", - "west_reach": "WEST REACH", -} - -# Quadrant angles for sector placement (radians, 0 = right, counterclockwise) -# North = top (-PI/2), East = right (0), South = bottom (PI/2), West = left (PI) -const SECTOR_ANGLE_CENTER: Dictionary = { - "north_reach": -PI / 2.0, - "east_reach": 0.0, - "south_reach": PI / 2.0, - "west_reach": PI, -} -const SECTOR_ANGLE_SPREAD: float = PI / 2.5 # each sector occupies ~72° of arc -const CORE_ANGLE_SPREAD: float = TAU # core systems spread full circle -const DEEP_FRONTIER_ANGLE_SPREAD: float = TAU # deep frontier wraps entire outer edge - -# Pan/zoom -const ZOOM_MIN: float = 0.3 -const ZOOM_MAX: float = 3.0 -const ZOOM_STEP: float = 0.15 - -# Internal state -var _nodes: Array = [] -var _edges: Array = [] -var _node_positions: Dictionary = {} # system_id -> Vector2 (screen coords relative to map center) -var _node_lookup: Dictionary = {} # system_id -> node dict -var _selected_system: String = "" -var _hovered_system: String = "" - -var _zoom: float = 1.0 -var _pan_offset: Vector2 = Vector2.ZERO -var _is_panning: bool = false -var _pan_start: Vector2 = Vector2.ZERO -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.size.x = POPUP_WIDTH - _info_panel.visible = false - _info_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE - add_child(_info_panel) - - # D-170: Register with HUD layer groups - HudGroups.register(self, "implant/map/starchart") - HudGroups.app_changed.connect(_on_app_changed) - - _load_data() - if _data_loaded: - _compute_layout() - - -func _process(_delta: float) -> void: - if not visible: - return - if _dirty and _data_loaded: - queue_redraw() - _dirty = false - - -## Called from main.gd when insert state changes. -func set_insert_active(active: bool) -> void: - _insert_active = active - if not active and HudGroups.is_app_active("implant/map/starchart"): - HudGroups.close_app() - - -## Toggle via HUD layer system (D-170). -func toggle_visible() -> void: - HudGroups.toggle_app("implant/map/starchart") - - -## Respond to app layer changes (D-170). -func _on_app_changed(app_path: String, mode: int) -> void: - if app_path != "implant/map/starchart": - return - if mode == HudGroups.Mode.FULLSCREEN or mode == HudGroups.Mode.INSERT: - visible = true - _dirty = true - else: - visible = false - - -## Return the currently selected system data, or empty dict. -func get_selected_system() -> Dictionary: - return _node_lookup.get(_selected_system, {}) - - -## Return total system count. -func get_system_count() -> int: - return _nodes.size() - - -## Return total edge count. -func get_edge_count() -> int: - return _edges.size() - - -# ============================================================================= -# Data loading -# ============================================================================= - - -func _load_data() -> void: - if not FileAccess.file_exists(DATA_PATH): - push_warning("StarMapRenderer: data file not found at %s" % DATA_PATH) - return - var file := FileAccess.open(DATA_PATH, FileAccess.READ) - if file == null: - push_warning("StarMapRenderer: could not open %s" % DATA_PATH) - return - var parsed: Variant = JSON.parse_string(file.get_as_text()) - file.close() - if not (parsed is Dictionary): - push_warning("StarMapRenderer: invalid JSON in %s" % DATA_PATH) - return - var data := parsed as Dictionary - _nodes = data.get("nodes", []) - _edges = data.get("edges", []) - for node: Dictionary in _nodes: - _node_lookup[node.get("system_id", "")] = node - _data_loaded = true - - -# ============================================================================= -# Layout — place systems on concentric rings by hop distance -# ============================================================================= - - -func _compute_layout() -> void: - _node_positions.clear() - - # Group nodes by hop distance - var rings: Dictionary = {} # hop -> Array of nodes - for node: Dictionary in _nodes: - var hop: int = int(node.get("hop_distance", 0)) - if not rings.has(hop): - rings[hop] = [] - rings[hop].append(node) - - # Place each ring - for hop: int in rings: - var ring_nodes: Array = rings[hop] - var radius: float = MIN_RING_RADIUS + hop * RING_SPACING - - if hop == 0: - # Gateway at center - for node: Dictionary in ring_nodes: - _node_positions[node["system_id"]] = Vector2.ZERO - continue - - # Sort nodes within ring by sector for angular grouping - ring_nodes.sort_custom(_sort_by_sector_angle) - - # Distribute nodes within their sector's angular range - var sector_groups: Dictionary = {} - for node: Dictionary in ring_nodes: - var sector: String = node.get("geographic_sector", "unknown") - if not sector_groups.has(sector): - sector_groups[sector] = [] - sector_groups[sector].append(node) - - for sector: String in sector_groups: - var group: Array = sector_groups[sector] - var count: int = group.size() - - var center_angle: float - var spread: float - if sector == "core": - center_angle = 0.0 - spread = CORE_ANGLE_SPREAD - elif sector == "deep_frontier": - center_angle = 0.0 - spread = DEEP_FRONTIER_ANGLE_SPREAD - elif SECTOR_ANGLE_CENTER.has(sector): - center_angle = SECTOR_ANGLE_CENTER[sector] - spread = SECTOR_ANGLE_SPREAD - else: - center_angle = 0.0 - spread = TAU - - # Distribute evenly within sector arc, with deterministic offset per system - for i: int in range(count): - var node: Dictionary = group[i] - var t: float - if count == 1: - t = 0.0 - else: - t = float(i) / float(count) - 0.5 # -0.5 to +0.5 - var angle: float = center_angle + t * spread - # Add small per-node jitter based on system_id hash for visual variety - var jitter: float = _system_hash(node["system_id"]) * 0.08 - angle += jitter - # Slight radial variation to avoid perfect circles - var r_var: float = ( - radius + _system_hash(node["system_id"] + "r") * RING_SPACING * 0.3 - ) - _node_positions[node["system_id"]] = Vector2(cos(angle), sin(angle)) * r_var - - -func _sort_by_sector_angle(a: Dictionary, b: Dictionary) -> bool: - var sa: float = _sector_sort_key(a) - var sb: float = _sector_sort_key(b) - if sa != sb: - return sa < sb - return a.get("system_id", "") < b.get("system_id", "") - - -func _sector_sort_key(node: Dictionary) -> float: - var sector: String = node.get("geographic_sector", "unknown") - match sector: - "core": - return 0.0 - "north_reach": - return 1.0 - "east_reach": - return 2.0 - "south_reach": - return 3.0 - "west_reach": - return 4.0 - "deep_frontier": - return 5.0 - _: - return 6.0 # gdlint:ignore = max-returns - - -## Deterministic float in [-1, 1] from a string key. -func _system_hash(key: String) -> float: - var h: int = key.hash() & 0x7FFFFFFF # mask to 31-bit positive range - return float(h) / 2147483647.0 * 2.0 - 1.0 - - -# ============================================================================= -# Drawing -# ============================================================================= - - -func _draw() -> void: - if not _data_loaded: - return - - var sz: Vector2 = get_rect().size - var center: Vector2 = sz * MAP_CENTER_FRACTION + _pan_offset - - # Background - draw_rect(Rect2(Vector2.ZERO, sz), COLOR_BG) - - # Hop rings (concentric circles) - _draw_rings(center) - - # Sector labels - _draw_sector_labels(center) - - # Edges — only draw from selected system (UX rule: full edge web is too dense) - if _selected_system != "": - _draw_edges(center) - - # System dots - _draw_systems(center) - - # Selection highlight - if _selected_system != "": - _draw_selection(center) - - # Info panel positioned in top-right, clamped to screen (D-169) - if _info_panel: - _info_panel.visible = _selected_system != "" - if _info_panel.visible: - # Force layout so size.y is accurate for clamping - _info_panel.reset_size() - var px: float = sz.x - POPUP_WIDTH - POPUP_MARGIN - var py: float = POPUP_MARGIN - # Clamp to keep panel fully on screen - var panel_h: float = _info_panel.size.y - if panel_h > 0.0 and py + panel_h > sz.y - POPUP_MARGIN: - py = sz.y - panel_h - POPUP_MARGIN - px = maxf(POPUP_MARGIN, px) - py = maxf(POPUP_MARGIN, py) - _info_panel.position = Vector2(px, py) - - # Title - _draw_title() - - -func _draw_rings(center: Vector2) -> void: - for hop: int in range(MAX_HOP_RINGS + 1): - var radius: float = (MIN_RING_RADIUS + hop * RING_SPACING) * _zoom - if radius < 1.0 or radius > 2000.0: - continue - var color: Color = COLOR_RING_MAJOR if hop % 5 == 0 else COLOR_RING - draw_arc(center, radius, 0.0, TAU, 64, color, 0.5 if hop % 5 == 0 else 0.3, true) - - -func _draw_sector_labels(center: Vector2) -> void: - var label_radius: float = (MIN_RING_RADIUS + 12 * RING_SPACING) * _zoom - for sector: String in SECTOR_LABELS: - var angle: float = SECTOR_ANGLE_CENTER.get(sector, 0.0) - var pos: Vector2 = center + Vector2(cos(angle), sin(angle)) * label_radius - var label: String = SECTOR_LABELS[sector] - var color: Color = SECTOR_COLORS.get(sector, COLOR_TEXT_DIM) - var font := get_theme_default_font() - var font_size: int = 10 - var text_size: Vector2 = font.get_string_size( - label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size - ) - draw_string( - font, pos - text_size / 2.0, label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color - ) - - -func _draw_systems(center: Vector2) -> void: - for node: Dictionary in _nodes: - var sid: String = node.get("system_id", "") - if not _node_positions.has(sid): - continue - var pos: Vector2 = center + _node_positions[sid] * _zoom - var sector: String = node.get("geographic_sector", "unknown") - var topology: String = node.get("gate_topology", "") - var color: Color = SECTOR_COLORS.get(sector, COLOR_TEXT_DIM) - var radius: float = _dot_radius(topology) - - # Gateway gets special treatment - if node.get("is_gateway", false): - color = COLOR_GATEWAY - radius = GATEWAY_RADIUS - - # Dim deep frontier slightly - if sector == "deep_frontier": - color.a = 0.7 - - # Hover highlight - if sid == _hovered_system and sid != _selected_system: - draw_arc( - pos, radius + 3.0, 0.0, TAU, 16, Color(color.r, color.g, color.b, 0.4), 1.0, true - ) - - draw_circle(pos, radius, color) - - # System name label — centered below dot, zoom-based LoD - var label: String = node.get("proper_name", "") - if not label.is_empty() and label != sid: - var show_label := false - if sid == _selected_system or sid == _hovered_system: - show_label = true # always show selected/hovered - elif _zoom >= 2.0: - show_label = true # zoomed in: show all - elif _zoom >= 1.2: - show_label = topology in ["hub", "junction", ""] # medium: hubs + junctions + gateway - # else: zoomed out, only selected/hovered - - if show_label: - var label_color: Color - if sid == _selected_system or sid == _hovered_system: - label_color = COLOR_TEXT - else: - label_color = COLOR_TEXT_DIM - var font := get_theme_default_font() - var label_size := font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, 9) - draw_string( - font, - pos + Vector2(-label_size.x / 2.0, radius + 10.0), - label, - HORIZONTAL_ALIGNMENT_LEFT, - -1, - 9, - label_color, - ) - - -func _draw_edges(center: Vector2) -> void: - # Only draw edges connected to the selected system (full web is unreadable at 301 systems) - var node: Dictionary = _node_lookup.get(_selected_system, {}) - var adj: Array = node.get("adjacent_systems", []) - if adj.is_empty(): - return - var color := Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, EDGE_SELECTED_ALPHA) - var sel_pos: Vector2 = center + _node_positions.get(_selected_system, Vector2.ZERO) * _zoom - for neighbor_id: String in adj: - if not _node_positions.has(neighbor_id): - continue - var neighbor_pos: Vector2 = center + _node_positions[neighbor_id] * _zoom - draw_line(sel_pos, neighbor_pos, color, EDGE_WIDTH, true) - - -func _draw_selection(center: Vector2) -> void: - if not _node_positions.has(_selected_system): - return - var pos: Vector2 = center + _node_positions[_selected_system] * _zoom - # Selection ring only — label is drawn by _draw_systems() - draw_arc(pos, SELECTION_RING_RADIUS, 0.0, TAU, 24, COLOR_SELECTION, 1.2, true) - - -## 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 - - # ── 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", "") - if not star_type.is_empty(): - _info_panel.add_component(ImplantDataRow.new(star_type + " star")) - - var sector_str: String = node.get("geographic_sector", "unknown").replace("_", " ").to_upper() - var hop: int = int(node.get("hop_distance", 0)) - var sector_color: Color = SECTOR_COLORS.get(node.get("geographic_sector", ""), COLOR_TEXT_DIM) - _info_panel.add_component( - ImplantDataRow.new("%s corridor (hop %d)" % [sector_str, hop], sector_color) - ) - - # Bodies — single line - var bodies: String = node.get("bodies", "") - if not bodies.is_empty(): - _info_panel.add_component(ImplantDataRow.new(bodies)) - - # Population + GDP - var population: String = node.get("population", "") - var gdp: String = node.get("gdp", "") - if not population.is_empty() or not gdp.is_empty(): - _info_panel.add_component(ImplantDataRow.new("")) # blank line spacer - if not population.is_empty(): - _info_panel.add_component(ImplantDataRow.new("pop " + population)) - var gdp_label: String = "gdp " + (gdp if not gdp.is_empty() else "—") - _info_panel.add_component(ImplantDataRow.new(gdp_label)) - - # ── 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", []) - 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) - _info_panel.add_component(ImplantTextBlock.new(" · ".join(adj_names))) - - -func _draw_title() -> void: - var font := get_theme_default_font() - draw_string( - font, - Vector2(16, 28), - "THE REACH — NAVIGATOR", - HORIZONTAL_ALIGNMENT_LEFT, - -1, - 16, - COLOR_TEXT - ) - draw_string( - font, - Vector2(16, 44), - "Concord Assembly Gate Network — %d Systems" % _nodes.size(), - HORIZONTAL_ALIGNMENT_LEFT, - -1, - 10, - COLOR_TEXT_DIM - ) - - -func _dot_radius(topology: String) -> float: - match topology: - "hub": - return DOT_RADIUS_HUB - "junction": - return DOT_RADIUS_JUNCTION - "dead_end": - return DOT_RADIUS_DEAD_END - _: - return DOT_RADIUS_DEFAULT - - -# ============================================================================= -# Input — selection, pan, zoom -# ============================================================================= - - -func _gui_input(event: InputEvent) -> void: - if event is InputEventMouseButton: - var mb := event as InputEventMouseButton - if mb.pressed: - match mb.button_index: - MOUSE_BUTTON_LEFT: - _handle_click(mb.position) - MOUSE_BUTTON_MIDDLE: - _is_panning = true - _pan_start = mb.position - _pan_start_offset = _pan_offset - MOUSE_BUTTON_WHEEL_UP: - var old_zoom := _zoom - _zoom = clampf(_zoom + ZOOM_STEP, ZOOM_MIN, ZOOM_MAX) - if _zoom != old_zoom: - _dirty = true - MOUSE_BUTTON_WHEEL_DOWN: - var old_zoom := _zoom - _zoom = clampf(_zoom - ZOOM_STEP, ZOOM_MIN, ZOOM_MAX) - if _zoom != old_zoom: - _dirty = true - else: - if mb.button_index == MOUSE_BUTTON_MIDDLE: - _is_panning = false - - elif event is InputEventMouseMotion: - var mm := event as InputEventMouseMotion - if _is_panning: - _pan_offset = _pan_start_offset + (mm.position - _pan_start) - _dirty = true - else: - _update_hover(mm.position) - - -## Find the system_id of the nearest node to screen position, or "" if none within HIT_RADIUS. -func _find_nearest_system(pos: Vector2) -> String: - var sz: Vector2 = get_rect().size - var center: Vector2 = sz * MAP_CENTER_FRACTION + _pan_offset - var best_dist: float = HIT_RADIUS - var best_sid: String = "" - for node: Dictionary in _nodes: - var sid: String = node.get("system_id", "") - if not _node_positions.has(sid): - continue - var node_pos: Vector2 = center + _node_positions[sid] * _zoom - var dist: float = pos.distance_to(node_pos) - if dist < best_dist: - best_dist = dist - best_sid = sid - return best_sid - - -func _handle_click(pos: Vector2) -> void: - var nearest := _find_nearest_system(pos) - _selected_system = nearest - _rebuild_info_panel() - _dirty = true - - -func _update_hover(pos: Vector2) -> void: - var nearest := _find_nearest_system(pos) - if nearest != _hovered_system: - _hovered_system = nearest - _dirty = true diff --git a/client/ui/star_map.tscn b/client/ui/star_map.tscn deleted file mode 100644 index 0754fb0f6..000000000 --- a/client/ui/star_map.tscn +++ /dev/null @@ -1,18 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://ui/star_map.gd" id="1_starmap"] - -; #674: Star map insert module — concentric hop-ring view of the Settled Reach gate network. -; Sector-colored, interactive selection, pan/zoom. Accessible from the insert UI. -; Data source: res://data/star_map_data.json (enriched from star-map.json + systems.db). -; Positioned as full-size overlay. Toggle visibility via set_insert_active() or toggle_visible(). - -[node name="StarMapRenderer" type="Control"] -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 1 -script = ExtResource("1_starmap")