Merge remote-tracking branch 'origin/sprint-34/client'
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Overheard conversations for all 31 zone types (was 5): 78 new ambient dialogue entries (94 total) with D-078 occlusion-resilient authoring, investigative knowledge payloads, and culture-neutral role-pair conversations
|
- Overheard conversations for all 31 zone types (was 5): 78 new ambient dialogue entries (94 total) with D-078 occlusion-resilient authoring, investigative knowledge payloads, and culture-neutral role-pair conversations
|
||||||
|
- Star map info panel shows system population and GDP when data is available (#785)
|
||||||
|
- Economics Monitor implant panel — system selector, 6-commodity price table with trend arrows, GDP strip (#824)
|
||||||
|
- Debug console `econ inject`, `econ param`, `econ inspect` commands for runtime economics manipulation (#825)
|
||||||
|
|
||||||
## [v0.1.33] — 2026-04-08
|
## [v0.1.33] — 2026-04-08
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -116,6 +116,12 @@ var ai_enhanced_dialogue_enabled: bool = true
|
|||||||
# "full" response hydrates ai_enhanced_dialogue_enabled (server is authoritative for persisted state).
|
# "full" response hydrates ai_enhanced_dialogue_enabled (server is authoritative for persisted state).
|
||||||
var settings_response: Variant = null
|
var settings_response: Variant = null
|
||||||
|
|
||||||
|
# v21 fields (#824, D-181): Economy snapshot from server.
|
||||||
|
# Dictionary keyed by system_id → { price_current, price_trend, trade_flow_volume,
|
||||||
|
# corporate_presence, stockpile_weeks, production_vs_baseline, official_coverage_ratio }
|
||||||
|
# Null when no economy data in the current snapshot.
|
||||||
|
var economy_snapshot: Variant = null
|
||||||
|
|
||||||
# v7 fields (#431, D-059/D-060)
|
# v7 fields (#431, D-059/D-060)
|
||||||
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
|
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ extends Node
|
|||||||
## "implant/map/starchart" — star map navigator
|
## "implant/map/starchart" — star map navigator
|
||||||
## "implant/wiki/gttr" — Drifter's Guide reader
|
## "implant/wiki/gttr" — Drifter's Guide reader
|
||||||
## "implant/journal" — knowledge journal
|
## "implant/journal" — knowledge journal
|
||||||
|
## "implant/economics" — economics monitor (D-181, #824)
|
||||||
##
|
##
|
||||||
## Usage:
|
## Usage:
|
||||||
## HudGroups.register(self, "implant/map/starchart")
|
## HudGroups.register(self, "implant/map/starchart")
|
||||||
|
|||||||
+15
-1
@@ -9,7 +9,6 @@ var _pending_record_inputs: Array = [] # #507: accumulates server-bound inputs
|
|||||||
var _router: SnapshotEventRouter # #559: callable-based snapshot dispatch
|
var _router: SnapshotEventRouter # #559: callable-based snapshot dispatch
|
||||||
var _consumers: SnapshotConsumers # #775: non-dialogue snapshot consumers
|
var _consumers: SnapshotConsumers # #775: non-dialogue snapshot consumers
|
||||||
var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers
|
var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers
|
||||||
|
|
||||||
@onready var world_renderer = $World
|
@onready var world_renderer = $World
|
||||||
@onready var fog_entities = $World/FogEntities # D-059/D-060: cognitive delay fog visualization
|
@onready var fog_entities = $World/FogEntities # D-059/D-060: cognitive delay fog visualization
|
||||||
@onready var camera = $Camera2D
|
@onready var camera = $Camera2D
|
||||||
@@ -34,6 +33,7 @@ var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers
|
|||||||
@onready var debug_console = $ModalLayer/DebugConsole # #581: tilde debug console
|
@onready var debug_console = $ModalLayer/DebugConsole # #581: tilde debug console
|
||||||
@onready var news_ticker = $UILayer/NewsTicker # #592: scrolling headline bar (D-049 z-7)
|
@onready var news_ticker = $UILayer/NewsTicker # #592: scrolling headline bar (D-049 z-7)
|
||||||
@onready var star_map = $InsertOverlay/HUD/StarMap # #674: star map insert module (hop-ring view)
|
@onready var star_map = $InsertOverlay/HUD/StarMap # #674: star map insert module (hop-ring view)
|
||||||
|
@onready var economics_panel = $InsertOverlay/HUD/EconomicsPanel # #824: economics monitor (D-170)
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -92,6 +92,7 @@ func _ready() -> void:
|
|||||||
"interaction_prompt": interaction_prompt,
|
"interaction_prompt": interaction_prompt,
|
||||||
"minimap": minimap,
|
"minimap": minimap,
|
||||||
"star_map": star_map,
|
"star_map": star_map,
|
||||||
|
"economics_panel": economics_panel,
|
||||||
},
|
},
|
||||||
_screen_flash
|
_screen_flash
|
||||||
)
|
)
|
||||||
@@ -154,6 +155,7 @@ func _ready() -> void:
|
|||||||
_router.register("dialogue_response", _dialogue.consume_dialogue_response)
|
_router.register("dialogue_response", _dialogue.consume_dialogue_response)
|
||||||
_router.register("save_result", _consumers.consume_save_result)
|
_router.register("save_result", _consumers.consume_save_result)
|
||||||
_router.register("debug_response", _consumers.consume_debug_response)
|
_router.register("debug_response", _consumers.consume_debug_response)
|
||||||
|
_router.register("economy_snapshot", _consumers.consume_economy_snapshot)
|
||||||
|
|
||||||
# #581: Wire settings_dialog debug console toggle
|
# #581: Wire settings_dialog debug console toggle
|
||||||
if settings_dialog and debug_console:
|
if settings_dialog and debug_console:
|
||||||
@@ -170,6 +172,18 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||||||
if event is InputEventKey and event.keycode == KEY_M:
|
if event is InputEventKey and event.keycode == KEY_M:
|
||||||
if star_map:
|
if star_map:
|
||||||
star_map.toggle_visible()
|
star_map.toggle_visible()
|
||||||
|
elif event is InputEventKey and event.keycode == KEY_N:
|
||||||
|
# #824: N — toggle Economics Monitor implant panel (E is bound to interact)
|
||||||
|
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:
|
func _process(delta: float) -> void:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ var interaction_list: Node = null
|
|||||||
var interaction_prompt: Node = null
|
var interaction_prompt: Node = null
|
||||||
var minimap: Node = null
|
var minimap: Node = null
|
||||||
var star_map: Node = null
|
var star_map: Node = null
|
||||||
|
var economics_panel: Node = null # #824: economics monitor (D-181)
|
||||||
|
|
||||||
var _screen_flash_fn: Callable # Callable(color: Color, duration: float)
|
var _screen_flash_fn: Callable # Callable(color: Color, duration: float)
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ func init(refs: Dictionary, screen_flash: Callable) -> SnapshotConsumers:
|
|||||||
interaction_prompt = refs.get("interaction_prompt")
|
interaction_prompt = refs.get("interaction_prompt")
|
||||||
minimap = refs.get("minimap")
|
minimap = refs.get("minimap")
|
||||||
star_map = refs.get("star_map")
|
star_map = refs.get("star_map")
|
||||||
|
economics_panel = refs.get("economics_panel")
|
||||||
_screen_flash_fn = screen_flash
|
_screen_flash_fn = screen_flash
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -54,6 +56,8 @@ func propagate_insert_state() -> void:
|
|||||||
minimap.set_insert_active(insert_state)
|
minimap.set_insert_active(insert_state)
|
||||||
if star_map:
|
if star_map:
|
||||||
star_map.set_insert_active(insert_state)
|
star_map.set_insert_active(insert_state)
|
||||||
|
if economics_panel:
|
||||||
|
economics_panel.set_insert_active(insert_state)
|
||||||
|
|
||||||
|
|
||||||
# D-057: Update interaction list from game state.
|
# D-057: Update interaction list from game state.
|
||||||
@@ -173,6 +177,15 @@ func consume_debug_response() -> void:
|
|||||||
GameState.debug_response = null
|
GameState.debug_response = null
|
||||||
|
|
||||||
|
|
||||||
|
# #824: Forward economy_snapshot from server to the economics panel (D-181).
|
||||||
|
func consume_economy_snapshot() -> void:
|
||||||
|
if GameState.economy_snapshot == null or not economics_panel:
|
||||||
|
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.
|
# #174: Consume examine result — show overlay when server sends character-filtered observation.
|
||||||
func consume_examine_result() -> void:
|
func consume_examine_result() -> void:
|
||||||
if GameState.current_examine_result == null or not examine_display:
|
if GameState.current_examine_result == null or not examine_display:
|
||||||
|
|||||||
@@ -213,6 +213,12 @@ static func apply(snapshot: Dictionary) -> void:
|
|||||||
else:
|
else:
|
||||||
GameState.settings_response = null
|
GameState.settings_response = null
|
||||||
|
|
||||||
|
# v21: economy_snapshot (#824, D-181)
|
||||||
|
if snapshot.has("economy_snapshot") and snapshot.economy_snapshot is Dictionary:
|
||||||
|
GameState.economy_snapshot = snapshot.economy_snapshot
|
||||||
|
else:
|
||||||
|
GameState.economy_snapshot = null
|
||||||
|
|
||||||
# #718: character_visual_descriptor — restored from server snapshot on save/load.
|
# #718: character_visual_descriptor — restored from server snapshot on save/load.
|
||||||
if (
|
if (
|
||||||
snapshot.has("character_visual_descriptor")
|
snapshot.has("character_visual_descriptor")
|
||||||
|
|||||||
@@ -225,10 +225,178 @@ func _dispatch(line: String) -> void:
|
|||||||
_send_debug("ListPopulation")
|
_send_debug("ListPopulation")
|
||||||
"status":
|
"status":
|
||||||
_send_debug("GetContaminationStatus")
|
_send_debug("GetContaminationStatus")
|
||||||
|
"econ":
|
||||||
|
_dispatch_econ(parts)
|
||||||
_:
|
_:
|
||||||
_append_text("unknown command: '%s' (type 'help')" % cmd, ERROR_COLOR)
|
_append_text("unknown command: '%s' (type 'help')" % cmd, ERROR_COLOR)
|
||||||
|
|
||||||
|
|
||||||
|
# #825: Economics debug console commands (D-178, D-180, D-181).
|
||||||
|
# Three subcommands: inject (fire EconEvent), param (tweak α/β/friction), inspect (read signals).
|
||||||
|
# TODO: Server-side DebugCommandKind variants (InjectEconEvent, SetEconParam, GetEconState)
|
||||||
|
# ship with #823. Until then, _send_debug will transmit the payload but the server will
|
||||||
|
# respond with an "unknown command" error. Wire format is ready; server handler is not.
|
||||||
|
func _dispatch_econ(parts: Array) -> void:
|
||||||
|
if parts.size() < 2:
|
||||||
|
_append_text(
|
||||||
|
(
|
||||||
|
"usage:\n"
|
||||||
|
+ " econ inject <system_id> [commodity_id] <shock|boost> <magnitude> [ticks]\n"
|
||||||
|
+ " econ param <alpha|beta|friction> <value> [system_a] [system_b]\n"
|
||||||
|
+ " econ inspect <system_id>"
|
||||||
|
),
|
||||||
|
ERROR_COLOR,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
var sub := parts[1].to_lower()
|
||||||
|
match sub:
|
||||||
|
"inject":
|
||||||
|
_econ_inject(parts)
|
||||||
|
"param":
|
||||||
|
_econ_param(parts)
|
||||||
|
"inspect":
|
||||||
|
_econ_inspect(parts)
|
||||||
|
_:
|
||||||
|
_append_text("econ: unknown subcommand '%s'" % sub, ERROR_COLOR)
|
||||||
|
|
||||||
|
|
||||||
|
# econ inject <system_id> [commodity_id] <shock|boost> <magnitude> [ticks]
|
||||||
|
# Minimal form: econ inject Sol shock 0.5
|
||||||
|
# Full form: econ inject Sol fusion_fuel boost 1.2 100
|
||||||
|
func _econ_inject(parts: Array) -> void:
|
||||||
|
# parts[0]="econ", parts[1]="inject", rest is args
|
||||||
|
var args := parts.slice(2)
|
||||||
|
if args.size() < 3:
|
||||||
|
_append_text(
|
||||||
|
"usage: econ inject <system_id> [commodity_id] <shock|boost> <magnitude> [ticks]",
|
||||||
|
ERROR_COLOR,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse: detect whether commodity_id is present by checking if args[1] is an effect keyword
|
||||||
|
var system_id: String = args[0]
|
||||||
|
var commodity_id: String = ""
|
||||||
|
var effect: String = ""
|
||||||
|
var magnitude_str: String = ""
|
||||||
|
var duration_ticks: int = 0
|
||||||
|
|
||||||
|
var ticks_str := ""
|
||||||
|
if args[1].to_lower() in ["shock", "boost"]:
|
||||||
|
# No commodity_id: econ inject <system> <effect> <magnitude> [ticks]
|
||||||
|
effect = args[1].to_lower()
|
||||||
|
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]
|
||||||
|
if args.size() < 4:
|
||||||
|
_append_text(
|
||||||
|
"usage: econ inject <system_id> <commodity_id> <shock|boost> <magnitude> [ticks]",
|
||||||
|
ERROR_COLOR,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
effect = args[2].to_lower()
|
||||||
|
if not effect in ["shock", "boost"]:
|
||||||
|
_append_text(
|
||||||
|
"econ inject: effect must be 'shock' or 'boost', got '%s'" % effect, ERROR_COLOR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
magnitude_str = args[3]
|
||||||
|
if args.size() >= 5:
|
||||||
|
ticks_str = args[4]
|
||||||
|
|
||||||
|
if not ticks_str.is_empty():
|
||||||
|
if not ticks_str.is_valid_int():
|
||||||
|
_append_text("econ inject: invalid ticks '%s'" % ticks_str, ERROR_COLOR)
|
||||||
|
return
|
||||||
|
duration_ticks = int(ticks_str)
|
||||||
|
|
||||||
|
if not magnitude_str.is_valid_float():
|
||||||
|
_append_text("econ inject: invalid magnitude '%s'" % magnitude_str, ERROR_COLOR)
|
||||||
|
return
|
||||||
|
var magnitude: float = float(magnitude_str)
|
||||||
|
|
||||||
|
var payload := {
|
||||||
|
"system_id": system_id,
|
||||||
|
"effect": effect,
|
||||||
|
"magnitude": magnitude,
|
||||||
|
}
|
||||||
|
if not commodity_id.is_empty():
|
||||||
|
payload["commodity_id"] = commodity_id
|
||||||
|
if duration_ticks > 0:
|
||||||
|
payload["duration_ticks"] = duration_ticks
|
||||||
|
|
||||||
|
_append_text(
|
||||||
|
(
|
||||||
|
"injecting %s on %s%s (mag=%.2f, ticks=%d)"
|
||||||
|
% [
|
||||||
|
effect,
|
||||||
|
system_id,
|
||||||
|
" / " + commodity_id if not commodity_id.is_empty() else "",
|
||||||
|
magnitude,
|
||||||
|
duration_ticks
|
||||||
|
]
|
||||||
|
),
|
||||||
|
TEXT_COLOR,
|
||||||
|
)
|
||||||
|
_send_debug({"InjectEconEvent": payload})
|
||||||
|
|
||||||
|
|
||||||
|
# econ param <alpha|beta|friction> <value> [system_a] [system_b]
|
||||||
|
func _econ_param(parts: Array) -> void:
|
||||||
|
var args := parts.slice(2)
|
||||||
|
if args.size() < 2:
|
||||||
|
_append_text(
|
||||||
|
"usage: econ param <alpha|beta|friction> <value> [system_a] [system_b]",
|
||||||
|
ERROR_COLOR,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
var param_name: String = args[0].to_lower()
|
||||||
|
if not param_name in ["alpha", "beta", "friction"]:
|
||||||
|
_append_text(
|
||||||
|
"econ param: must be alpha, beta, or friction — got '%s'" % param_name, ERROR_COLOR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not args[1].is_valid_float():
|
||||||
|
_append_text("econ param: invalid value '%s'" % args[1], ERROR_COLOR)
|
||||||
|
return
|
||||||
|
var value: float = float(args[1])
|
||||||
|
|
||||||
|
var payload := {"param": param_name, "value": value}
|
||||||
|
if args.size() >= 3:
|
||||||
|
payload["system_a"] = args[2]
|
||||||
|
if args.size() >= 4:
|
||||||
|
payload["system_b"] = args[3]
|
||||||
|
|
||||||
|
var scope := "global"
|
||||||
|
if payload.has("system_a") and payload.has("system_b"):
|
||||||
|
scope = "%s ↔ %s" % [payload["system_a"], payload["system_b"]]
|
||||||
|
elif payload.has("system_a"):
|
||||||
|
scope = payload["system_a"]
|
||||||
|
|
||||||
|
_append_text("setting %s = %.4f (%s)" % [param_name, value, scope], TEXT_COLOR)
|
||||||
|
_send_debug({"SetEconParam": payload})
|
||||||
|
|
||||||
|
|
||||||
|
# econ inspect <system_id> — request all 7 D-181 signals for a system
|
||||||
|
func _econ_inspect(parts: Array) -> void:
|
||||||
|
if parts.size() < 3:
|
||||||
|
_append_text("usage: econ inspect <system_id>", ERROR_COLOR)
|
||||||
|
return
|
||||||
|
var system_id: String = parts[2]
|
||||||
|
_append_text("inspecting economy: %s" % system_id, TEXT_COLOR)
|
||||||
|
_send_debug({"GetEconState": system_id})
|
||||||
|
|
||||||
|
|
||||||
func _send_debug(kind: Variant) -> void:
|
func _send_debug(kind: Variant) -> void:
|
||||||
var err := (
|
var err := (
|
||||||
SimBridge
|
SimBridge
|
||||||
@@ -284,6 +452,14 @@ func _print_help() -> void:
|
|||||||
+ " triangles — list all triangles\n"
|
+ " triangles — list all triangles\n"
|
||||||
+ " pop — list active NPCs\n"
|
+ " pop — list active NPCs\n"
|
||||||
+ " status — contamination status\n"
|
+ " status — contamination status\n"
|
||||||
|
+ "\n"
|
||||||
|
+ "Economics (D-178/D-180/D-181):\n"
|
||||||
|
+ " econ inject <sys> [commodity] <shock|boost> <mag> [ticks]\n"
|
||||||
|
+ " — fire an EconEvent at a system\n"
|
||||||
|
+ " econ param <alpha|beta|friction> <val> [sys_a] [sys_b]\n"
|
||||||
|
+ " — set tâtonnement parameter\n"
|
||||||
|
+ " econ inspect <sys> — show all 7 signals for a system\n"
|
||||||
|
+ "\n"
|
||||||
+ " help — this list"
|
+ " help — this list"
|
||||||
),
|
),
|
||||||
TEXT_COLOR
|
TEXT_COLOR
|
||||||
|
|||||||
+7
-1
@@ -1,7 +1,8 @@
|
|||||||
[gd_scene load_steps=2 format=3 uid="uid://cq1y5w3hmxr8b"]
|
[gd_scene load_steps=3 format=3 uid="uid://cq1y5w3hmxr8b"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://ui/hud.gd" id="1_hud"]
|
[ext_resource type="Script" path="res://ui/hud.gd" id="1_hud"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/star_map.tscn" id="2_starmap"]
|
[ext_resource type="PackedScene" path="res://ui/star_map.tscn" id="2_starmap"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/implant/economics_panel.tscn" id="3_econ"]
|
||||||
|
|
||||||
[node name="HUD" type="Control"]
|
[node name="HUD" type="Control"]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
@@ -19,3 +20,8 @@ script = ExtResource("1_hud")
|
|||||||
[node name="StarMap" parent="." instance=ExtResource("2_starmap")]
|
[node name="StarMap" parent="." instance=ExtResource("2_starmap")]
|
||||||
visible = false
|
visible = false
|
||||||
|
|
||||||
|
; #824: Economics Monitor — implant/economics INSERT panel. Toggled via E key from main.gd.
|
||||||
|
; Composes ImplantPanel from the D-169 component library. Placeholder data until #822 ships.
|
||||||
|
[node name="EconomicsPanel" parent="." instance=ExtResource("3_econ")]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
class_name EconomicsPanel
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## Economics Monitor — implant insert panel (#824, D-170, D-181).
|
||||||
|
##
|
||||||
|
## Displays price data and GDP for a selected system. Receives economy_snapshot
|
||||||
|
## from the server via snapshot_handler → GameState → snapshot_consumers pipeline.
|
||||||
|
##
|
||||||
|
## Data architecture:
|
||||||
|
## - Ring buffer: last 20 ticks of economy data per system (for trend display)
|
||||||
|
## - 7 D-181 signals per system: price_current, price_trend, trade_flow_volume,
|
||||||
|
## corporate_presence, stockpile_weeks, production_vs_baseline, official_coverage_ratio
|
||||||
|
## - Signals 1-2 (price_current, price_trend) are Phase 2 deliverables
|
||||||
|
## - Signals 3-7 are parsed and stored but not yet displayed (Phase 3)
|
||||||
|
##
|
||||||
|
## Visual layer: ImplantPanel composition built in _ready() from component library (D-169).
|
||||||
|
## System selector uses LEFT/RIGHT arrow keys to cycle through all 301 systems.
|
||||||
|
## Placeholder commodity prices shown until #822 ships.
|
||||||
|
|
||||||
|
## Emitted when new economy data arrives for the selected system.
|
||||||
|
signal economy_data_updated(system_id: String, data: Dictionary)
|
||||||
|
|
||||||
|
const APP_PATH := "implant/economics"
|
||||||
|
const RING_BUFFER_SIZE: int = 20
|
||||||
|
const STAR_MAP_DATA := "res://data/star_map_data.json"
|
||||||
|
const PANEL_WIDTH: float = 340.0
|
||||||
|
const PANEL_MARGIN: float = 16.0
|
||||||
|
|
||||||
|
# Placeholder commodity rows shown until server ships EconomySnapshot (#822).
|
||||||
|
# Commodity IDs match D-184 catalog.
|
||||||
|
const PLACEHOLDER_COMMODITIES: Array[Dictionary] = [
|
||||||
|
{"id": "fusion_fuel", "name": "FUSION FUEL", "price": 142, "trend": 1},
|
||||||
|
{"id": "basic_goods", "name": "BASIC GOODS", "price": 58, "trend": 0},
|
||||||
|
{"id": "machinery", "name": "MACHINERY", "price": 890, "trend": -1},
|
||||||
|
{"id": "organics", "name": "ORGANICS", "price": 34, "trend": 1},
|
||||||
|
{"id": "lattice_comp", "name": "LATTICE COMP.", "price": 2240, "trend": 0},
|
||||||
|
{"id": "pharmaceuticals", "name": "PHARMA", "price": 312, "trend": -1},
|
||||||
|
]
|
||||||
|
|
||||||
|
## Currently selected system for detailed display. Empty = no selection.
|
||||||
|
var selected_system: String = ""
|
||||||
|
|
||||||
|
## Ring buffer: system_id → Array[Dictionary] (most recent last, max RING_BUFFER_SIZE).
|
||||||
|
## Each entry is one tick's worth of D-181 signals for that system.
|
||||||
|
var _history: Dictionary = {}
|
||||||
|
|
||||||
|
var _insert_active: bool = true
|
||||||
|
|
||||||
|
# Visual panel state (D-169 component library)
|
||||||
|
var _panel: ImplantPanel = null # root container
|
||||||
|
var _implant_theme: ImplantTheme = null
|
||||||
|
var _header: ImplantHeader = null # kept for set_content() on system change
|
||||||
|
var _nav_row: ImplantDataRow = null # system selector nav hint
|
||||||
|
var _gdp_row: ImplantDataRow = null # GDP value row
|
||||||
|
var _commodity_rows: Array = [] # ImplantDataRow × 6, updated without full rebuild
|
||||||
|
var _placeholder_notice: ImplantTextBlock = null # hidden once live data arrives
|
||||||
|
|
||||||
|
# System list for the selector (populated from STAR_MAP_DATA)
|
||||||
|
var _systems: Array = [] # Array[Dictionary], sorted by proper_name
|
||||||
|
var _selected_idx: int = 0 # index into _systems
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||||
|
grow_vertical = Control.GROW_DIRECTION_BOTH
|
||||||
|
visible = false
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
|
||||||
|
# D-170: Register with HUD layer groups
|
||||||
|
HudGroups.register(self, APP_PATH)
|
||||||
|
HudGroups.app_changed.connect(_on_app_changed)
|
||||||
|
|
||||||
|
_implant_theme = load("res://ui/implant/default_implant.tres") as ImplantTheme
|
||||||
|
_load_system_list()
|
||||||
|
_build_panel()
|
||||||
|
economy_data_updated.connect(_on_economy_data_updated)
|
||||||
|
|
||||||
|
|
||||||
|
## Called from SnapshotConsumers when economy_snapshot arrives in GameState.
|
||||||
|
## data: Dictionary keyed by system_id → signal payload (D-181).
|
||||||
|
func receive_economy_data(data: Dictionary) -> void:
|
||||||
|
for system_id: String in data:
|
||||||
|
var signals: Variant = data[system_id]
|
||||||
|
if not signals is Dictionary:
|
||||||
|
continue
|
||||||
|
if not _history.has(system_id):
|
||||||
|
_history[system_id] = []
|
||||||
|
var buf: Array = _history[system_id]
|
||||||
|
buf.append(signals)
|
||||||
|
if buf.size() > RING_BUFFER_SIZE:
|
||||||
|
_history[system_id] = buf.slice(buf.size() - RING_BUFFER_SIZE)
|
||||||
|
|
||||||
|
# Notify listeners if the selected system received new data
|
||||||
|
if not selected_system.is_empty() and data.has(selected_system):
|
||||||
|
economy_data_updated.emit(selected_system, data[selected_system])
|
||||||
|
|
||||||
|
|
||||||
|
## Select a system for detailed display. Emits economy_data_updated if history exists.
|
||||||
|
func select_system(system_id: String) -> void:
|
||||||
|
selected_system = system_id
|
||||||
|
if not selected_system.is_empty() and _history.has(selected_system):
|
||||||
|
var buf: Array = _history[selected_system]
|
||||||
|
if buf.size() > 0:
|
||||||
|
economy_data_updated.emit(selected_system, buf[buf.size() - 1])
|
||||||
|
|
||||||
|
|
||||||
|
## Get the full ring buffer for a system (for chart/sparkline rendering).
|
||||||
|
## Returns empty array if no history exists.
|
||||||
|
func get_history(system_id: String) -> Array:
|
||||||
|
return _history.get(system_id, [])
|
||||||
|
|
||||||
|
|
||||||
|
## Get the latest tick's signals for a system, or empty dict.
|
||||||
|
func get_latest(system_id: String) -> Dictionary:
|
||||||
|
var buf: Array = _history.get(system_id, [])
|
||||||
|
if buf.size() > 0:
|
||||||
|
return buf[buf.size() - 1]
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
## Get all system IDs that have received at least one tick of data.
|
||||||
|
func get_known_systems() -> Array:
|
||||||
|
return _history.keys()
|
||||||
|
|
||||||
|
|
||||||
|
## Toggle via HUD layer system (D-170). INSERT mode — shares screen with gameplay.
|
||||||
|
func toggle_visible() -> void:
|
||||||
|
HudGroups.toggle_app(APP_PATH, HudGroups.Mode.INSERT)
|
||||||
|
|
||||||
|
|
||||||
|
## Called from main.gd when insert state changes (D-170).
|
||||||
|
func set_insert_active(active: bool) -> void:
|
||||||
|
_insert_active = active
|
||||||
|
if not active and HudGroups.is_app_active(APP_PATH):
|
||||||
|
HudGroups.close_app()
|
||||||
|
|
||||||
|
|
||||||
|
## Respond to app layer changes (D-170).
|
||||||
|
func _on_app_changed(app_path: String, mode: int) -> void:
|
||||||
|
if app_path != APP_PATH:
|
||||||
|
return
|
||||||
|
if mode == HudGroups.Mode.FULLSCREEN or mode == HudGroups.Mode.INSERT:
|
||||||
|
visible = true
|
||||||
|
else:
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# System list — populated from star_map_data.json
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
func _load_system_list() -> void:
|
||||||
|
if not FileAccess.file_exists(STAR_MAP_DATA):
|
||||||
|
push_warning("EconomicsPanel: %s not found" % STAR_MAP_DATA)
|
||||||
|
return
|
||||||
|
var file := FileAccess.open(STAR_MAP_DATA, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
return
|
||||||
|
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||||
|
file.close()
|
||||||
|
if not (parsed is Dictionary):
|
||||||
|
return
|
||||||
|
for node: Dictionary in parsed.get("nodes", []):
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if not _systems.is_empty():
|
||||||
|
selected_system = _systems[0].get("system_id", "")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Visual panel — D-169 ImplantPanel composition
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
func _build_panel() -> void:
|
||||||
|
_panel = ImplantPanel.new()
|
||||||
|
_panel.name = "EconPanel"
|
||||||
|
_panel.theme_resource = _implant_theme
|
||||||
|
_panel.custom_minimum_size.x = PANEL_WIDTH
|
||||||
|
_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_panel.position = Vector2(PANEL_MARGIN, PANEL_MARGIN)
|
||||||
|
add_child(_panel)
|
||||||
|
_rebuild_panel()
|
||||||
|
|
||||||
|
|
||||||
|
## Full rebuild of panel components. Called on system change and initial build.
|
||||||
|
func _rebuild_panel() -> void:
|
||||||
|
if not _panel:
|
||||||
|
return
|
||||||
|
_panel.clear()
|
||||||
|
_commodity_rows.clear()
|
||||||
|
|
||||||
|
var node: Dictionary = _current_node()
|
||||||
|
var sys_name: String = node.get("proper_name", node.get("system_id", "—"))
|
||||||
|
var sys_id: String = node.get("system_id", "")
|
||||||
|
var total: int = _systems.size()
|
||||||
|
|
||||||
|
# ── Header ────────────────────────────────────────────────────────────────
|
||||||
|
_header = ImplantHeader.new("ECONOMICS MONITOR", sys_name)
|
||||||
|
_panel.add_component(_header)
|
||||||
|
|
||||||
|
_panel.add_component(ImplantSeparator.new())
|
||||||
|
|
||||||
|
# ── System selector nav ────────────────────────────────────────────────────
|
||||||
|
var nav_hint := "◄ ► · %s [%d / %d]" % [sys_id, _selected_idx + 1, total]
|
||||||
|
_nav_row = ImplantDataRow.new(nav_hint)
|
||||||
|
_panel.add_component(_nav_row)
|
||||||
|
|
||||||
|
_panel.add_component(ImplantSeparator.new())
|
||||||
|
|
||||||
|
# ── GDP strip ─────────────────────────────────────────────────────────────
|
||||||
|
var gdp_str: String = node.get("gdp", "—")
|
||||||
|
_gdp_row = ImplantDataRow.new("gdp " + gdp_str)
|
||||||
|
_panel.add_component(_gdp_row)
|
||||||
|
|
||||||
|
_panel.add_component(ImplantSeparator.new())
|
||||||
|
|
||||||
|
# ── Price table ───────────────────────────────────────────────────────────
|
||||||
|
_panel.add_component(ImplantTextBlock.new("MARKET PRICES"))
|
||||||
|
|
||||||
|
var latest: Dictionary = get_latest(sys_id)
|
||||||
|
var commodity_signals: Array = latest.get("price_current", [])
|
||||||
|
|
||||||
|
for c: Dictionary in PLACEHOLDER_COMMODITIES:
|
||||||
|
var cid: String = c.get("id", "")
|
||||||
|
var price: int = c.get("price", 0)
|
||||||
|
var trend: int = c.get("trend", 0)
|
||||||
|
|
||||||
|
# Overlay live data when available (D-181 signal 1-2)
|
||||||
|
for sig: Dictionary in commodity_signals:
|
||||||
|
if sig.get("commodity_id", "") == cid:
|
||||||
|
price = int(sig.get("price_current", price))
|
||||||
|
trend = int(sig.get("price_trend", trend))
|
||||||
|
break
|
||||||
|
|
||||||
|
var row_text: String = "%-14s %5d %s" % [c["name"], price, _trend_glyph(trend)]
|
||||||
|
var row := ImplantDataRow.new(row_text)
|
||||||
|
_panel.add_component(row)
|
||||||
|
_commodity_rows.append(row)
|
||||||
|
|
||||||
|
# ── Placeholder notice ────────────────────────────────────────────────────
|
||||||
|
_panel.add_component(ImplantSeparator.new())
|
||||||
|
var notice_text: String = (
|
||||||
|
"[LIVE MARKET — #822 PENDING]" if _history.is_empty() else "LIVE DATA ACTIVE"
|
||||||
|
)
|
||||||
|
_placeholder_notice = ImplantTextBlock.new(notice_text)
|
||||||
|
_panel.add_component(_placeholder_notice)
|
||||||
|
|
||||||
|
_panel.add_component(ImplantSeparator.new())
|
||||||
|
_panel.add_component(ImplantTextBlock.new("[ ] select system · N close"))
|
||||||
|
|
||||||
|
|
||||||
|
func _current_node() -> Dictionary:
|
||||||
|
if _systems.is_empty():
|
||||||
|
return {}
|
||||||
|
_selected_idx = clampi(_selected_idx, 0, _systems.size() - 1)
|
||||||
|
return _systems[_selected_idx]
|
||||||
|
|
||||||
|
|
||||||
|
func _trend_glyph(trend: int) -> String:
|
||||||
|
if trend > 0:
|
||||||
|
return "▲"
|
||||||
|
if trend < 0:
|
||||||
|
return "▼"
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
|
||||||
|
## Respond to economy_data_updated signal — refresh the price table in-place.
|
||||||
|
func _on_economy_data_updated(system_id: String, data: Dictionary) -> void:
|
||||||
|
if not _panel or _commodity_rows.is_empty():
|
||||||
|
return
|
||||||
|
if system_id != selected_system:
|
||||||
|
return
|
||||||
|
|
||||||
|
var commodity_signals: Array = data.get("price_current", [])
|
||||||
|
|
||||||
|
for i: int in range(PLACEHOLDER_COMMODITIES.size()):
|
||||||
|
if i >= _commodity_rows.size():
|
||||||
|
break
|
||||||
|
var c: Dictionary = PLACEHOLDER_COMMODITIES[i]
|
||||||
|
var cid: String = c.get("id", "")
|
||||||
|
var price: int = c.get("price", 0)
|
||||||
|
var trend: int = c.get("trend", 0)
|
||||||
|
|
||||||
|
for sig: Dictionary in commodity_signals:
|
||||||
|
if sig.get("commodity_id", "") == cid:
|
||||||
|
price = int(sig.get("price_current", price))
|
||||||
|
trend = int(sig.get("price_trend", trend))
|
||||||
|
break
|
||||||
|
|
||||||
|
var row_text: String = "%-14s %5d %s" % [c["name"], price, _trend_glyph(trend)]
|
||||||
|
_commodity_rows[i].text = row_text
|
||||||
|
|
||||||
|
if _placeholder_notice and not _history.is_empty():
|
||||||
|
_placeholder_notice.text = "LIVE DATA ACTIVE"
|
||||||
|
|
||||||
|
|
||||||
|
## 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 + delta, 0, _systems.size())
|
||||||
|
selected_system = _systems[_selected_idx].get("system_id", "")
|
||||||
|
_rebuild_panel()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/implant/economics_panel.gd" id="1_econ"]
|
||||||
|
|
||||||
|
; #824: Economics Monitor insert panel — price data and GDP for selected system.
|
||||||
|
; Composed from ImplantPanel component library (D-169). Registered under implant/economics (D-170).
|
||||||
|
; Toggle with E key in implant mode. Data flows from EconomySnapshot via snapshot_consumers.
|
||||||
|
; Placeholder commodity prices shown until server ticket #822 ships.
|
||||||
|
|
||||||
|
[node name="EconomicsPanel" 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_econ")
|
||||||
@@ -135,7 +135,6 @@ func _process(_delta: float) -> void:
|
|||||||
_dirty = false
|
_dirty = false
|
||||||
|
|
||||||
|
|
||||||
## Called from main.gd when insert state changes.
|
|
||||||
## Called from main.gd when insert state changes.
|
## Called from main.gd when insert state changes.
|
||||||
func set_insert_active(active: bool) -> void:
|
func set_insert_active(active: bool) -> void:
|
||||||
_insert_active = active
|
_insert_active = active
|
||||||
@@ -507,10 +506,13 @@ func _rebuild_info_panel() -> void:
|
|||||||
|
|
||||||
# Population + GDP
|
# Population + GDP
|
||||||
var population: String = node.get("population", "")
|
var population: String = node.get("population", "")
|
||||||
if not population.is_empty():
|
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
|
_info_panel.add_component(ImplantDataRow.new("")) # blank line spacer
|
||||||
_info_panel.add_component(ImplantDataRow.new("pop " + population))
|
if not population.is_empty():
|
||||||
_info_panel.add_component(ImplantDataRow.new("GDP —"))
|
_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 ─────────────────────────────────────────────────────────
|
# ── GTTR excerpt ─────────────────────────────────────────────────────────
|
||||||
var gttr: String = node.get("gttr_excerpt", "")
|
var gttr: String = node.get("gttr_excerpt", "")
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ Run from any directory — paths are resolved relative to this script's location
|
|||||||
|
|
||||||
Sources:
|
Sources:
|
||||||
docs/design/star-map.json — graph topology (nodes + edges)
|
docs/design/star-map.json — graph topology (nodes + edges)
|
||||||
server/server/data/systems.db — proper names, geographic sectors
|
server/data/systems.db — proper names, geographic sectors, bodies, GDP tier
|
||||||
wiki/star-systems/ — star type, bodies, population, GTTR excerpt
|
wiki/star-systems/ — star type, GTTR excerpt (bodies/population from systems.db)
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
client/data/star_map_data.json — self-contained client data for the star map UI
|
client/data/star_map_data.json — self-contained client data for the star map UI
|
||||||
@@ -21,16 +21,11 @@ import sqlite3
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
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.
|
# Works regardless of cwd — no fragile relative path guessing.
|
||||||
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
_PROJECT_ROOT = os.path.dirname(_SCRIPT_DIR)
|
_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")
|
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")
|
SYSTEMS_DB_PATH = os.path.join(_PROJECT_ROOT, "server", "data", "systems.db")
|
||||||
WIKI_PATH = os.path.join(_PROJECT_ROOT, "wiki", "star-systems")
|
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:
|
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)
|
slug = system_id_to_wiki_slug(system_id)
|
||||||
path = os.path.join(WIKI_PATH, slug, "index.md")
|
path = os.path.join(WIKI_PATH, slug, "index.md")
|
||||||
result = {"star_type": "", "bodies": "", "population": ""}
|
result = {"star_type": ""}
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return result
|
return result
|
||||||
with open(path, encoding="utf-8") as f:
|
with open(path, encoding="utf-8") as f:
|
||||||
@@ -60,18 +56,7 @@ def parse_wiki_index(system_id: str) -> dict:
|
|||||||
if m:
|
if m:
|
||||||
raw = m.group(1).strip()
|
raw = m.group(1).strip()
|
||||||
# Extract spectral class — everything before " ·" or end of string
|
# Extract spectral class — everything before " ·" or end of string
|
||||||
star_type = raw.split("·")[0].strip()
|
result["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
|
return result
|
||||||
|
|
||||||
@@ -106,6 +91,45 @@ def parse_gttr_excerpt(system_id: str) -> str:
|
|||||||
return " ".join(paragraph_lines)
|
return " ".join(paragraph_lines)
|
||||||
|
|
||||||
|
|
||||||
|
GDP_PER_CAPITA: dict = {
|
||||||
|
5: 75_000,
|
||||||
|
4: 40_000,
|
||||||
|
3: 15_000,
|
||||||
|
2: 5_000,
|
||||||
|
1: 2_000,
|
||||||
|
0: 500,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def infer_tier_from_population(pop: int) -> int:
|
||||||
|
"""Infer an economic tier from total population when no explicit tier is set."""
|
||||||
|
if pop >= 5_000_000_000:
|
||||||
|
return 5
|
||||||
|
if pop >= 1_000_000_000:
|
||||||
|
return 4
|
||||||
|
if pop >= 200_000_000:
|
||||||
|
return 3
|
||||||
|
if pop >= 10_000_000:
|
||||||
|
return 2
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def compute_gdp(total_pop: int, economic_tier: int | None) -> str:
|
||||||
|
"""Return a formatted GDP string in Tractus, or empty string if no population."""
|
||||||
|
if total_pop == 0:
|
||||||
|
return ""
|
||||||
|
tier = economic_tier if economic_tier is not None else infer_tier_from_population(total_pop)
|
||||||
|
per_cap = GDP_PER_CAPITA.get(tier, GDP_PER_CAPITA[1])
|
||||||
|
value = total_pop * per_cap
|
||||||
|
if value < 1_000_000_000:
|
||||||
|
return f"{value / 1_000_000:.1f} MTr"
|
||||||
|
if value < 1_000_000_000_000:
|
||||||
|
return f"{value / 1_000_000_000:.1f} BTr"
|
||||||
|
if value < 1_000_000_000_000_000:
|
||||||
|
return f"{value / 1_000_000_000_000:.1f} TTr"
|
||||||
|
return f"{value / 1_000_000_000_000_000:.1f} QTr"
|
||||||
|
|
||||||
|
|
||||||
def build_adjacency(edges: list) -> dict:
|
def build_adjacency(edges: list) -> dict:
|
||||||
"""Build a map from system_id to list of adjacent system_ids from edges."""
|
"""Build a map from system_id to list of adjacent system_ids from edges."""
|
||||||
adj: dict = {}
|
adj: dict = {}
|
||||||
@@ -134,35 +158,45 @@ def generate() -> dict:
|
|||||||
with open(STAR_MAP_PATH) as f:
|
with open(STAR_MAP_PATH) as f:
|
||||||
star_map = json.load(f)
|
star_map = json.load(f)
|
||||||
|
|
||||||
conn = sqlite3.connect(SYSTEMS_DB_PATH)
|
try:
|
||||||
conn.row_factory = sqlite3.Row
|
conn = sqlite3.connect(SYSTEMS_DB_PATH)
|
||||||
cur = conn.cursor()
|
conn.row_factory = sqlite3.Row
|
||||||
cur.execute(
|
cur = conn.cursor()
|
||||||
"SELECT system_id, proper_name, geographic_sector, geographic_band "
|
|
||||||
"FROM star_systems"
|
|
||||||
)
|
|
||||||
db_lookup = {row["system_id"]: dict(row) for row in cur.fetchall()}
|
|
||||||
|
|
||||||
# Aggregate body data per system from the bodies table
|
cur.execute(
|
||||||
cur.execute("""
|
"SELECT system_id, proper_name, geographic_sector, geographic_band "
|
||||||
SELECT system_id,
|
"FROM star_systems"
|
||||||
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,
|
db_lookup = {row["system_id"]: dict(row) for row in cur.fetchall()}
|
||||||
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()}
|
|
||||||
|
|
||||||
# Also sum station populations
|
# Aggregate body data per system from the bodies table
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT system_id,
|
SELECT system_id,
|
||||||
SUM(COALESCE(population, 0)) AS station_pop
|
SUM(CASE WHEN atmosphere IN ('breathable','standard') AND body_type IN ('planet','moon') THEN 1 ELSE 0 END) AS habitable,
|
||||||
FROM stations
|
SUM(CASE WHEN inhabited = 1 THEN 1 ELSE 0 END) AS inhabited,
|
||||||
GROUP BY system_id
|
SUM(CASE WHEN inhabited = 1 THEN COALESCE(population, 0) ELSE 0 END) AS total_pop
|
||||||
""")
|
FROM bodies
|
||||||
station_stats = {row["system_id"]: dict(row) for row in cur.fetchall()}
|
GROUP BY system_id
|
||||||
conn.close()
|
""")
|
||||||
|
body_stats = {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()}
|
||||||
|
|
||||||
|
# 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"])
|
adjacency = build_adjacency(star_map["edges"])
|
||||||
|
|
||||||
@@ -204,6 +238,9 @@ def generate() -> dict:
|
|||||||
|
|
||||||
entry["bodies"] = "%d habitable · %d inhabited" % (hab, inh)
|
entry["bodies"] = "%d habitable · %d inhabited" % (hab, inh)
|
||||||
entry["population"] = "{:,}".format(total_pop)
|
entry["population"] = "{:,}".format(total_pop)
|
||||||
|
gdp_str = compute_gdp(total_pop, econ_tiers.get(sid))
|
||||||
|
if gdp_str:
|
||||||
|
entry["gdp"] = gdp_str
|
||||||
if gttr:
|
if gttr:
|
||||||
entry["gttr_excerpt"] = gttr
|
entry["gttr_excerpt"] = gttr
|
||||||
if n.get("is_gateway"):
|
if n.get("is_gateway"):
|
||||||
|
|||||||
Reference in New Issue
Block a user