Compare commits
5
Commits
v0.1.33
...
532fcd64a5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
532fcd64a5 | ||
|
|
f17a6c638c | ||
|
|
949d721eac | ||
|
|
766da969de | ||
|
|
9d9ea96be1 |
@@ -6,6 +6,11 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- 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
|
||||
|
||||
### Added
|
||||
|
||||
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).
|
||||
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)
|
||||
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/wiki/gttr" — Drifter's Guide reader
|
||||
## "implant/journal" — knowledge journal
|
||||
## "implant/economics" — economics monitor (D-181, #824)
|
||||
##
|
||||
## Usage:
|
||||
## HudGroups.register(self, "implant/map/starchart")
|
||||
|
||||
@@ -9,7 +9,6 @@ var _pending_record_inputs: Array = [] # #507: accumulates server-bound inputs
|
||||
var _router: SnapshotEventRouter # #559: callable-based snapshot dispatch
|
||||
var _consumers: SnapshotConsumers # #775: non-dialogue snapshot consumers
|
||||
var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers
|
||||
|
||||
@onready var world_renderer = $World
|
||||
@onready var fog_entities = $World/FogEntities # D-059/D-060: cognitive delay fog visualization
|
||||
@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 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 economics_panel = $InsertOverlay/HUD/EconomicsPanel # #824: economics monitor (D-170)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -92,6 +92,7 @@ func _ready() -> void:
|
||||
"interaction_prompt": interaction_prompt,
|
||||
"minimap": minimap,
|
||||
"star_map": star_map,
|
||||
"economics_panel": economics_panel,
|
||||
},
|
||||
_screen_flash
|
||||
)
|
||||
@@ -154,6 +155,7 @@ func _ready() -> void:
|
||||
_router.register("dialogue_response", _dialogue.consume_dialogue_response)
|
||||
_router.register("save_result", _consumers.consume_save_result)
|
||||
_router.register("debug_response", _consumers.consume_debug_response)
|
||||
_router.register("economy_snapshot", _consumers.consume_economy_snapshot)
|
||||
|
||||
# #581: Wire settings_dialog debug console toggle
|
||||
if settings_dialog and debug_console:
|
||||
@@ -170,6 +172,10 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.keycode == KEY_M:
|
||||
if star_map:
|
||||
star_map.toggle_visible()
|
||||
elif event is InputEventKey and event.keycode == KEY_E:
|
||||
# #824: E — toggle Economics Monitor implant panel
|
||||
if economics_panel:
|
||||
economics_panel.toggle_visible()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
|
||||
@@ -17,6 +17,7 @@ var interaction_list: Node = null
|
||||
var interaction_prompt: Node = null
|
||||
var minimap: 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)
|
||||
|
||||
@@ -37,6 +38,7 @@ func init(refs: Dictionary, screen_flash: Callable) -> SnapshotConsumers:
|
||||
interaction_prompt = refs.get("interaction_prompt")
|
||||
minimap = refs.get("minimap")
|
||||
star_map = refs.get("star_map")
|
||||
economics_panel = refs.get("economics_panel")
|
||||
_screen_flash_fn = screen_flash
|
||||
return self
|
||||
|
||||
@@ -54,6 +56,8 @@ func propagate_insert_state() -> void:
|
||||
minimap.set_insert_active(insert_state)
|
||||
if star_map:
|
||||
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.
|
||||
@@ -173,6 +177,14 @@ func consume_debug_response() -> void:
|
||||
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)
|
||||
|
||||
|
||||
# #174: Consume examine result — show overlay when server sends character-filtered observation.
|
||||
func consume_examine_result() -> void:
|
||||
if GameState.current_examine_result == null or not examine_display:
|
||||
|
||||
@@ -213,6 +213,12 @@ static func apply(snapshot: Dictionary) -> void:
|
||||
else:
|
||||
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.
|
||||
if (
|
||||
snapshot.has("character_visual_descriptor")
|
||||
|
||||
@@ -225,10 +225,172 @@ func _dispatch(line: String) -> void:
|
||||
_send_debug("ListPopulation")
|
||||
"status":
|
||||
_send_debug("GetContaminationStatus")
|
||||
"econ":
|
||||
_dispatch_econ(parts)
|
||||
_:
|
||||
_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]
|
||||
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:
|
||||
var err := (
|
||||
SimBridge
|
||||
@@ -284,6 +446,14 @@ func _print_help() -> void:
|
||||
+ " triangles — list all triangles\n"
|
||||
+ " pop — list active NPCs\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"
|
||||
),
|
||||
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="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"]
|
||||
layout_mode = 3
|
||||
@@ -19,3 +20,8 @@ script = ExtResource("1_hud")
|
||||
[node name="StarMap" parent="." instance=ExtResource("2_starmap")]
|
||||
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,330 @@
|
||||
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 · E 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"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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:
|
||||
if _systems.is_empty():
|
||||
return
|
||||
_selected_idx = wrapi(_selected_idx + direction, 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")
|
||||
@@ -507,10 +507,13 @@ func _rebuild_info_panel() -> void:
|
||||
|
||||
# Population + GDP
|
||||
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("pop " + population))
|
||||
_info_panel.add_component(ImplantDataRow.new("GDP —"))
|
||||
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", "")
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,92 @@
|
||||
# Sprint 34: Pulse — Client Tasks
|
||||
|
||||
**Goal:** Close Phase 2 — wire the economics simulation into the live game, expose price history and trade flows in the implant, and make the economy observable and tweakable at runtime.
|
||||
|
||||
**Branch:** `sprint-34/client`
|
||||
**Agents:** Stig (UI), Tyre (architecture)
|
||||
|
||||
## New Tickets
|
||||
|
||||
| # | Title | Blocked by |
|
||||
|---|-------|------------|
|
||||
| #785 | Add system population and GDP to star map info panel | — |
|
||||
| #824 | Economics insert panel — price history charts and GDP display | #822 (server) |
|
||||
| #825 | Economics debug console commands — event triggers and param sliders | #823 (server) |
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- `decisions/economics.md` — D-181 (7-signal vocabulary — signals 1-2 are Phase 2: price_current, price_trend), D-180 (event port — the commands #825 fires)
|
||||
- `decisions/architecture.md` — D-169 (implant UI component library — compose from client/ui/implant/), D-170 (HUD visibility groups — economics panel lives in INSERT mode), D-020 (IPC — EconomySnapshot arrives in ObserverSnapshot)
|
||||
|
||||
## Notes
|
||||
|
||||
### #785 — System population and GDP to star map info panel
|
||||
|
||||
When a system is selected in the star map (`client/ui/star_map.gd`, `client/ui/star_map.tscn`), the popup built with ImplantPanel components shows: name, star type, hop distance, corridor, GTTR excerpt, bodies, adjacents. Add two new `ImplantDataRow` entries: `POPULATION` and `GDP`. Data is already in `res://data/star_map_data.json` (regenerated by `tooling/generate-star-map-data.py` from `systems.db`). Check whether population and GDP fields are present in the JSON; if not, update the generation script as part of this ticket. This is standalone — no server dependency. Good warmup ticket; complete it first.
|
||||
|
||||
### #824 — Economics insert panel
|
||||
|
||||
New implant panel: **Economics Monitor**. Lives in INSERT mode (D-170), accessible via implant navigation alongside the star map.
|
||||
|
||||
Scene: `client/ui/implant/economics_panel.tscn` + `client/ui/implant/economics_panel.gd`
|
||||
|
||||
Compose strictly from the existing component library (`client/ui/implant/`):
|
||||
- `ImplantPanel` — root container
|
||||
- `ImplantHeader` — "ECONOMICS MONITOR" title + selected system subtitle
|
||||
- `ImplantSeparator` — section dividers
|
||||
- `ImplantDataRow` — key/value rows for price and GDP data
|
||||
- `ImplantTextBlock` — top commodity summary text
|
||||
|
||||
Layout (three sections):
|
||||
1. **System selector** — searchable/scrollable list of systems (can reuse star map system data). Selecting a system triggers an `EconStateQuery` PlayerAction to the server.
|
||||
2. **Price table** — top 6 commodities for selected system, each as an `ImplantDataRow` with `price_current` and a directional trend indicator (▲ / ▼ / —) derived from `price_trend`.
|
||||
3. **GDP strip** — total economic activity for the system displayed as a single row. Update each time a new `EconomySnapshot` arrives.
|
||||
|
||||
Data flow: `snapshot_handler.gd` receives ObserverSnapshot v21. When `economy_snapshot` is present, forward to `economics_panel.gd` via a signal or direct call. The panel caches the last 20 ticks of price data per system for trend display (ring buffer in GDScript Dictionary).
|
||||
|
||||
Do NOT draw custom canvas sparklines unless time allows — `ImplantDataRow` with a trend arrow is the MVP. The price chart can be a follow-on.
|
||||
|
||||
Register the panel in `client/scripts/autoloads/hud_groups.gd` under path `implant/economics`. Add a keyboard shortcut (e.g. `E` in implant mode) and an entry in the implant navigation menu.
|
||||
|
||||
Blocked by #822 (server must expose EconomySnapshot before the panel has real data). Build the panel with placeholder data first; wire live data once #822 ships.
|
||||
|
||||
### #825 — Economics debug console commands
|
||||
|
||||
The debug console exists at `client/ui/debug_console.gd` + `client/ui/debug_console.tscn`. The console already dispatches `DebugCommandKind` variants via `PlayerAction::DebugCommand` through the IPC bridge.
|
||||
|
||||
Add three new command parsers in `_parse_command()` / `_dispatch_command()`:
|
||||
|
||||
```
|
||||
econ inject <system_id> [commodity_id] <shock|boost> <magnitude> [ticks]
|
||||
→ InjectEconEvent { system_id, commodity_id, effect, magnitude, duration_ticks }
|
||||
|
||||
econ param <alpha|beta|friction> <value> [system_a] [system_b]
|
||||
→ SetEconParam { param, value }
|
||||
|
||||
econ inspect <system_id>
|
||||
→ GetEconState { system_id }
|
||||
```
|
||||
|
||||
`econ inspect` returns all 7 D-181 signals for the system; display in console output log as a multi-line block. `econ inject` and `econ param` print a confirmation + the server's `DebugResponsePayload.text`.
|
||||
|
||||
Update `_print_help()` to include the `econ` command family. Blocked by #823 (server must handle the variants before the client can send them meaningfully, though client-side parsing can be built in parallel).
|
||||
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
#785 (star map GDP) — standalone, start here
|
||||
|
||||
#822 (server IPC, Sprint 34/server) → #824 (economics insert panel)
|
||||
#823 (server debug handler, Sprint 34/server) → #825 (debug console commands)
|
||||
|
||||
#824 and #825 are parallel after their respective server blockers clear.
|
||||
```
|
||||
|
||||
## PR Workflow
|
||||
|
||||
```bash
|
||||
tea pr create --repo jpmschweitzer/settled-reach --login schweitz \
|
||||
--title "feat(ui): economics insert panel and debug console econ commands" \
|
||||
--description "Sprint 34 client work" \
|
||||
--base main --head sprint-34/client
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# Sprint 34: Pulse — Copy Tasks
|
||||
|
||||
**Goal:** Close Phase 2 — wire the economics simulation into the live game, expose price history and trade flows in the implant, and make the economy observable and tweakable at runtime.
|
||||
|
||||
**Branch:** `sprint-34/copy`
|
||||
**Agents:** Mellanie (author), Paula (narrative)
|
||||
|
||||
## New Tickets
|
||||
|
||||
| # | Title | Blocked by |
|
||||
|---|-------|------------|
|
||||
| #814 | Rail infrastructure corporation gap | — |
|
||||
| #695 | Author overheard conversations for remaining 24 zone types | — |
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- `decisions/economics.md` — D-182 (TOML source of truth — all economics content lives in `wiki/economics/`), D-175 (corporation taxonomy — Tier 1/2/3 structure)
|
||||
- `decisions/content.md` — D-142 (zone-type template architecture — 31 zone types defined), D-139 (composable behavior primitives — overheard conversations are Layer 2 cultural flavor)
|
||||
|
||||
## Notes
|
||||
|
||||
### #814 — Rail infrastructure corporation gap
|
||||
|
||||
**Context:** The corporation validation pipeline (`tooling/economy-db/import_economics.py`) flagged a gap: no existing wiki corporation produces the `rail_infrastructure` commodity. This is a lore-world gap as much as a data gap — rail is the primary intra-continental transit system on inhabited worlds (see `decisions/architecture.md` D-093 for Sova Transit context, `wiki/economics/production_chains.toml` for chain definitions).
|
||||
|
||||
**Deliverable:** Either (a) assign `rail_infrastructure` production to an existing Tier 1 or Tier 2 corporation (MVG — Marvian Gravity Works — is the most plausible candidate given its Tier 1 infrastructure mandate) or (b) create a new corporation if no existing corp fits. Update:
|
||||
- `wiki/economics/corporations.toml` — add production entry
|
||||
- Corresponding wiki corporation page (if new corp: `wiki/corporations/<name>.md`)
|
||||
- Verify `make economy-db` passes after the change
|
||||
|
||||
Do not assign to a Tier 3 regional corp — rail infrastructure is a systemic commodity that should have Tier 1 or Tier 2 backing.
|
||||
|
||||
### #695 — Overheard conversations for remaining 24 zone types
|
||||
|
||||
**Context:** `server/content/global/overheard.ron` currently covers 5 of 29 zone types (~17% coverage). The remaining 24 types need 2-4 role-pair conversations each. These are the passive ambient dialogue lines that play when NPCs are overheard by the player without direct engagement (D-078).
|
||||
|
||||
**Format:** Each conversation entry in `overheard.ron` follows the existing pattern — two role slugs, a setting line (terse, 1 sentence describing where/when), and 3-5 lines of dialogue. Lines should feel naturalistic for the zone type; the NPC pair should be plausible co-workers or passers-by given the zone's economic activity.
|
||||
|
||||
**Zone types to cover:** Check `server/content/global/zone-types/` for the full list. Currently covered: the 5 types already in `overheard.ron` (verify by reading the file). Write 2-4 conversations per remaining zone type. Prioritize the zone types most likely to be visited first in a playthrough: `residential_dense`, `commercial_retail`, `transit_hub`, `office_district`, `industrial_light`.
|
||||
|
||||
**Lore anchors:** Use the wiki cultural pages (`wiki/cultures/`) for voice and slang. Zone types that map to specific planetary environments (agricultural, wilderness) should reflect the relevant culture. Avoid generic SF clichés — these lines should feel like they belong in the Reach.
|
||||
|
||||
**Volume:** 24 zone types × 3 conversations average × 4 lines each = ~288 lines total. Work zone-type by zone-type; commit partial coverage. Do not block on completing all 24 before committing.
|
||||
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
#814 (rail corp gap) — standalone
|
||||
#695 (overheard conversations) — standalone, parallel
|
||||
```
|
||||
|
||||
Both tickets are independent and can run in parallel.
|
||||
|
||||
## PR Workflow
|
||||
|
||||
```bash
|
||||
tea pr create --repo jpmschweitzer/settled-reach --login schweitz \
|
||||
--title "content(economics): rail corp gap and overheard conversation coverage" \
|
||||
--description "Sprint 34 copy work" \
|
||||
--base main --head sprint-34/copy
|
||||
```
|
||||
@@ -0,0 +1,115 @@
|
||||
# Sprint 34: Pulse — Joint Briefing
|
||||
|
||||
**Goal:** Close Phase 2 — wire the economics simulation into the live game, expose price history and trade flows in the implant, and make the economy observable and tweakable at runtime.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Sprint: Decisions and Schema
|
||||
|
||||
No blocking pre-sprint decisions required. All economic architecture decisions (D-178 through D-188) are confirmed. #810 (event port) is the technical gate for the full chain — it must be the first ticket the server team starts.
|
||||
|
||||
| Item | Owner | Status |
|
||||
|------|-------|--------|
|
||||
| D-178: Economic Model Architecture | decisions/economics.md | Confirmed |
|
||||
| D-179: Stability Acceptance Criteria | decisions/economics.md | Confirmed |
|
||||
| D-180: Event Input Port | decisions/economics.md | Confirmed |
|
||||
| D-181: Signal Vocabulary | decisions/economics.md | Confirmed |
|
||||
| ObserverSnapshot v21 schema | Server → client | New this sprint (#822) |
|
||||
|
||||
---
|
||||
|
||||
## Sprint Ticket Map
|
||||
|
||||
### Server (sprint-34/server)
|
||||
```
|
||||
#810 Event port implementation
|
||||
→ #821 Integrate econ-sim into server tick loop
|
||||
→ #822 Expose economy state over IPC (ObserverSnapshot v21)
|
||||
→ #823 Economics debug command handler
|
||||
```
|
||||
|
||||
### Client (sprint-34/client)
|
||||
```
|
||||
#785 Star map: system population + GDP (standalone)
|
||||
#822 (server, blocker) → #824 Economics insert panel
|
||||
#823 (server, blocker) → #825 Debug console econ commands
|
||||
```
|
||||
|
||||
### Copy (sprint-34/copy)
|
||||
```
|
||||
#814 Rail infrastructure corporation gap (standalone)
|
||||
#695 Overheard conversations — 24 zone types (standalone, parallel)
|
||||
```
|
||||
|
||||
### Planning (sprint-34/planning)
|
||||
```
|
||||
#811 Brand layer design (early sprint)
|
||||
#748 Phase 3 breakdown workshop (after server tickets in_progress)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-Team Integration Points
|
||||
|
||||
**ObserverSnapshot v21 (server → client)**
|
||||
- Server: `server/src/bridge/types.rs` — add `EconomySnapshot` struct, bump `PROTOCOL_VERSION` to 21
|
||||
- Client: `client/scripts/snapshot_handler.gd` — parse `economy_snapshot` field, route to economics panel
|
||||
- Coordination: Server team defines the struct; client team consumes it. Server team ships #822 first; client team builds #824 with placeholder data in the meantime.
|
||||
|
||||
**Debug command flow (both teams)**
|
||||
- Server: `server/src/bridge/types.rs` — add `InjectEconEvent`, `SetEconParam`, `GetEconState` to `DebugCommandKind`
|
||||
- Client: `client/ui/debug_console.gd` — add `econ inject`, `econ param`, `econ inspect` command parsers
|
||||
- Coordination: Server team ships #823 before client team wires #825. Client team can build command parsing and help text independently; just gate the send path on #823 being merged.
|
||||
|
||||
**Star map GDP (#785)**
|
||||
- Client-only ticket. Check whether `res://data/star_map_data.json` already includes `population` and `gdp` fields. If not, update `tooling/generate-star-map-data.py` to include them from `server/data/systems.db`. This is a self-contained warmup — complete before #824.
|
||||
|
||||
---
|
||||
|
||||
## Cascade Enforcement
|
||||
|
||||
**No Phase 4 tickets.** This sprint closes Phase 2 and initiates Phase 3 planning via #748 and #811. The following are explicitly out of scope and must not be started, designed, or discussed:
|
||||
|
||||
- Character creation (#618, #619, #694, #606)
|
||||
- Tycoon starting states (#615)
|
||||
- Bookmark system (#614)
|
||||
- NPC personality surface area (#621)
|
||||
- Apartment generator (#617, #681)
|
||||
- Any ticket under Phase 4 epic #749
|
||||
|
||||
Phase 4 cannot start until Phase 3 delivers (Atlas of the Reach). Phase 3 planning workshop (#748) runs this sprint — but Phase 3 implementation tickets do not start until Sprint 35+.
|
||||
|
||||
---
|
||||
|
||||
## Sprint Completion Proof
|
||||
|
||||
The sprint is done when a developer can do all three of the following in the running game:
|
||||
|
||||
1. **Open the implant economics panel** — select any system, see live price data (price_current and price_trend for at least 6 commodities) updating in real time as the economy runs.
|
||||
2. **Trigger a supply shock from the debug console** — type `econ inject <system_id> shock 0.5 200`, observe the price_current values shift in the economics panel within a few ticks, then recover toward equilibrium over the next 200 ticks.
|
||||
3. **Mutate α from the debug console** — type `econ param alpha 0.01`, observe slower price adjustment in the panel; type `econ param alpha 0.06`, observe faster adjustment.
|
||||
|
||||
None of the above require a character. The economics panel and debug console can be exercised from the main game loop without entering the simulation world — they operate via the IPC bridge on whatever player session is active.
|
||||
|
||||
---
|
||||
|
||||
## Test Plan
|
||||
|
||||
**Phase alignment:** Sprint 34 is Phase 2 delivery infrastructure. Test focus is economic state correctness over IPC, not simulation model correctness (that was Sprint 33 / D-179 stability tests).
|
||||
|
||||
| Test | Tier | Owner |
|
||||
|------|------|-------|
|
||||
| ObserverSnapshot v21 roundtrip — serialize/deserialize EconomySnapshot | Tier 1 (fixture) | Server |
|
||||
| EconStateQuery → economy_snapshot present in next snapshot | Tier 2 (bridge) | Server |
|
||||
| `econ inject` → DebugResponsePayload.success true + price shift observable | Tier 2 (bridge) | Server |
|
||||
| Economics insert panel renders with fixture EconomySnapshot | Tier 3 (client live) | Client |
|
||||
| Debug console parses `econ inject` without error | Tier 3 (client live) | Client |
|
||||
| Star map popup shows population + GDP fields | Tier 3 (client live) | Client |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
None blocking implementation. One design question in-flight:
|
||||
|
||||
- **Q: Brand layer architecture** (#811 planning) — not blocking Sprint 34 implementation work. Resolved by planning team this sprint; produces Phase 3 tickets for Sprint 35.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Sprint 34: Pulse — Planning Tasks
|
||||
|
||||
**Goal:** Close Phase 2 — wire the economics simulation into the live game, expose price history and trade flows in the implant, and make the economy observable and tweakable at runtime.
|
||||
|
||||
**Branch:** `sprint-34/planning`
|
||||
**Agents:** Gestalt (systems), Burnelli-Sheldon (economics), Tyre (technical), Miri (worldbuilding), Qatux (documenter), SI (project manager)
|
||||
|
||||
## Tickets
|
||||
|
||||
| # | Title | Blocked by |
|
||||
|---|-------|------------|
|
||||
| #748 | Phase 3: Planetary/moon maps and station layouts — Atlas of the Reach | #747 (in progress → closes this sprint) |
|
||||
| #811 | Brand layer design | — |
|
||||
|
||||
## Context to Read Before Discussion
|
||||
|
||||
For **#811 (Brand layer design):**
|
||||
- `decisions/economics.md` — D-185 (Brands Are Not Commodities), D-184 (Commodity Catalog — what exists), D-173 (Commodity Taxonomy — three-tier structure)
|
||||
- `decisions/scope.md` — D-131 (broad economic verb vocabulary), D-118 (small business owner starting state — brands are the Phase 3 player layer)
|
||||
- `wiki/economics/commodities.toml`, `wiki/economics/production_chains.toml`
|
||||
|
||||
For **#748 (Phase 3 breakdown):**
|
||||
- `CLAUDE.md` — Development cascade table (Phase 3 = Planetary/moon maps, deliverable = Atlas of the Reach)
|
||||
- `decisions/architecture.md` — D-093 (Sova Transit District spatial layout), D-094 (district spatial hierarchy), D-095 (Horizon stations and gate infrastructure)
|
||||
- Sprint 33 deliverable: `server/data/systems.db` populated with 300+ systems, gate links, currency zones
|
||||
- `docs/atlas/` — existing atlas content
|
||||
|
||||
---
|
||||
|
||||
## #811 — Brand Layer Design
|
||||
|
||||
**Type:** Planning discussion — produces a D-record in `decisions/economics.md`
|
||||
|
||||
**What this is:** The brand/luxury goods system sits on top of the commodity layer (D-185 confirms brands are NOT commodities). Brands consume commodities as inputs. Brand pricing is driven by cultural/emotional/want mechanics, not tâtonnement. This design work answers the Phase 3 question: how does a player engage with the economy as a participant (producer/trader/brand-builder) rather than an observer?
|
||||
|
||||
**Discussion rounds:**
|
||||
|
||||
**Round 1 — Inventory (what exists, what is missing)**
|
||||
- What is the full design space of "brand" in the Reach? (Gestalt, Miri)
|
||||
- What D-records already constrain brand design? (Tyre reads economics.md, scope.md)
|
||||
- What is the player's economic verb set when brands exist? (Burnelli-Sheldon, Gestalt)
|
||||
|
||||
**Round 2 — Proposals**
|
||||
- Brand representation: is a brand a DB entity, a modifier on a commodity, or a separate production chain layer? (Tyre, Burnelli-Sheldon)
|
||||
- Cultural pricing model: how does a brand's cultural origin affect demand across currency zones? (Miri, Gestalt)
|
||||
- Player access: what verbs does a player have toward an existing brand vs. founding one? (Gestalt)
|
||||
|
||||
**Round 3 — Convergence**
|
||||
- Draft one D-record covering: brand representation in the data model, cultural demand pricing, player access verbs, and the boundary with Phase 2 commodity tâtonnement
|
||||
- SI creates follow-up implementation tickets for Phase 3 sprint
|
||||
|
||||
**Output:** D-NNN in `decisions/economics.md` (claim ID via `tooling/db/decision claim D economics "Brand layer architecture"`). Qatux files the record. SI creates 2-4 Phase 3 implementation tickets from the decision.
|
||||
|
||||
**CONSTRAINT:** This design session covers brand layer architecture only. No character creation, no tycoon states, no apartment generators. Phase 4 work is out of scope until Phase 3 delivers.
|
||||
|
||||
---
|
||||
|
||||
## #748 — Phase 3 Planetary Maps Breakdown Workshop
|
||||
|
||||
**Type:** Planning discussion — produces a sprint-ready ticket breakdown for Phase 3
|
||||
|
||||
**What this is:** Phase 3 deliverable is the Atlas of the Reach (implant app) — region-level maps at hundreds-of-km scale. Cities, rivers, mountains, rail lines, gate/portal locations, road hierarchy, named areas. This workshop answers: what is the minimal scope for a shippable Phase 3, and what tickets does it generate?
|
||||
|
||||
**Timing:** This discussion runs AFTER the server team confirms Phase 2 is closing (economics in-game, IPC bridge live). Do not start this discussion until Sprint 34 server tickets are at least in_progress.
|
||||
|
||||
**Discussion rounds:**
|
||||
|
||||
**Round 1 — Inventory**
|
||||
- What does Phase 3 require that does not exist? Read `docs/atlas/`, existing world data in `server/data/systems.db`. (Miri, Tyre)
|
||||
- What systems from Phase 2 does Phase 3 build on (gate network, system data, planet_class)? (Gestalt, Tyre)
|
||||
- What is the rendering target? (Implant app panel — same component library as economics panel?) (Tyre)
|
||||
|
||||
**Round 2 — Scope definition**
|
||||
- Define the MVP Atlas: which systems get maps first? (Miri — Sova/Krenn as canonical first-system per D-036)
|
||||
- Data authoring pipeline: how are planetary maps authored? Hand-drawn overlays on procedural heightmaps? Pure procedural? (Miri, Gestalt)
|
||||
- Implant app design: what does the Atlas panel look like? Click-through from the star map? (Tyre)
|
||||
|
||||
**Round 3 — Ticket breakdown**
|
||||
- Break Phase 3 into 4-8 implementation tickets across server, client, copy, visual teams
|
||||
- Assign team and priority to each
|
||||
- SI creates the tickets and blocks them appropriately under #748
|
||||
|
||||
**Output:** 4-8 new tickets (server + client + copy + visual) with team assignments, priorities, and explicit dependencies. SI creates them immediately at round end.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
#811 (brand layer design) — run early in sprint, unblocks Phase 3 planning
|
||||
#748 (Phase 3 breakdown) — run after server Sprint 34 tickets are in_progress
|
||||
```
|
||||
|
||||
## PR Workflow
|
||||
|
||||
Planning branch produces decisions and ticket updates only — no code. Commit decisions and close tickets:
|
||||
|
||||
```bash
|
||||
tea pr create --repo jpmschweitzer/settled-reach --login schweitz \
|
||||
--title "planning(economics): brand layer design and Phase 3 breakdown" \
|
||||
--description "Sprint 34 planning work" \
|
||||
--base main --head sprint-34/planning
|
||||
```
|
||||
@@ -0,0 +1,123 @@
|
||||
# Sprint 34: Pulse — Server Tasks
|
||||
|
||||
**Goal:** Close Phase 2 — wire the economics simulation into the live game, expose price history and trade flows in the implant, and make the economy observable and tweakable at runtime.
|
||||
|
||||
**Branch:** `sprint-34/server`
|
||||
**Agents:** Dudley (simulation), Tyre (architecture)
|
||||
|
||||
## New Tickets
|
||||
|
||||
| # | Title | Blocked by |
|
||||
|---|-------|------------|
|
||||
| #810 | Event input port implementation | #809 (done) |
|
||||
| #821 | Integrate econ-sim into game server tick loop | #810 |
|
||||
| #822 | Expose economy state over IPC bridge to client | #821 |
|
||||
| #823 | Economics debug command handler — event injection and parameter mutation | #821 |
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- `decisions/economics.md` — D-178 (model architecture — Leontief + tâtonnement + agents), D-179 (stability criteria), D-180 (event input port — EconEvent struct and visibility modes), D-181 (7-signal vocabulary per node), D-183 (iterative dev cycle)
|
||||
- `decisions/architecture.md` — D-020 (IPC architecture — ObserverSnapshot + PlayerAction), D-031 (tick-to-time mapping — 10 ticks = 1 game-minute)
|
||||
|
||||
## Notes
|
||||
|
||||
### #810 — Event input port implementation
|
||||
|
||||
The EconEvent struct (D-180) must be added to `tooling/econ-sim/src/model.rs` or a new `events.rs` module. The port is the typed interface through which all external disruptions enter the simulation. An event carries:
|
||||
|
||||
```
|
||||
EconEvent {
|
||||
target: Node | NodeSet | Corridor | TradeRoute | Currency | Commodity,
|
||||
effect: ProductivityMultiplier | CapacityMultiplier | DemandShock | ExchangeShock,
|
||||
duration: ticks,
|
||||
visibility: Global | Proximate(hops) | Disclosed(specific_nodes) | Hidden,
|
||||
}
|
||||
```
|
||||
|
||||
Visibility modes are defined in D-180. For this sprint, only `Global` and `Proximate` need to be exercised — `Hidden` is Phase 3 territory (requires the player inspect verb). The port must accept events from: (a) the server tick loop (#821), and (b) debug commands (#823). Test: inject a supply shock, verify cascade propagates and prices recover within 200 ticks per D-179 Test 3.
|
||||
|
||||
### #821 — Integrate econ-sim into game server tick loop
|
||||
|
||||
The econ-sim is currently a standalone CLI binary at `tooling/econ-sim/`. This ticket makes it run inside the server process. Approach:
|
||||
|
||||
1. Extract the simulation logic from `tooling/econ-sim/src/main.rs` into a reusable library crate (e.g. `tooling/econ-sim/src/lib.rs` or a new `server/src/economy/` module — Tyre to decide the crate boundary).
|
||||
2. Add a `bevy_ecs` `System` that advances the economy N ticks per game tick (rate TBD — likely 1 economy tick per 10 game ticks given D-031 tick-to-time mapping).
|
||||
3. Store the current economy state as a `Resource` in bevy_ecs so downstream systems (#822, #823) can query it.
|
||||
4. Economy state must include all 7 D-181 signals per active node so the bridge can later serialize the relevant subset.
|
||||
|
||||
Key files: `server/src/simulation/ticker.rs` (where per-tick systems run), `tooling/econ-sim/src/model.rs` (simulation state), `tooling/econ-sim/src/trade.rs` (tâtonnement step). The DB at `server/data/systems.db` is already populated from Sprint 33.
|
||||
|
||||
Do NOT load the econ DB on every tick — load once at server startup into the bevy_ecs Resource.
|
||||
|
||||
### #822 — Expose economy state over IPC bridge to client
|
||||
|
||||
Extend `ObserverSnapshot` to version 21 with an `economy_snapshot` field:
|
||||
|
||||
```rust
|
||||
#[serde(default)]
|
||||
pub economy_snapshot: Option<EconomySnapshot>,
|
||||
```
|
||||
|
||||
`EconomySnapshot` carries per-system data for the client's economics panel (#824). Phase 2 deliverable is D-181 signals 1–2 only (price_current, price_trend). Struct sketch:
|
||||
|
||||
```rust
|
||||
pub struct EconomySnapshot {
|
||||
pub tick: u64,
|
||||
pub nodes: Vec<EconNodeSnapshot>,
|
||||
}
|
||||
|
||||
pub struct EconNodeSnapshot {
|
||||
pub system_id: u32,
|
||||
pub commodity_id: u32,
|
||||
pub price_current: f64,
|
||||
pub price_trend: f64, // delta over last N ticks
|
||||
}
|
||||
```
|
||||
|
||||
Add `EconStateQuery` to the `PlayerAction` enum for on-demand pulls — the client does not need economy data every tick (that would balloon snapshot size). The server responds to `EconStateQuery` by populating `economy_snapshot` on the next snapshot. Without a query, `economy_snapshot` is `None`.
|
||||
|
||||
Update `PROTOCOL_VERSION` to 21 in `server/src/bridge/types.rs`.
|
||||
|
||||
### #823 — Economics debug command handler
|
||||
|
||||
Extend `DebugCommandKind` in `server/src/bridge/types.rs` with three new variants:
|
||||
|
||||
```rust
|
||||
/// Inject an economic event into the running simulation.
|
||||
InjectEconEvent {
|
||||
system_id: u32,
|
||||
commodity_id: Option<u32>, // None = system-wide
|
||||
effect: EconDebugEffect,
|
||||
magnitude: f64,
|
||||
duration_ticks: u32,
|
||||
},
|
||||
/// Mutate a tâtonnement parameter at runtime.
|
||||
SetEconParam {
|
||||
param: EconParamKind, // Alpha | Beta | CorridorFriction { system_a, system_b }
|
||||
value: f64,
|
||||
},
|
||||
/// Return all 7 D-181 signals for a named system.
|
||||
GetEconState {
|
||||
system_id: u32,
|
||||
},
|
||||
```
|
||||
|
||||
Wire these into the existing debug command dispatch in `server/src/simulation/` (wherever `DebugCommandKind` is matched). Return results via `DebugResponsePayload.text` as a human-readable multi-line string. Blocked by #821 (economy resource must exist to query or mutate).
|
||||
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
#810 (event port) → #821 (server tick integration) → #822 (IPC exposure)
|
||||
→ #823 (debug command handler)
|
||||
```
|
||||
|
||||
#822 and #823 are parallel after #821 completes.
|
||||
|
||||
## PR Workflow
|
||||
|
||||
```bash
|
||||
tea pr create --repo jpmschweitzer/settled-reach --login schweitz \
|
||||
--title "feat(simulation): economics in-game tick loop and IPC bridge" \
|
||||
--description "Sprint 34 server work" \
|
||||
--base main --head sprint-34/server
|
||||
```
|
||||
@@ -106,6 +106,45 @@ def parse_gttr_excerpt(system_id: str) -> str:
|
||||
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:
|
||||
"""Build a map from system_id to list of adjacent system_ids from edges."""
|
||||
adj: dict = {}
|
||||
@@ -162,6 +201,10 @@ def generate() -> dict:
|
||||
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()}
|
||||
conn.close()
|
||||
|
||||
adjacency = build_adjacency(star_map["edges"])
|
||||
@@ -204,6 +247,9 @@ def generate() -> dict:
|
||||
|
||||
entry["bodies"] = "%d habitable · %d inhabited" % (hab, inh)
|
||||
entry["population"] = "{:,}".format(total_pop)
|
||||
gdp_str = compute_gdp(total_pop, econ_tiers.get(sid))
|
||||
if gdp_str:
|
||||
entry["gdp"] = gdp_str
|
||||
if gttr:
|
||||
entry["gttr_excerpt"] = gttr
|
||||
if n.get("is_gateway"):
|
||||
|
||||
Reference in New Issue
Block a user