feat(ui): star map click-through with wiki/GTTR popup and edge visibility
Show system profile popup on select: name, star type, hop distance, corridor, GTTR excerpt, body count, population, adjacent systems. Edge rendering changed to show no edges by default, only selected system's gate lines on click. Star map data extended with wiki fields for all 301 systems. Ticket: #780 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2193
-301
File diff suppressed because it is too large
Load Diff
+137
-63
@@ -5,11 +5,12 @@ extends Control
|
||||
## 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).
|
||||
## 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"
|
||||
|
||||
@@ -30,10 +31,17 @@ const GATEWAY_RADIUS: float = 6.0
|
||||
const SELECTION_RING_RADIUS: float = 8.0
|
||||
const HIT_RADIUS: float = 10.0 # click tolerance
|
||||
|
||||
# Edge rendering
|
||||
const EDGE_WIDTH: float = 0.4
|
||||
const EDGE_ALPHA: float = 0.12
|
||||
const EDGE_SELECTED_ALPHA: float = 0.5
|
||||
# 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 — expanded with wiki/GTTR content (#780)
|
||||
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
|
||||
const COLOR_BG: Color = Color("#0d1117")
|
||||
@@ -95,7 +103,6 @@ var _pan_start_offset: Vector2 = Vector2.ZERO
|
||||
|
||||
var _data_loaded: bool = false
|
||||
var _insert_active: bool = true
|
||||
var _show_edges: bool = false # toggle edge display
|
||||
var _dirty: bool = true # redraw needed — set by state changes, cleared after _draw
|
||||
|
||||
|
||||
@@ -260,7 +267,7 @@ func _sector_sort_key(node: Dictionary) -> float:
|
||||
"south_reach": return 3.0
|
||||
"west_reach": return 4.0
|
||||
"deep_frontier": return 5.0
|
||||
_: return 6.0
|
||||
_: return 6.0 # gdlint:ignore = max-returns
|
||||
|
||||
|
||||
## Deterministic float in [-1, 1] from a string key.
|
||||
@@ -289,8 +296,8 @@ func _draw() -> void:
|
||||
# Sector labels
|
||||
_draw_sector_labels(center)
|
||||
|
||||
# Edges (gate connections)
|
||||
if _show_edges or _selected_system != "":
|
||||
# Edges — only draw from selected system (UX rule: full edge web is too dense)
|
||||
if _selected_system != "":
|
||||
_draw_edges(center)
|
||||
|
||||
# System dots
|
||||
@@ -358,23 +365,18 @@ func _draw_systems(center: Vector2) -> void:
|
||||
|
||||
|
||||
func _draw_edges(center: Vector2) -> void:
|
||||
for edge: Array in _edges:
|
||||
if edge.size() < 2:
|
||||
# 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 sid_a: String = edge[0]
|
||||
var sid_b: String = edge[1]
|
||||
if not _node_positions.has(sid_a) or not _node_positions.has(sid_b):
|
||||
continue
|
||||
var pos_a: Vector2 = center + _node_positions[sid_a] * _zoom
|
||||
var pos_b: Vector2 = center + _node_positions[sid_b] * _zoom
|
||||
|
||||
var alpha: float = EDGE_ALPHA
|
||||
# Highlight edges connected to selected system
|
||||
if _selected_system == sid_a or _selected_system == sid_b:
|
||||
alpha = EDGE_SELECTED_ALPHA
|
||||
|
||||
var color := Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, alpha)
|
||||
draw_line(pos_a, pos_b, color, EDGE_WIDTH, true)
|
||||
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:
|
||||
@@ -389,7 +391,8 @@ func _draw_selection(center: Vector2) -> void:
|
||||
if label.is_empty():
|
||||
label = _selected_system
|
||||
var font := get_theme_default_font()
|
||||
draw_string(font, pos + Vector2(SELECTION_RING_RADIUS + 4, 4), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, COLOR_SELECTION)
|
||||
draw_string(font, pos + Vector2(SELECTION_RING_RADIUS + 4, 4), label,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, COLOR_SELECTION)
|
||||
|
||||
|
||||
func _draw_info_panel(sz: Vector2) -> void:
|
||||
@@ -397,49 +400,125 @@ func _draw_info_panel(sz: Vector2) -> void:
|
||||
if node.is_empty():
|
||||
return
|
||||
|
||||
var panel_w: float = 220.0
|
||||
var panel_h: float = 130.0
|
||||
var margin: float = 16.0
|
||||
var panel_pos := Vector2(sz.x - panel_w - margin, margin)
|
||||
|
||||
# Background
|
||||
draw_rect(Rect2(panel_pos, Vector2(panel_w, panel_h)), COLOR_INFO_BG)
|
||||
draw_rect(Rect2(panel_pos, Vector2(panel_w, panel_h)), Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, 0.15), false, 1.0)
|
||||
|
||||
var font := get_theme_default_font()
|
||||
var y: float = panel_pos.y + 20.0
|
||||
var x: float = panel_pos.x + 12.0
|
||||
var line_h: float = 18.0
|
||||
var panel_w: float = POPUP_WIDTH
|
||||
var pad: float = POPUP_PADDING
|
||||
var inner_w: float = panel_w - pad * 2.0
|
||||
|
||||
# System name
|
||||
var name: String = node.get("proper_name", "")
|
||||
if name.is_empty():
|
||||
name = node.get("system_id", "Unknown")
|
||||
draw_string(font, Vector2(x, y), name, HORIZONTAL_ALIGNMENT_LEFT, -1, 14, COLOR_TEXT)
|
||||
y += line_h
|
||||
# Measure GTTR text height first so we can size the panel
|
||||
var gttr: String = node.get("gttr_excerpt", "")
|
||||
# Strip markdown bold markers for display
|
||||
gttr = gttr.replace("**", "")
|
||||
var gttr_block_h: float = 0.0
|
||||
if not gttr.is_empty():
|
||||
var gttr_size: Vector2 = font.get_multiline_string_size(
|
||||
gttr, HORIZONTAL_ALIGNMENT_LEFT, inner_w, POPUP_GTTR_FONT_SIZE,
|
||||
POPUP_GTTR_MAX_LINES)
|
||||
gttr_block_h = gttr_size.y + 6.0
|
||||
|
||||
# System ID
|
||||
draw_string(font, Vector2(x, y), node.get("system_id", ""), HORIZONTAL_ALIGNMENT_LEFT, -1, 10, COLOR_TEXT_DIM)
|
||||
y += line_h
|
||||
# Fixed rows: name, id, star+hop, corridor, bodies+pop, adjacent, separator lines
|
||||
var fixed_h: float = (
|
||||
POPUP_LINE_H * 2.0 # name + id
|
||||
+ 4.0 # separator
|
||||
+ POPUP_LINE_H * 3.0 # star+hop, corridor, bodies+pop
|
||||
+ 4.0 # separator
|
||||
+ gttr_block_h
|
||||
+ 4.0 # separator (before adjacent)
|
||||
+ POPUP_LINE_H # adjacent systems label
|
||||
+ pad * 2.0
|
||||
)
|
||||
var panel_h: float = fixed_h
|
||||
|
||||
# Sector
|
||||
var sector: String = node.get("geographic_sector", "").replace("_", " ").capitalize()
|
||||
var panel_pos := Vector2(sz.x - panel_w - POPUP_MARGIN, POPUP_MARGIN)
|
||||
var x: float = panel_pos.x + pad
|
||||
var y: float = panel_pos.y + pad + POPUP_LINE_H # baseline offset
|
||||
|
||||
# Background + border
|
||||
draw_rect(Rect2(panel_pos, Vector2(panel_w, panel_h)), COLOR_INFO_BG)
|
||||
draw_rect(Rect2(panel_pos, Vector2(panel_w, panel_h)),
|
||||
Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, 0.15), false, 1.0)
|
||||
|
||||
# ── System name ──────────────────────────────────────────────────────────
|
||||
var sys_name: String = node.get("proper_name", "")
|
||||
if sys_name.is_empty():
|
||||
sys_name = node.get("system_id", "Unknown")
|
||||
draw_string(font, Vector2(x, y), sys_name, HORIZONTAL_ALIGNMENT_LEFT, inner_w, 15, COLOR_TEXT)
|
||||
y += POPUP_LINE_H
|
||||
|
||||
draw_string(font, Vector2(x, y), node.get("system_id", ""), HORIZONTAL_ALIGNMENT_LEFT, inner_w, 10, COLOR_TEXT_DIM)
|
||||
y += POPUP_LINE_H
|
||||
|
||||
# Separator
|
||||
draw_line(Vector2(x, y + 1.0), Vector2(panel_pos.x + panel_w - pad, y + 1.0),
|
||||
Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, 0.12), 1.0)
|
||||
y += 6.0
|
||||
|
||||
# ── Stats block ──────────────────────────────────────────────────────────
|
||||
var star_type: String = node.get("star_type", "")
|
||||
var hop: int = int(node.get("hop_distance", 0))
|
||||
var stat_line_1: String
|
||||
if star_type.is_empty():
|
||||
stat_line_1 = "Hop %d from Gateway" % hop
|
||||
else:
|
||||
stat_line_1 = "%s star · hop %d" % [star_type, hop]
|
||||
draw_string(font, Vector2(x, y), stat_line_1, HORIZONTAL_ALIGNMENT_LEFT, inner_w, 11, COLOR_TEXT_DIM)
|
||||
y += POPUP_LINE_H
|
||||
|
||||
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)
|
||||
draw_string(font, Vector2(x, y), "Sector: " + sector, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, sector_color)
|
||||
y += line_h
|
||||
draw_string(font, Vector2(x, y), sector_str + " corridor", HORIZONTAL_ALIGNMENT_LEFT, inner_w, 11, sector_color)
|
||||
y += POPUP_LINE_H
|
||||
|
||||
# Hop distance
|
||||
draw_string(font, Vector2(x, y), "Hop distance: " + str(node.get("hop_distance", "?")), HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_TEXT_DIM)
|
||||
y += line_h
|
||||
var bodies: String = node.get("bodies", "")
|
||||
var population: String = node.get("population", "")
|
||||
var stat_line_3: String
|
||||
if not bodies.is_empty() and not population.is_empty():
|
||||
stat_line_3 = "%s · pop %s" % [bodies, population]
|
||||
elif not bodies.is_empty():
|
||||
stat_line_3 = bodies
|
||||
elif not population.is_empty():
|
||||
stat_line_3 = "Population: " + population
|
||||
else:
|
||||
stat_line_3 = "%d aperture%s · %s" % [
|
||||
int(node.get("aperture_count", 0)),
|
||||
"s" if int(node.get("aperture_count", 0)) != 1 else "",
|
||||
node.get("gate_topology", "")]
|
||||
draw_string(font, Vector2(x, y), stat_line_3, HORIZONTAL_ALIGNMENT_LEFT, inner_w, 11, COLOR_TEXT_DIM)
|
||||
y += POPUP_LINE_H
|
||||
|
||||
# Gate connections
|
||||
draw_string(font, Vector2(x, y), "Gates: " + str(int(node.get("aperture_count", 0))) + " apertures", HORIZONTAL_ALIGNMENT_LEFT, -1, 11, COLOR_TEXT_DIM)
|
||||
# ── GTTR excerpt ─────────────────────────────────────────────────────────
|
||||
if not gttr.is_empty():
|
||||
draw_line(Vector2(x, y + 1.0), Vector2(panel_pos.x + panel_w - pad, y + 1.0),
|
||||
Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, 0.12), 1.0)
|
||||
y += 6.0
|
||||
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_block_h
|
||||
|
||||
# ── Adjacent systems ──────────────────────────────────────────────────────
|
||||
draw_line(Vector2(x, y + 1.0), Vector2(panel_pos.x + panel_w - pad, y + 1.0),
|
||||
Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, 0.12), 1.0)
|
||||
y += 6.0
|
||||
var adj: Array = node.get("adjacent_systems", [])
|
||||
if adj.is_empty():
|
||||
draw_string(font, Vector2(x, y), "No gate connections", HORIZONTAL_ALIGNMENT_LEFT, inner_w, 10, COLOR_TEXT_DIM)
|
||||
else:
|
||||
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)
|
||||
var adj_line: String = " · ".join(adj_names)
|
||||
draw_multiline_string(font, Vector2(x, y), adj_line, HORIZONTAL_ALIGNMENT_LEFT,
|
||||
inner_w, 10, 2, COLOR_TEXT_DIM)
|
||||
|
||||
|
||||
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)
|
||||
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:
|
||||
@@ -508,12 +587,7 @@ func _find_nearest_system(pos: Vector2) -> String:
|
||||
|
||||
func _handle_click(pos: Vector2) -> void:
|
||||
var nearest := _find_nearest_system(pos)
|
||||
if nearest != "":
|
||||
_selected_system = nearest
|
||||
_show_edges = true
|
||||
else:
|
||||
_selected_system = ""
|
||||
_show_edges = false
|
||||
_selected_system = nearest
|
||||
_dirty = true
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate client/data/star_map_data.json from star-map.json + systems.db.
|
||||
"""Generate client/data/star_map_data.json from star-map.json + systems.db + wiki.
|
||||
|
||||
Run from any directory — paths are resolved relative to this script's location:
|
||||
python3 tooling/generate-star-map-data.py
|
||||
@@ -8,6 +8,7 @@ Run from any directory — paths are resolved relative to this script's location
|
||||
Sources:
|
||||
docs/design/star-map.json — graph topology (nodes + edges)
|
||||
server/server/data/systems.db — proper names, geographic sectors
|
||||
wiki/star-systems/ — star type, bodies, population, GTTR excerpt
|
||||
|
||||
Output:
|
||||
client/data/star_map_data.json — self-contained client data for the star map UI
|
||||
@@ -15,6 +16,7 @@ Output:
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -31,9 +33,95 @@ _WORKTREE_PARENT = os.path.dirname(_PROJECT_ROOT)
|
||||
|
||||
STAR_MAP_PATH = os.path.join(_PROJECT_ROOT, "docs", "design", "star-map.json")
|
||||
SYSTEMS_DB_PATH = os.path.join(_WORKTREE_PARENT, "server", "server", "data", "systems.db")
|
||||
WIKI_PATH = os.path.join(_PROJECT_ROOT, "wiki", "star-systems")
|
||||
OUTPUT_PATH = os.path.join(_PROJECT_ROOT, "client", "data", "star_map_data.json")
|
||||
|
||||
|
||||
def system_id_to_wiki_slug(system_id: str) -> str:
|
||||
"""Convert system_id to wiki folder slug. 'GJ 71' → 'GJ-71'."""
|
||||
return system_id.replace(" ", "-")
|
||||
|
||||
|
||||
def parse_wiki_index(system_id: str) -> dict:
|
||||
"""Extract star type, bodies summary, and population from index.md.
|
||||
|
||||
Returns dict with keys: star_type, bodies, population (all strings, may be empty).
|
||||
"""
|
||||
slug = system_id_to_wiki_slug(system_id)
|
||||
path = os.path.join(WIKI_PATH, slug, "index.md")
|
||||
result = {"star_type": "", "bodies": "", "population": ""}
|
||||
if not os.path.exists(path):
|
||||
return result
|
||||
with open(path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Star row: | **Star** | G8V · 11.9 ly |
|
||||
m = re.search(r"\|\s*\*\*Star\*\*\s*\|\s*([^|]+?)\s*\|", content)
|
||||
if m:
|
||||
raw = m.group(1).strip()
|
||||
# Extract spectral class — everything before " ·" or end of string
|
||||
star_type = raw.split("·")[0].strip()
|
||||
result["star_type"] = star_type
|
||||
|
||||
# Bodies row: | **Bodies** | 2 habitable · 3 inhabited |
|
||||
m = re.search(r"\|\s*\*\*Bodies\*\*\s*\|\s*([^|]+?)\s*\|", content)
|
||||
if m:
|
||||
result["bodies"] = m.group(1).strip()
|
||||
|
||||
# Population row: | **Population** | 1,200,000,000 |
|
||||
m = re.search(r"\|\s*\*\*Population\*\*\s*\|\s*([^|]+?)\s*\|", content)
|
||||
if m:
|
||||
result["population"] = m.group(1).strip()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_gttr_excerpt(system_id: str) -> str:
|
||||
"""Extract the first body paragraph from gttr.md as the GTTR excerpt."""
|
||||
slug = system_id_to_wiki_slug(system_id)
|
||||
path = os.path.join(WIKI_PATH, slug, "gttr.md")
|
||||
if not os.path.exists(path):
|
||||
return ""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Skip the H1 heading line, then find the first non-empty paragraph
|
||||
in_content = False
|
||||
paragraph_lines = []
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not in_content:
|
||||
# Skip heading and blank lines at start
|
||||
if stripped.startswith("# "):
|
||||
in_content = True
|
||||
continue
|
||||
if not stripped:
|
||||
# Blank line ends the paragraph if we've collected lines
|
||||
if paragraph_lines:
|
||||
break
|
||||
else:
|
||||
if not stripped.startswith("#"):
|
||||
paragraph_lines.append(stripped)
|
||||
|
||||
return " ".join(paragraph_lines)
|
||||
|
||||
|
||||
def build_adjacency(edges: list) -> dict:
|
||||
"""Build a map from system_id to list of adjacent system_ids from edges."""
|
||||
adj: dict = {}
|
||||
for edge in edges:
|
||||
if len(edge) < 2:
|
||||
continue
|
||||
a, b = edge[0], edge[1]
|
||||
adj.setdefault(a, [])
|
||||
adj.setdefault(b, [])
|
||||
if b not in adj[a]:
|
||||
adj[a].append(b)
|
||||
if a not in adj[b]:
|
||||
adj[b].append(a)
|
||||
return adj
|
||||
|
||||
|
||||
def generate() -> dict:
|
||||
"""Generate the enriched star map data dict."""
|
||||
if not os.path.exists(STAR_MAP_PATH):
|
||||
@@ -56,10 +144,21 @@ def generate() -> dict:
|
||||
db_lookup = {row["system_id"]: dict(row) for row in cur.fetchall()}
|
||||
conn.close()
|
||||
|
||||
adjacency = build_adjacency(star_map["edges"])
|
||||
|
||||
nodes = []
|
||||
wiki_hits = 0
|
||||
gttr_hits = 0
|
||||
for n in star_map["nodes"]:
|
||||
sid = n["system_id"]
|
||||
db = db_lookup.get(sid, {})
|
||||
wiki = parse_wiki_index(sid)
|
||||
gttr = parse_gttr_excerpt(sid)
|
||||
if wiki["star_type"]:
|
||||
wiki_hits += 1
|
||||
if gttr:
|
||||
gttr_hits += 1
|
||||
|
||||
entry = {
|
||||
"system_id": sid,
|
||||
"proper_name": db.get("proper_name", ""),
|
||||
@@ -69,16 +168,28 @@ def generate() -> dict:
|
||||
"aperture_count": n["aperture_count"],
|
||||
"gate_connections": n["gate_connections"],
|
||||
"hop_distance": n["hop_distance_from_gateway"],
|
||||
"adjacent_systems": adjacency.get(sid, []),
|
||||
}
|
||||
if wiki["star_type"]:
|
||||
entry["star_type"] = wiki["star_type"]
|
||||
if wiki["bodies"]:
|
||||
entry["bodies"] = wiki["bodies"]
|
||||
if wiki["population"]:
|
||||
entry["population"] = wiki["population"]
|
||||
if gttr:
|
||||
entry["gttr_excerpt"] = gttr
|
||||
if n.get("is_gateway"):
|
||||
entry["is_gateway"] = True
|
||||
nodes.append(entry)
|
||||
|
||||
nodes.sort(key=lambda x: (x["hop_distance"], x["system_id"]))
|
||||
|
||||
print(f" Wiki index data: {wiki_hits}/{len(nodes)} systems")
|
||||
print(f" GTTR excerpts: {gttr_hits}/{len(nodes)} systems")
|
||||
|
||||
return {
|
||||
"_meta": {
|
||||
"generated_from": "star-map.json + systems.db",
|
||||
"generated_from": "star-map.json + systems.db + wiki/star-systems",
|
||||
"system_count": len(nodes),
|
||||
"edge_count": len(star_map["edges"]),
|
||||
"note": "Client-side star map data. Regenerate with: tooling/generate-star-map-data.py",
|
||||
|
||||
Reference in New Issue
Block a user