fix(ui): address PR #124 review — navigation, error handling, cleanup

- Economics panel: replace dead _gui_input LEFT/RIGHT with public
  navigate() method, wire [ ] keys in main.gd (avoids movement
  key conflict, fixes focus_mode=NONE issue)
- Debug console: add explicit effect guard in econ inject no-commodity
  branch so invalid effects don't fall to commodity-form error
- Snapshot consumer: null-clear GameState.economy_snapshot after
  consuming (matches one-shot consumer invariant)
- Star map: remove duplicate doc comment above set_insert_active()
- Generate script: remove stale comment, dead _WORKTREE_PARENT var,
  dead field extraction in parse_wiki_index, add try/except around
  DB queries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 14:03:49 +02:00
co-authored by Claude Opus 4.6
parent 532fcd64a5
commit 28d95c23a8
6 changed files with 69 additions and 79 deletions
+8
View File
@@ -176,6 +176,14 @@ func _unhandled_key_input(event: InputEvent) -> void:
# #824: E — toggle Economics Monitor implant panel
if economics_panel:
economics_panel.toggle_visible()
elif event is InputEventKey and event.keycode == KEY_BRACKETLEFT:
# #824: [ — cycle economics panel system selector backward
if economics_panel and HudGroups.is_app_active("implant/economics"):
economics_panel.navigate(-1)
elif event is InputEventKey and event.keycode == KEY_BRACKETRIGHT:
# #824: ] — cycle economics panel system selector forward
if economics_panel and HudGroups.is_app_active("implant/economics"):
economics_panel.navigate(1)
func _process(delta: float) -> void:
+1
View File
@@ -183,6 +183,7 @@ func consume_economy_snapshot() -> void:
return
if economics_panel.has_method("receive_economy_data"):
economics_panel.receive_economy_data(GameState.economy_snapshot)
GameState.economy_snapshot = null
# #174: Consume examine result — show overlay when server sends character-filtered observation.
+6
View File
@@ -287,6 +287,12 @@ func _econ_inject(parts: Array) -> void:
magnitude_str = args[2]
if args.size() >= 4:
ticks_str = args[3]
elif args.size() == 3:
# 3 args but args[1] isn't shock/boost — bad effect keyword, not a commodity
_append_text(
"econ inject: effect must be 'shock' or 'boost', got '%s'" % args[1], ERROR_COLOR
)
return
else:
# With commodity_id: econ inject <system> <commodity> <effect> <magnitude> [ticks]
commodity_id = args[1]
+10 -25
View File
@@ -167,10 +167,11 @@ func _load_system_list() -> void:
var sid: String = node.get("system_id", "")
if not sid.is_empty():
_systems.append(node)
_systems.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var na: String = a.get("proper_name", a.get("system_id", ""))
var nb: String = b.get("proper_name", b.get("system_id", ""))
return na < nb
_systems.sort_custom(
func(a: Dictionary, b: Dictionary) -> bool:
var na: String = a.get("proper_name", a.get("system_id", ""))
var nb: String = b.get("proper_name", b.get("system_id", ""))
return na < nb
)
if not _systems.is_empty():
selected_system = _systems[0].get("system_id", "")
@@ -256,7 +257,7 @@ func _rebuild_panel() -> void:
_panel.add_component(_placeholder_notice)
_panel.add_component(ImplantSeparator.new())
_panel.add_component(ImplantTextBlock.new("◄ ► select · E close"))
_panel.add_component(ImplantTextBlock.new("[ ] select system · E close"))
func _current_node() -> Dictionary:
@@ -304,27 +305,11 @@ func _on_economy_data_updated(system_id: String, data: Dictionary) -> void:
_placeholder_notice.text = "LIVE DATA ACTIVE"
# =============================================================================
# Input — LEFT/RIGHT arrow navigation between systems
# =============================================================================
func _gui_input(event: InputEvent) -> void:
if not visible:
return
if event is InputEventKey and event.pressed and not event.is_echo():
match event.keycode:
KEY_LEFT:
_cycle_system(-1)
get_viewport().set_input_as_handled()
KEY_RIGHT:
_cycle_system(1)
get_viewport().set_input_as_handled()
func _cycle_system(direction: int) -> void:
## Cycle the system selector by delta steps (+1 or -1).
## Called from main.gd _unhandled_key_input — [ and ] keys when panel is active.
func navigate(delta: int) -> void:
if _systems.is_empty():
return
_selected_idx = wrapi(_selected_idx + direction, 0, _systems.size())
_selected_idx = wrapi(_selected_idx + delta, 0, _systems.size())
selected_system = _systems[_selected_idx].get("system_id", "")
_rebuild_panel()
-1
View File
@@ -135,7 +135,6 @@ func _process(_delta: float) -> void:
_dirty = false
## Called from main.gd when insert state changes.
## Called from main.gd when insert state changes.
func set_insert_active(active: bool) -> void:
_insert_active = active
+44 -53
View File
@@ -7,8 +7,8 @@ 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
server/data/systems.db — proper names, geographic sectors, bodies, GDP tier
wiki/star-systems/ — star type, GTTR excerpt (bodies/population from systems.db)
Output:
client/data/star_map_data.json — self-contained client data for the star map UI
@@ -21,16 +21,11 @@ import sqlite3
import sys
import tempfile
# Resolve project root from this script's location: tooling/ is one level below root.
# Resolve project root from this script's location.
# Works regardless of cwd — no fragile relative path guessing.
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
_PROJECT_ROOT = os.path.dirname(_SCRIPT_DIR)
# Worktree layout: settled-reach/{client,server,main}/
# This script lives in client/tooling/, so _PROJECT_ROOT = client/.
# The parent of _PROJECT_ROOT is the worktree parent where sibling dirs live.
_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(_PROJECT_ROOT, "server", "data", "systems.db")
WIKI_PATH = os.path.join(_PROJECT_ROOT, "wiki", "star-systems")
@@ -43,13 +38,14 @@ def system_id_to_wiki_slug(system_id: str) -> str:
def parse_wiki_index(system_id: str) -> dict:
"""Extract star type, bodies summary, and population from index.md.
"""Extract star type from index.md.
Returns dict with keys: star_type, bodies, population (all strings, may be empty).
Bodies and population are authoritative from systems.db — not read from wiki.
Returns dict with key: star_type (string, 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": ""}
result = {"star_type": ""}
if not os.path.exists(path):
return result
with open(path, encoding="utf-8") as f:
@@ -60,18 +56,7 @@ def parse_wiki_index(system_id: str) -> dict:
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()
result["star_type"] = raw.split("·")[0].strip()
return result
@@ -173,39 +158,45 @@ def generate() -> dict:
with open(STAR_MAP_PATH) as f:
star_map = json.load(f)
conn = sqlite3.connect(SYSTEMS_DB_PATH)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute(
"SELECT system_id, proper_name, geographic_sector, geographic_band "
"FROM star_systems"
)
db_lookup = {row["system_id"]: dict(row) for row in cur.fetchall()}
try:
conn = sqlite3.connect(SYSTEMS_DB_PATH)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
# Aggregate body data per system from the bodies table
cur.execute("""
SELECT system_id,
SUM(CASE WHEN atmosphere IN ('breathable','standard') AND body_type IN ('planet','moon') THEN 1 ELSE 0 END) AS habitable,
SUM(CASE WHEN inhabited = 1 THEN 1 ELSE 0 END) AS inhabited,
SUM(CASE WHEN inhabited = 1 THEN COALESCE(population, 0) ELSE 0 END) AS total_pop
FROM bodies
GROUP BY system_id
""")
body_stats = {row["system_id"]: dict(row) for row in cur.fetchall()}
cur.execute(
"SELECT system_id, proper_name, geographic_sector, geographic_band "
"FROM star_systems"
)
db_lookup = {row["system_id"]: dict(row) for row in cur.fetchall()}
# Also sum station populations
cur.execute("""
SELECT system_id,
SUM(COALESCE(population, 0)) AS station_pop
FROM stations
GROUP BY system_id
""")
station_stats = {row["system_id"]: dict(row) for row in cur.fetchall()}
# Aggregate body data per system from the bodies table
cur.execute("""
SELECT system_id,
SUM(CASE WHEN atmosphere IN ('breathable','standard') AND body_type IN ('planet','moon') THEN 1 ELSE 0 END) AS habitable,
SUM(CASE WHEN inhabited = 1 THEN 1 ELSE 0 END) AS inhabited,
SUM(CASE WHEN inhabited = 1 THEN COALESCE(population, 0) ELSE 0 END) AS total_pop
FROM bodies
GROUP BY system_id
""")
body_stats = {row["system_id"]: dict(row) for row in cur.fetchall()}
# Economic tier for GDP calculation
cur.execute("SELECT system_id, economic_tier FROM system_economy")
econ_tiers = {row["system_id"]: row["economic_tier"] for row in cur.fetchall()}
conn.close()
# Also sum station populations
cur.execute("""
SELECT system_id,
SUM(COALESCE(population, 0)) AS station_pop
FROM stations
GROUP BY system_id
""")
station_stats = {row["system_id"]: dict(row) for row in cur.fetchall()}
# Economic tier for GDP calculation
cur.execute("SELECT system_id, economic_tier FROM system_economy")
econ_tiers = {row["system_id"]: row["economic_tier"] for row in cur.fetchall()}
except sqlite3.Error as e:
print(f"ERROR: systems.db query failed: {e}", file=sys.stderr)
sys.exit(1)
finally:
conn.close()
adjacency = build_adjacency(star_map["edges"])