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:
@@ -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