From fb5c8854e8630e7154d3cc8b3136d9d5ebaa67ab Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 5 Apr 2026 09:37:04 +0200 Subject: [PATCH 1/5] refactor(simulation): split atlas.rs subcommands into modules Extract 2119-line monolithic atlas.rs into 8 focused modules under src/bin/atlas/: main.rs (thin dispatch), common.rs (shared types and DB helpers), show.rs, mutate.rs, stats.rs, systems.rs, author.rs, and sync_wiki.rs. Commands enum stays in main.rs; each match arm delegates to module::cmd_fn(&conn, args). No behavior change. Closes #776. Co-Authored-By: Claude Opus 4.6 --- server/Cargo.lock | 2 +- server/src/bin/atlas.rs | 2119 ----------------------------- server/src/bin/atlas/author.rs | 640 +++++++++ server/src/bin/atlas/common.rs | 244 ++++ server/src/bin/atlas/main.rs | 275 ++++ server/src/bin/atlas/mutate.rs | 109 ++ server/src/bin/atlas/show.rs | 127 ++ server/src/bin/atlas/stats.rs | 90 ++ server/src/bin/atlas/sync_wiki.rs | 319 +++++ server/src/bin/atlas/systems.rs | 382 ++++++ 10 files changed, 2187 insertions(+), 2120 deletions(-) delete mode 100644 server/src/bin/atlas.rs create mode 100644 server/src/bin/atlas/author.rs create mode 100644 server/src/bin/atlas/common.rs create mode 100644 server/src/bin/atlas/main.rs create mode 100644 server/src/bin/atlas/mutate.rs create mode 100644 server/src/bin/atlas/show.rs create mode 100644 server/src/bin/atlas/stats.rs create mode 100644 server/src/bin/atlas/sync_wiki.rs create mode 100644 server/src/bin/atlas/systems.rs diff --git a/server/Cargo.lock b/server/Cargo.lock index b510e031b..820c955e0 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "settled-reach-server" -version = "0.1.29" +version = "0.1.30" dependencies = [ "bevy_app", "bevy_ecs", diff --git a/server/src/bin/atlas.rs b/server/src/bin/atlas.rs deleted file mode 100644 index f346ef3b5..000000000 --- a/server/src/bin/atlas.rs +++ /dev/null @@ -1,2119 +0,0 @@ -//! Atlas CLI — query and manage celestial bodies and stations in systems.db. -//! -//! # Usage -//! -//! ```sh -//! # Via wrapper script (recommended): -//! tooling/atlas list-bodies --system "GJ 15A" -//! tooling/atlas list-bodies --type planet --inhabited -//! tooling/atlas show-system "GJ 15A" -//! tooling/atlas show-body "GJ 15Ab" -//! tooling/atlas add-body --system "GJ 15A" --type planet --orbit 1 --id "GJ 15Ab" -//! tooling/atlas stats -//! tooling/atlas corridor-status # remaining systems by corridor/hop -//! tooling/atlas populate # bulk classifier pass -//! -//! # Direct: -//! cargo run --bin atlas -- [args] -//! ``` - -use std::path::PathBuf; -use std::process; - -use clap::{Parser, Subcommand}; -use rusqlite::{params, Connection}; -use serde::Serialize; - -// --------------------------------------------------------------------------- -// CLI structure -// --------------------------------------------------------------------------- - -#[derive(Parser)] -#[command( - name = "atlas", - about = "Query and manage celestial bodies and stations in systems.db" -)] -struct Cli { - /// Path to the SQLite database (default: auto-detect from worktree) - #[arg(long, global = true)] - db: Option, - - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Show full system hierarchy (star → bodies → stations) - ShowSystem { - /// System ID (e.g., "GJ 15A") - system_id: String, - }, - /// Show a single body's details - ShowBody { - /// Body ID (e.g., "GJ 15Ab") - body_id: String, - }, - /// Show a single station's details - ShowStation { - /// Station ID (e.g., "GJ 15Ab-S1") - station_id: String, - }, - /// List bodies with optional filters - ListBodies { - #[arg(long)] - system: Option, - #[arg(long, value_name = "TYPE")] - r#type: Option, - #[arg(long)] - inhabited: bool, - #[arg(long)] - unnamed: bool, - }, - /// List stations with optional filters - ListStations { - #[arg(long)] - system: Option, - #[arg(long, value_name = "TYPE")] - r#type: Option, - }, - /// Add a body record - AddBody { - #[arg(long)] - id: String, - #[arg(long)] - system: String, - #[arg(long, value_name = "TYPE")] - r#type: String, - #[arg(long)] - orbit: Option, - #[arg(long)] - name: Option, - #[arg(long)] - parent: Option, - #[arg(long)] - mass_class: Option, - #[arg(long)] - atmosphere: Option, - #[arg(long)] - gravity: Option, - #[arg(long)] - biome: Option, - #[arg(long)] - inhabited: bool, - #[arg(long)] - population: Option, - }, - /// Add a station record - AddStation { - #[arg(long)] - id: String, - #[arg(long)] - system: String, - #[arg(long)] - orbits: Option, - #[arg(long, value_name = "TYPE")] - r#type: String, - #[arg(long)] - name: Option, - #[arg(long)] - population: Option, - #[arg(long)] - docking: Option, - #[arg(long)] - gate: bool, - }, - /// Database statistics - Stats, - /// Generate a body/station proposal for a single system (writes JSON file for review) - Author { - /// System ID (e.g., "GJ 71") - system_id: String, - /// Output directory for proposal JSON (default: docs/atlas/proposals/) - #[arg(long, default_value = "docs/atlas/proposals")] - outdir: String, - /// Path to wiki directory (default: wiki/star-systems/) - #[arg(long, default_value = "wiki/star-systems")] - wiki: String, - }, - /// Commit an approved proposal JSON to the database - CommitSystem { - /// Path to the proposal JSON file - path: String, - }, - /// Wipe all bodies and stations for a single system - WipeSystem { - /// System ID - system_id: String, - }, - /// List systems with optional filters - ListSystems { - /// Filter by geographic sector (e.g., "west_reach", "east_reach") - #[arg(long)] - sector: Option, - /// Filter by hop distance - #[arg(long)] - hop: Option, - /// Only show systems that have bodies authored - #[arg(long)] - finished: bool, - /// Only show systems that have NO bodies yet - #[arg(long)] - unfinished: bool, - }, - /// List systems that have no bodies yet - Unfinished { - /// Filter by geographic sector - #[arg(long)] - sector: Option, - }, - /// List unfinished systems at a specific hop distance (or next available hop) - Next { - /// Hop distance (omit to find the lowest hop with unfinished systems) - hop: Option, - /// Filter by geographic sector - #[arg(long)] - sector: Option, - }, - /// Sync body/station data into wiki page for a system (or all systems) - SyncWiki { - /// System ID (omit for all systems with bodies) - system_id: Option, - /// Path to wiki directory (default: wiki/star-systems/) - #[arg(long, default_value = "wiki/star-systems")] - wiki: String, - }, - /// Show remaining unfinished systems grouped by geographic sector and hop distance - CorridorStatus, -} - -// --------------------------------------------------------------------------- -// Output types -// --------------------------------------------------------------------------- - -#[derive(Serialize)] -struct BodyRow { - body_id: String, - system_id: String, - parent_body_id: Option, - body_type: String, - orbit_index: Option, - proper_name: Option, - mass_class: Option, - atmosphere: Option, - surface_gravity: Option, - biome_summary: Option, - hydrosphere: Option, - inhabited: bool, - population: i64, - economic_role: Option, - cultural_corridor: Option, - industrial_corridor: Option, -} - -#[derive(Serialize)] -struct StationRow { - station_id: String, - system_id: String, - orbits_body_id: Option, - station_type: String, - proper_name: Option, - population: i64, - economic_role: Option, - governance_type: Option, - docking_class: Option, - has_gate_infrastructure: bool, - district_count: i32, -} - -#[derive(Serialize)] -struct SystemSummary { - system_id: String, - proper_name: Option, - star_type: Option, - geographic_sector: Option, - habitable_planet_count: Option, - inhabited_planet_count: Option, - bodies: Vec, - stations: Vec, -} - -#[derive(Serialize)] -struct StatsOutput { - systems: i64, - bodies: i64, - bodies_by_type: Vec, - inhabited_bodies: i64, - stations: i64, - stations_by_type: Vec, - systems_with_bodies: i64, - systems_without_bodies: i64, -} - -#[derive(Serialize)] -struct TypeCount { - r#type: String, - count: i64, -} - -#[derive(Serialize, serde::Deserialize, Clone)] -struct ProposalBody { - body_id: String, - proper_name: Option, - body_type: String, - orbit_index: i32, - parent_body_id: Option, - inhabited: bool, - population: Option, - mass_class: Option, - surface_gravity: Option, - orbital_period_days: Option, - rotation_period_hours: Option, - atmosphere: Option, - biome_summary: Option, - hydrosphere: Option, - economic_role: Option, - settlement_pattern: Option, - industrial_corridor: Option, - notes: String, -} - -#[derive(Serialize, serde::Deserialize, Clone)] -struct ProposalStation { - station_id: String, - proper_name: Option, - orbits_body_id: String, - station_type: String, - population: Option, - economic_role: Option, - docking_class: Option, - has_gate_infrastructure: bool, - notes: String, -} - -#[derive(Serialize, serde::Deserialize)] -struct SystemProposal { - system_id: String, - proper_name: Option, - star_type: Option, - spectral_class: Option, - wiki_data: WikiData, - bodies: Vec, - stations: Vec, -} - -#[derive(Serialize, serde::Deserialize)] -struct WikiData { - habitable_count: i32, - inhabited_count: i32, - has_gas_giant: bool, - has_asteroid_belt: bool, - has_horizon_station: bool, - raw_bodies_line: Option, -} - -// --------------------------------------------------------------------------- -// Database helpers -// --------------------------------------------------------------------------- - -fn resolve_db_path(explicit: Option) -> PathBuf { - if let Some(p) = explicit { - return p; - } - // Walk up from CWD looking for server/data/systems.db - let mut dir = std::env::current_dir().expect("Cannot determine CWD"); - loop { - let candidate = dir.join("server").join("data").join("systems.db"); - if candidate.exists() { - return candidate; - } - if !dir.pop() { - break; - } - } - eprintln!("error: cannot find server/data/systems.db — pass --db explicitly"); - process::exit(1); -} - -fn open_db(path: &PathBuf) -> Connection { - let conn = Connection::open(path).unwrap_or_else(|e| { - eprintln!("error: cannot open {}: {}", path.display(), e); - process::exit(1); - }); - conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;") - .unwrap(); - conn -} - -// --------------------------------------------------------------------------- -// Commands -// --------------------------------------------------------------------------- - -fn cmd_show_system(conn: &Connection, system_id: &str) { - let mut stmt = conn - .prepare( - "SELECT system_id, proper_name, star_type, geographic_sector, - habitable_planet_count, inhabited_planet_count - FROM star_systems WHERE system_id = ?1", - ) - .unwrap(); - - let sys: Option = stmt - .query_row(params![system_id], |row| { - Ok(SystemSummary { - system_id: row.get(0)?, - proper_name: row.get(1)?, - star_type: row.get(2)?, - geographic_sector: row.get(3)?, - habitable_planet_count: row.get(4)?, - inhabited_planet_count: row.get(5)?, - bodies: Vec::new(), - stations: Vec::new(), - }) - }) - .ok(); - - let Some(mut sys) = sys else { - eprintln!("error: system '{}' not found", system_id); - process::exit(1); - }; - - sys.bodies = query_bodies(conn, Some(system_id), None, false, false); - sys.stations = query_stations(conn, Some(system_id), None); - - println!("{}", serde_json::to_string_pretty(&sys).unwrap()); -} - -fn cmd_show_body(conn: &Connection, body_id: &str) { - let row = conn - .query_row( - "SELECT body_id, system_id, parent_body_id, body_type, orbit_index, - proper_name, mass_class, atmosphere, surface_gravity, - biome_summary, hydrosphere, inhabited, population, - economic_role, cultural_corridor, industrial_corridor - FROM bodies WHERE body_id = ?1", - params![body_id], - |row| { - Ok(BodyRow { - body_id: row.get(0)?, - system_id: row.get(1)?, - parent_body_id: row.get(2)?, - body_type: row.get(3)?, - orbit_index: row.get(4)?, - proper_name: row.get(5)?, - mass_class: row.get(6)?, - atmosphere: row.get(7)?, - surface_gravity: row.get(8)?, - biome_summary: row.get(9)?, - hydrosphere: row.get(10)?, - inhabited: row.get::<_, i32>(11)? != 0, - population: row.get::<_, Option>(12)?.unwrap_or(0), - economic_role: row.get(13)?, - cultural_corridor: row.get(14)?, - industrial_corridor: row.get(15)?, - }) - }, - ) - .unwrap_or_else(|_| { - eprintln!("error: body '{}' not found", body_id); - process::exit(1); - }); - - // Also fetch stations orbiting this body - let stations = query_stations_for_body(conn, body_id); - - #[derive(Serialize)] - struct BodyDetail { - #[serde(flatten)] - body: BodyRow, - stations: Vec, - } - - let detail = BodyDetail { - body: row, - stations, - }; - println!("{}", serde_json::to_string_pretty(&detail).unwrap()); -} - -fn cmd_show_station(conn: &Connection, station_id: &str) { - let row = conn - .query_row( - "SELECT station_id, system_id, orbits_body_id, station_type, - proper_name, population, economic_role, governance_type, - docking_class, has_gate_infrastructure, district_count - FROM stations WHERE station_id = ?1", - params![station_id], - |row| { - Ok(StationRow { - station_id: row.get(0)?, - system_id: row.get(1)?, - orbits_body_id: row.get(2)?, - station_type: row.get(3)?, - proper_name: row.get(4)?, - population: row.get::<_, Option>(5)?.unwrap_or(0), - economic_role: row.get(6)?, - governance_type: row.get(7)?, - docking_class: row.get(8)?, - has_gate_infrastructure: row.get::<_, Option>(9)?.unwrap_or(0) != 0, - district_count: row.get::<_, Option>(10)?.unwrap_or(1), - }) - }, - ) - .unwrap_or_else(|_| { - eprintln!("error: station '{}' not found", station_id); - process::exit(1); - }); - - println!("{}", serde_json::to_string_pretty(&row).unwrap()); -} - -fn query_bodies( - conn: &Connection, - system: Option<&str>, - body_type: Option<&str>, - inhabited_only: bool, - unnamed_only: bool, -) -> Vec { - let mut sql = String::from( - "SELECT body_id, system_id, parent_body_id, body_type, orbit_index, - proper_name, mass_class, atmosphere, surface_gravity, - biome_summary, hydrosphere, inhabited, population, - economic_role, cultural_corridor, industrial_corridor - FROM bodies WHERE 1=1", - ); - let mut param_values: Vec> = Vec::new(); - - if let Some(s) = system { - sql.push_str(" AND system_id = ?"); - param_values.push(Box::new(s.to_string())); - } - if let Some(t) = body_type { - sql.push_str(" AND body_type = ?"); - param_values.push(Box::new(t.to_string())); - } - if inhabited_only { - sql.push_str(" AND inhabited = 1"); - } - if unnamed_only { - sql.push_str(" AND proper_name IS NULL"); - } - sql.push_str(" ORDER BY system_id, orbit_index"); - - let params_ref: Vec<&dyn rusqlite::types::ToSql> = - param_values.iter().map(|p| p.as_ref()).collect(); - let mut stmt = conn.prepare(&sql).unwrap(); - let rows = stmt - .query_map(params_ref.as_slice(), |row| { - Ok(BodyRow { - body_id: row.get(0)?, - system_id: row.get(1)?, - parent_body_id: row.get(2)?, - body_type: row.get(3)?, - orbit_index: row.get(4)?, - proper_name: row.get(5)?, - mass_class: row.get(6)?, - atmosphere: row.get(7)?, - surface_gravity: row.get(8)?, - biome_summary: row.get(9)?, - hydrosphere: row.get(10)?, - inhabited: row.get::<_, i32>(11)? != 0, - population: row.get::<_, Option>(12)?.unwrap_or(0), - economic_role: row.get(13)?, - cultural_corridor: row.get(14)?, - industrial_corridor: row.get(15)?, - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect(); - rows -} - -fn query_stations( - conn: &Connection, - system: Option<&str>, - station_type: Option<&str>, -) -> Vec { - let mut sql = String::from( - "SELECT station_id, system_id, orbits_body_id, station_type, - proper_name, population, economic_role, governance_type, - docking_class, has_gate_infrastructure, district_count - FROM stations WHERE 1=1", - ); - let mut param_values: Vec> = Vec::new(); - - if let Some(s) = system { - sql.push_str(" AND system_id = ?"); - param_values.push(Box::new(s.to_string())); - } - if let Some(t) = station_type { - sql.push_str(" AND station_type = ?"); - param_values.push(Box::new(t.to_string())); - } - sql.push_str(" ORDER BY system_id, station_id"); - - let params_ref: Vec<&dyn rusqlite::types::ToSql> = - param_values.iter().map(|p| p.as_ref()).collect(); - let mut stmt = conn.prepare(&sql).unwrap(); - stmt.query_map(params_ref.as_slice(), |row| { - Ok(StationRow { - station_id: row.get(0)?, - system_id: row.get(1)?, - orbits_body_id: row.get(2)?, - station_type: row.get(3)?, - proper_name: row.get(4)?, - population: row.get::<_, Option>(5)?.unwrap_or(0), - economic_role: row.get(6)?, - governance_type: row.get(7)?, - docking_class: row.get(8)?, - has_gate_infrastructure: row.get::<_, Option>(9)?.unwrap_or(0) != 0, - district_count: row.get::<_, Option>(10)?.unwrap_or(1), - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect() -} - -fn query_stations_for_body(conn: &Connection, body_id: &str) -> Vec { - let mut stmt = conn - .prepare( - "SELECT station_id, system_id, orbits_body_id, station_type, - proper_name, population, economic_role, governance_type, - docking_class, has_gate_infrastructure, district_count - FROM stations WHERE orbits_body_id = ?1 ORDER BY station_id", - ) - .unwrap(); - stmt.query_map(params![body_id], |row| { - Ok(StationRow { - station_id: row.get(0)?, - system_id: row.get(1)?, - orbits_body_id: row.get(2)?, - station_type: row.get(3)?, - proper_name: row.get(4)?, - population: row.get::<_, Option>(5)?.unwrap_or(0), - economic_role: row.get(6)?, - governance_type: row.get(7)?, - docking_class: row.get(8)?, - has_gate_infrastructure: row.get::<_, Option>(9)?.unwrap_or(0) != 0, - district_count: row.get::<_, Option>(10)?.unwrap_or(1), - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect() -} - -fn cmd_list_bodies( - conn: &Connection, - system: Option<&str>, - body_type: Option<&str>, - inhabited: bool, - unnamed: bool, -) { - let bodies = query_bodies(conn, system, body_type, inhabited, unnamed); - println!("{}", serde_json::to_string_pretty(&bodies).unwrap()); -} - -fn cmd_list_stations(conn: &Connection, system: Option<&str>, station_type: Option<&str>) { - let stations = query_stations(conn, system, station_type); - println!("{}", serde_json::to_string_pretty(&stations).unwrap()); -} - -fn cmd_add_body(conn: &Connection, args: &Commands) { - let Commands::AddBody { - id, - system, - r#type, - orbit, - name, - parent, - mass_class, - atmosphere, - gravity, - biome, - inhabited, - population, - } = args - else { - unreachable!() - }; - - conn.execute( - "INSERT INTO bodies (body_id, system_id, parent_body_id, body_type, orbit_index, - proper_name, mass_class, atmosphere, surface_gravity, - biome_summary, inhabited, population) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", - params![ - id, - system, - parent, - r#type, - orbit, - name, - mass_class, - atmosphere, - gravity, - biome, - *inhabited as i32, - population.unwrap_or(0), - ], - ) - .unwrap_or_else(|e| { - eprintln!("error: {}", e); - process::exit(1); - }); - - println!(r#"{{"ok": true, "body_id": "{}"}}"#, id); -} - -fn cmd_add_station(conn: &Connection, args: &Commands) { - let Commands::AddStation { - id, - system, - orbits, - r#type, - name, - population, - docking, - gate, - } = args - else { - unreachable!() - }; - - conn.execute( - "INSERT INTO stations (station_id, system_id, orbits_body_id, station_type, - proper_name, population, docking_class, has_gate_infrastructure) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - params![ - id, - system, - orbits, - r#type, - name, - population.unwrap_or(0), - docking, - *gate as i32, - ], - ) - .unwrap_or_else(|e| { - eprintln!("error: {}", e); - process::exit(1); - }); - - println!(r#"{{"ok": true, "station_id": "{}"}}"#, id); -} - -fn cmd_stats(conn: &Connection) { - let systems: i64 = conn - .query_row("SELECT COUNT(*) FROM star_systems", [], |r| r.get(0)) - .unwrap(); - let bodies: i64 = conn - .query_row("SELECT COUNT(*) FROM bodies", [], |r| r.get(0)) - .unwrap(); - let inhabited_bodies: i64 = conn - .query_row("SELECT COUNT(*) FROM bodies WHERE inhabited = 1", [], |r| { - r.get(0) - }) - .unwrap(); - let stations: i64 = conn - .query_row("SELECT COUNT(*) FROM stations", [], |r| r.get(0)) - .unwrap(); - let systems_with_bodies: i64 = conn - .query_row("SELECT COUNT(DISTINCT system_id) FROM bodies", [], |r| { - r.get(0) - }) - .unwrap(); - - let mut bodies_by_type = Vec::new(); - { - let mut stmt = conn - .prepare("SELECT body_type, COUNT(*) FROM bodies GROUP BY body_type ORDER BY body_type") - .unwrap(); - let rows = stmt - .query_map([], |row| { - Ok(TypeCount { - r#type: row.get(0)?, - count: row.get(1)?, - }) - }) - .unwrap(); - for r in rows.flatten() { - bodies_by_type.push(r); - } - } - - let mut stations_by_type = Vec::new(); - { - let mut stmt = conn - .prepare("SELECT station_type, COUNT(*) FROM stations GROUP BY station_type ORDER BY station_type") - .unwrap(); - let rows = stmt - .query_map([], |row| { - Ok(TypeCount { - r#type: row.get(0)?, - count: row.get(1)?, - }) - }) - .unwrap(); - for r in rows.flatten() { - stations_by_type.push(r); - } - } - - let stats = StatsOutput { - systems, - bodies, - bodies_by_type, - inhabited_bodies, - stations, - stations_by_type, - systems_with_bodies, - systems_without_bodies: systems - systems_with_bodies, - }; - println!("{}", serde_json::to_string_pretty(&stats).unwrap()); -} - -fn parse_wiki_bodies_line(wiki_dir: &str, system_id: &str) -> (Option, i32, i32) { - // Try to find the wiki page for this system - let sid_slug = system_id.replace(' ', "-"); - let wiki_path = std::path::Path::new(wiki_dir) - .join(&sid_slug) - .join("index.md"); - if !wiki_path.exists() { - return (None, 0, 0); - } - let content = std::fs::read_to_string(&wiki_path).unwrap_or_default(); - // Look for "| **Bodies** | X habitable · Y inhabited |" - for line in content.lines() { - if line.contains("**Bodies**") { - let raw = line.to_string(); - let mut hab = 0i32; - let mut inh = 0i32; - // Parse "N habitable" - if let Some(pos) = line.find("habitable") { - let before = &line[..pos]; - let parts: Vec<&str> = before.split_whitespace().collect(); - if let Some(n) = parts.last() { - hab = n.parse().unwrap_or(0); - } - } - // Parse "N inhabited" - if let Some(pos) = line.find("inhabited") { - let before = &line[..pos]; - let parts: Vec<&str> = before.split_whitespace().collect(); - if let Some(n) = parts.last() { - inh = n.parse().unwrap_or(0); - } - } - return (Some(raw), hab, inh); - } - } - (None, 0, 0) -} - -fn generate_body_matrix( - system_id: &str, - star_type: Option<&str>, - spectral: Option<&str>, - wiki_hab: i32, - wiki_inh: i32, - db_gas_giant: bool, - db_belt: bool, - has_horizon: bool, -) -> (Vec, Vec) { - let sid = system_id.replace(' ', ""); - let mut bodies = Vec::new(); - let mut stations = Vec::new(); - let mut orbit = 1; - - // Determine planet count from star type if wiki doesn't specify - let spectral_char = spectral.and_then(|s| s.chars().next()).unwrap_or('M'); - - // Base planet count by spectral type. - // Sol has 8. TRAPPIST-1 (M-dwarf) has 7. Minimum 6 for any star. - let total_planets = if wiki_hab > 0 || wiki_inh > 0 { - // Wiki has data — use it as the inhabited/habitable core, pad to realistic count - let known = wiki_hab.max(wiki_inh); - match spectral_char { - 'O' | 'B' | 'A' => (known + 4).max(6), // hot stars — fewer but still 6+ - 'F' => (known + 5).max(8), // bright — wide system, 8+ - 'G' => (known + 5).max(8), // sol-like — 8 is baseline - 'K' => (known + 4).max(7), // cooler — 7+ typical - 'M' => (known + 4).max(6), // compact but TRAPPIST-1 has 7 - _ => (known + 4).max(6), - } - } else { - // No wiki data — generate realistic count - match spectral_char { - 'O' | 'B' | 'A' => 6, - 'F' => 8, - 'G' => 8, - 'K' => 7, - 'M' => 6, - _ => 6, - } - }; - - // Determine if binary — affects naming - let is_binary = star_type == Some("binary"); - - // Place inner barren rocky planets - let inner_barren = if total_planets > wiki_inh + 1 { 1 } else { 0 }; - for _ in 0..inner_barren { - let letter = (b'b' + orbit as u8 - 1) as char; - let body_id = if is_binary { - format!("{}A{}", sid, letter) // circumbinary assumed for now - } else { - format!("{}{}", sid, letter) - }; - bodies.push(ProposalBody { - body_id, - body_type: "planet".into(), - orbit_index: orbit, - parent_body_id: None, - inhabited: false, - proper_name: None, - population: None, - mass_class: Some("terrestrial".into()), - surface_gravity: Some(0.38), - orbital_period_days: Some(88.0), - rotation_period_hours: Some(1408.0), - atmosphere: Some("none".into()), - biome_summary: Some("barren".into()), - hydrosphere: None, - economic_role: None, - settlement_pattern: None, - industrial_corridor: None, - notes: "inner rocky, uninhabited".into(), - }); - orbit += 1; - } - - // Place inhabited planets - for i in 0..wiki_inh { - let letter = (b'b' + orbit as u8 - 1) as char; - let body_id = if is_binary { - format!("{}A{}", sid, letter) - } else { - format!("{}{}", sid, letter) - }; - bodies.push(ProposalBody { - body_id, - body_type: "planet".into(), - orbit_index: orbit, - parent_body_id: None, - inhabited: true, - proper_name: None, - population: None, - mass_class: Some("terrestrial".into()), - surface_gravity: Some(0.9), - orbital_period_days: Some(365.0), - rotation_period_hours: Some(24.0), - atmosphere: Some("standard".into()), - biome_summary: Some("temperate".into()), - hydrosphere: None, - economic_role: None, - settlement_pattern: None, - industrial_corridor: None, - notes: format!("inhabited planet {}/{}", i + 1, wiki_inh), - }); - orbit += 1; - } - - // Place habitable-but-uninhabited planets (hab > inh) - let hab_only = (wiki_hab - wiki_inh).max(0); - for _ in 0..hab_only { - let letter = (b'b' + orbit as u8 - 1) as char; - let body_id = if is_binary { - format!("{}A{}", sid, letter) - } else { - format!("{}{}", sid, letter) - }; - bodies.push(ProposalBody { - body_id, - body_type: "planet".into(), - orbit_index: orbit, - parent_body_id: None, - inhabited: false, - proper_name: None, - population: None, - mass_class: Some("terrestrial".into()), - surface_gravity: Some(0.85), - orbital_period_days: Some(400.0), - rotation_period_hours: Some(26.0), - atmosphere: Some("standard".into()), - biome_summary: Some("temperate".into()), - hydrosphere: None, - economic_role: None, - settlement_pattern: None, - industrial_corridor: None, - notes: "habitable, uninhabited".into(), - }); - orbit += 1; - } - - // Outer rocky/ice planets to fill remaining count - let placed = inner_barren + wiki_inh + hab_only; - let remaining_planets = (total_planets - placed).max(0); - for _ in 0..remaining_planets { - let letter = (b'b' + orbit as u8 - 1) as char; - let body_id = if is_binary { - format!("{}A{}", sid, letter) - } else { - format!("{}{}", sid, letter) - }; - bodies.push(ProposalBody { - body_id, - body_type: "planet".into(), - orbit_index: orbit, - parent_body_id: None, - inhabited: false, - proper_name: None, - population: None, - mass_class: Some("terrestrial".into()), - surface_gravity: Some(0.5), - orbital_period_days: Some(2000.0), - rotation_period_hours: Some(18.0), - atmosphere: Some("thin".into()), - biome_summary: Some("frozen".into()), - hydrosphere: None, - economic_role: None, - settlement_pattern: None, - industrial_corridor: None, - notes: "outer rocky/ice, uninhabited".into(), - }); - orbit += 1; - } - - // Asteroid belt - if db_belt { - bodies.push(ProposalBody { - body_id: format!("{}-belt", sid), - body_type: "asteroid_belt".into(), - orbit_index: orbit, - parent_body_id: None, - inhabited: false, - proper_name: None, - population: None, - mass_class: None, - surface_gravity: None, - orbital_period_days: None, - rotation_period_hours: None, - atmosphere: None, - biome_summary: None, - hydrosphere: None, - economic_role: None, - settlement_pattern: None, - industrial_corridor: None, - notes: "asteroid belt".into(), - }); - orbit += 1; - } - - // Gas giant (with up to 2 moons as default) - if db_gas_giant { - let letter = (b'b' + orbit as u8 - 1) as char; - let gg_id = if is_binary { - format!("{}A{}", sid, letter) - } else { - format!("{}{}", sid, letter) - }; - bodies.push(ProposalBody { - body_id: gg_id.clone(), - body_type: "gas_giant".into(), - orbit_index: orbit, - parent_body_id: None, - inhabited: false, - proper_name: None, - population: None, - mass_class: Some("gas_giant".into()), - surface_gravity: None, - orbital_period_days: Some(4300.0), - rotation_period_hours: Some(10.0), - atmosphere: Some("dense".into()), - biome_summary: None, - hydrosphere: None, - economic_role: None, - settlement_pattern: None, - industrial_corridor: None, - notes: "gas giant".into(), - }); - // 2 default moons - for m in 1..=2 { - bodies.push(ProposalBody { - body_id: format!("{}-{}", gg_id, m), - body_type: "moon".into(), - orbit_index: m, - parent_body_id: Some(gg_id.clone()), - inhabited: false, - proper_name: None, - population: None, - mass_class: Some("dwarf".into()), - surface_gravity: Some(0.1), - orbital_period_days: Some(3.5 * m as f64), - rotation_period_hours: Some(3.5 * 24.0 * m as f64), - atmosphere: Some("none".into()), - biome_summary: Some("barren".into()), - hydrosphere: None, - economic_role: None, - settlement_pattern: None, - industrial_corridor: None, - notes: format!("moon {} of gas giant", m), - }); - } - orbit += 1; - } - - // Oort cloud (always) - let oort_id = format!("{}-oort", sid); - bodies.push(ProposalBody { - body_id: oort_id.clone(), - body_type: "oort_cloud".into(), - orbit_index: orbit, - parent_body_id: None, - inhabited: false, - proper_name: None, - population: None, - mass_class: None, - surface_gravity: None, - orbital_period_days: None, - rotation_period_hours: None, - atmosphere: None, - biome_summary: None, - hydrosphere: None, - economic_role: None, - settlement_pattern: None, - industrial_corridor: None, - notes: "oort cloud".into(), - }); - - // Horizon station - if has_horizon { - stations.push(ProposalStation { - station_id: format!("{}-oort-S1", sid), - proper_name: None, - orbits_body_id: oort_id, - station_type: "horizon".into(), - population: None, - economic_role: Some("transit".into()), - docking_class: Some("major".into()), - has_gate_infrastructure: true, - notes: "horizon station — gate infrastructure".into(), - }); - } - - (bodies, stations) -} - -fn cmd_author(conn: &Connection, system_id: &str, outdir: &str, wiki_dir: &str) { - // Get system data from DB - let sys = conn - .query_row( - "SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class, - s.habitable_planet_count, s.inhabited_planet_count, - s.asteroid_belt, s.gas_giant, - g.horizon_station - FROM star_systems s - LEFT JOIN system_gates g ON s.system_id = g.system_id - WHERE s.system_id = ?1", - params![system_id], - |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, Option>(1)?, - row.get::<_, Option>(2)?, - row.get::<_, Option>(3)?, - row.get::<_, Option>(4)?, - row.get::<_, Option>(5)?, - row.get::<_, Option>(6)?, - row.get::<_, Option>(7)?, - row.get::<_, Option>(8)?, - )) - }, - ) - .unwrap_or_else(|_| { - eprintln!("error: system '{}' not found", system_id); - process::exit(1); - }); - - let (sid, proper_name, star_type, spectral, db_hab, db_inh, db_belt, db_gg, db_horizon) = sys; - - // Parse wiki for body info - let (raw_line, wiki_hab, wiki_inh) = parse_wiki_bodies_line(wiki_dir, &sid); - - // Use wiki data preferentially, fall back to DB - let hab = if wiki_hab > 0 { - wiki_hab - } else { - db_hab.unwrap_or(0) - }; - let inh = if wiki_inh > 0 { - wiki_inh - } else { - db_inh.unwrap_or(0) - }; - let has_belt = db_belt.unwrap_or(0) != 0; - let has_gg = db_gg.unwrap_or(0) != 0; - let has_horizon = db_horizon.unwrap_or(0) != 0; - - let wiki_data = WikiData { - habitable_count: hab, - inhabited_count: inh, - has_gas_giant: has_gg, - has_asteroid_belt: has_belt, - has_horizon_station: has_horizon, - raw_bodies_line: raw_line, - }; - - let (bodies, stations) = generate_body_matrix( - &sid, - star_type.as_deref(), - spectral.as_deref(), - hab, - inh, - has_gg, - has_belt, - has_horizon, - ); - - let proposal = SystemProposal { - system_id: sid.clone(), - proper_name, - star_type, - spectral_class: spectral, - wiki_data, - bodies, - stations, - }; - - // Write proposal JSON - let sid_compact = sid.replace(' ', ""); - std::fs::create_dir_all(outdir).unwrap(); - let path = std::path::Path::new(outdir).join(format!("{}.json", sid_compact)); - let json = serde_json::to_string_pretty(&proposal).unwrap(); - std::fs::write(&path, &json).unwrap(); - - eprintln!("Proposal written to {}", path.display()); - println!("{}", json); -} - -fn cmd_commit_system(conn: &Connection, path: &str) { - let content = std::fs::read_to_string(path).unwrap_or_else(|e| { - eprintln!("error: cannot read {}: {}", path, e); - process::exit(1); - }); - let proposal: SystemProposal = serde_json::from_str(&content).unwrap_or_else(|e| { - eprintln!("error: invalid proposal JSON: {}", e); - process::exit(1); - }); - - // Check for existing bodies - let existing: i64 = conn - .query_row( - "SELECT COUNT(*) FROM bodies WHERE system_id = ?1", - params![proposal.system_id], - |r| r.get(0), - ) - .unwrap(); - - if existing > 0 { - eprintln!( - "error: system '{}' already has {} bodies. Use wipe-system first.", - proposal.system_id, existing - ); - process::exit(1); - } - - let tx = conn.unchecked_transaction().unwrap(); - - for body in &proposal.bodies { - tx.execute( - "INSERT INTO bodies (body_id, system_id, parent_body_id, body_type, orbit_index, - proper_name, inhabited, population, mass_class, surface_gravity, - orbital_period_days, rotation_period_hours, atmosphere, - biome_summary, hydrosphere, economic_role, settlement_pattern, - industrial_corridor) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", - params![ - body.body_id, - proposal.system_id, - body.parent_body_id, - body.body_type, - body.orbit_index, - body.proper_name, - body.inhabited as i32, - body.population.unwrap_or(0), - body.mass_class, - body.surface_gravity, - body.orbital_period_days, - body.rotation_period_hours, - body.atmosphere, - body.biome_summary, - body.hydrosphere, - body.economic_role, - body.settlement_pattern, - body.industrial_corridor, - ], - ) - .unwrap_or_else(|e| { - eprintln!("error inserting body '{}': {}", body.body_id, e); - process::exit(1); - }); - } - - for station in &proposal.stations { - tx.execute( - "INSERT INTO stations (station_id, system_id, orbits_body_id, station_type, - proper_name, population, economic_role, docking_class, - has_gate_infrastructure) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - params![ - station.station_id, - proposal.system_id, - station.orbits_body_id, - station.station_type, - station.proper_name, - station.population.unwrap_or(0), - station.economic_role, - station.docking_class, - station.has_gate_infrastructure as i32, - ], - ) - .unwrap_or_else(|e| { - eprintln!("error inserting station '{}': {}", station.station_id, e); - process::exit(1); - }); - } - - // Update star_systems counts from the actual body data - let habitable_count: i32 = proposal - .bodies - .iter() - .filter(|b| { - matches!( - b.atmosphere.as_deref(), - Some("breathable") | Some("standard") - ) && (b.body_type == "planet" || b.body_type == "moon") - }) - .count() as i32; - - let inhabited_count: i32 = proposal.bodies.iter().filter(|b| b.inhabited).count() as i32 - + proposal - .stations - .iter() - .filter(|s| s.population.unwrap_or(0) > 0 || s.station_type == "horizon") - .count() as i32; - - let has_gas_giant: i32 = proposal.bodies.iter().any(|b| b.body_type == "gas_giant") as i32; - - let has_belt: i32 = proposal - .bodies - .iter() - .any(|b| b.body_type == "asteroid_belt") as i32; - - tx.execute( - "UPDATE star_systems SET habitable_planet_count = ?1, inhabited_planet_count = ?2, - gas_giant = ?3, asteroid_belt = ?4 - WHERE system_id = ?5", - params![ - habitable_count, - inhabited_count, - has_gas_giant, - has_belt, - proposal.system_id, - ], - ) - .unwrap(); - - tx.commit().unwrap(); - - #[derive(Serialize)] - struct CommitResult { - system_id: String, - bodies_created: usize, - stations_created: usize, - habitable_count: i32, - inhabited_count: i32, - } - let result = CommitResult { - system_id: proposal.system_id, - bodies_created: proposal.bodies.len(), - stations_created: proposal.stations.len(), - habitable_count, - inhabited_count, - }; - println!("{}", serde_json::to_string_pretty(&result).unwrap()); -} - -fn cmd_wipe_system(conn: &Connection, system_id: &str) { - let stations_deleted: usize = conn - .execute( - "DELETE FROM stations WHERE system_id = ?1", - params![system_id], - ) - .unwrap(); - let bodies_deleted: usize = conn - .execute( - "DELETE FROM bodies WHERE system_id = ?1", - params![system_id], - ) - .unwrap(); - - #[derive(Serialize)] - struct WipeResult { - system_id: String, - bodies_deleted: usize, - stations_deleted: usize, - } - let result = WipeResult { - system_id: system_id.to_string(), - bodies_deleted, - stations_deleted, - }; - println!("{}", serde_json::to_string_pretty(&result).unwrap()); -} - -fn cmd_list_systems( - conn: &Connection, - sector: Option<&str>, - hop: Option, - finished: bool, - unfinished: bool, -) { - // Build query dynamically based on filters - let mut conditions: Vec = Vec::new(); - let mut param_values: Vec> = Vec::new(); - let mut idx = 1; - - if let Some(s) = sector { - conditions.push(format!("s.geographic_sector = ?{idx}")); - param_values.push(Box::new(s.to_string())); - idx += 1; - } - if let Some(h) = hop { - conditions.push(format!("g.hop_distance_from_gateway = ?{idx}")); - param_values.push(Box::new(h)); - idx += 1; - } - let _ = idx; // suppress unused warning - if finished { - conditions.push("s.system_id IN (SELECT DISTINCT system_id FROM bodies)".to_string()); - } - if unfinished { - conditions.push("s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)".to_string()); - } - - let where_clause = if conditions.is_empty() { - String::new() - } else { - format!(" WHERE {}", conditions.join(" AND ")) - }; - - let sql = format!( - "SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class, - g.gate_topology, s.geographic_sector, - g.hop_distance_from_gateway, - s.habitable_planet_count, s.inhabited_planet_count, - e.population - FROM star_systems s - JOIN system_gates g ON s.system_id = g.system_id - LEFT JOIN system_economy e ON s.system_id = e.system_id - {where_clause} - ORDER BY g.hop_distance_from_gateway, s.system_id" - ); - - let mut stmt = conn.prepare(&sql).unwrap(); - let params_refs: Vec<&dyn rusqlite::types::ToSql> = - param_values.iter().map(|p| p.as_ref()).collect(); - - #[derive(Serialize)] - struct ListSystem { - system_id: String, - proper_name: Option, - star_type: Option, - spectral_class: Option, - gate_topology: Option, - geographic_sector: Option, - hop_distance: Option, - habitable_planet_count: Option, - inhabited_planet_count: Option, - population: Option, - } - - let systems: Vec = stmt - .query_map(params_refs.as_slice(), |row| { - Ok(ListSystem { - system_id: row.get(0)?, - proper_name: row.get(1)?, - star_type: row.get(2)?, - spectral_class: row.get(3)?, - gate_topology: row.get(4)?, - geographic_sector: row.get(5)?, - hop_distance: row.get(6)?, - habitable_planet_count: row.get(7)?, - inhabited_planet_count: row.get(8)?, - population: row.get(9)?, - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect(); - - #[derive(Serialize)] - struct ListResult { - count: usize, - #[serde(skip_serializing_if = "Option::is_none")] - sector: Option, - #[serde(skip_serializing_if = "Option::is_none")] - hop: Option, - systems: Vec, - } - - let result = ListResult { - count: systems.len(), - sector: sector.map(|s| s.to_string()), - hop, - systems, - }; - println!("{}", serde_json::to_string_pretty(&result).unwrap()); -} - -fn cmd_unfinished(conn: &Connection, sector: Option<&str>) { - let sql = if sector.is_some() { - "SELECT s.system_id, s.proper_name, s.star_type - FROM star_systems s - WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) - AND s.geographic_sector = ?1 - ORDER BY s.system_id" - } else { - "SELECT s.system_id, s.proper_name, s.star_type - FROM star_systems s - WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) - ORDER BY s.system_id" - }; - - let mut stmt = conn.prepare(sql).unwrap(); - - #[derive(Serialize)] - struct UnfinishedSystem { - system_id: String, - proper_name: Option, - star_type: Option, - } - - let rows: Vec = if let Some(s) = sector { - stmt.query_map(params![s], |row| { - Ok(UnfinishedSystem { - system_id: row.get(0)?, - proper_name: row.get(1)?, - star_type: row.get(2)?, - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect() - } else { - stmt.query_map([], |row| { - Ok(UnfinishedSystem { - system_id: row.get(0)?, - proper_name: row.get(1)?, - star_type: row.get(2)?, - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect() - }; - - #[derive(Serialize)] - struct UnfinishedResult { - count: usize, - sector: Option, - systems: Vec, - } - let result = UnfinishedResult { - count: rows.len(), - sector: sector.map(|s| s.to_string()), - systems: rows, - }; - println!("{}", serde_json::to_string_pretty(&result).unwrap()); -} - -fn cmd_next(conn: &Connection, hop: Option, sector: Option<&str>) { - // Find the target hop — either specified or the lowest with unfinished systems - let target_hop: i32 = if let Some(h) = hop { - h - } else { - let sql = if sector.is_some() { - "SELECT MIN(g.hop_distance_from_gateway) - FROM star_systems s - JOIN system_gates g ON s.system_id = g.system_id - WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) - AND s.geographic_sector = ?1" - } else { - "SELECT MIN(g.hop_distance_from_gateway) - FROM star_systems s - JOIN system_gates g ON s.system_id = g.system_id - WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)" - }; - if let Some(s) = sector { - conn.query_row(sql, params![s], |r| r.get::<_, Option>(0)) - } else { - conn.query_row(sql, [], |r| r.get::<_, Option>(0)) - } - .unwrap() - .unwrap_or(-1) - }; - - if target_hop < 0 { - println!( - r#"{{"hop": null, "count": 0, "systems": [], "message": "all systems have bodies"}}"# - ); - return; - } - - #[derive(Serialize)] - struct NextSystem { - system_id: String, - proper_name: Option, - star_type: Option, - spectral_class: Option, - gate_topology: Option, - geographic_sector: Option, - habitable_planet_count: Option, - inhabited_planet_count: Option, - population: Option, - } - - let sql = if sector.is_some() { - "SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class, - g.gate_topology, s.geographic_sector, - s.habitable_planet_count, s.inhabited_planet_count, - e.population - FROM star_systems s - JOIN system_gates g ON s.system_id = g.system_id - LEFT JOIN system_economy e ON s.system_id = e.system_id - WHERE g.hop_distance_from_gateway = ?1 - AND s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) - AND s.geographic_sector = ?2 - ORDER BY s.system_id" - } else { - "SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class, - g.gate_topology, s.geographic_sector, - s.habitable_planet_count, s.inhabited_planet_count, - e.population - FROM star_systems s - JOIN system_gates g ON s.system_id = g.system_id - LEFT JOIN system_economy e ON s.system_id = e.system_id - WHERE g.hop_distance_from_gateway = ?1 - AND s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) - ORDER BY s.system_id" - }; - - let mut stmt = conn.prepare(sql).unwrap(); - - let systems: Vec = if let Some(s) = sector { - stmt.query_map(params![target_hop, s], |row| { - Ok(NextSystem { - system_id: row.get(0)?, - proper_name: row.get(1)?, - star_type: row.get(2)?, - spectral_class: row.get(3)?, - gate_topology: row.get(4)?, - geographic_sector: row.get(5)?, - habitable_planet_count: row.get(6)?, - inhabited_planet_count: row.get(7)?, - population: row.get(8)?, - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect() - } else { - stmt.query_map(params![target_hop], |row| { - Ok(NextSystem { - system_id: row.get(0)?, - proper_name: row.get(1)?, - star_type: row.get(2)?, - spectral_class: row.get(3)?, - gate_topology: row.get(4)?, - geographic_sector: row.get(5)?, - habitable_planet_count: row.get(6)?, - inhabited_planet_count: row.get(7)?, - population: row.get(8)?, - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect() - }; - - #[derive(Serialize)] - struct NextResult { - hop: i32, - sector: Option, - count: usize, - systems: Vec, - } - - let result = NextResult { - hop: target_hop, - sector: sector.map(|s| s.to_string()), - count: systems.len(), - systems, - }; - println!("{}", serde_json::to_string_pretty(&result).unwrap()); -} - -fn cmd_sync_wiki(conn: &Connection, system_id: Option<&str>, wiki_dir: &str) { - // Get list of systems to sync - let system_ids: Vec = if let Some(sid) = system_id { - vec![sid.to_string()] - } else { - let mut stmt = conn - .prepare("SELECT DISTINCT system_id FROM bodies ORDER BY system_id") - .unwrap(); - stmt.query_map([], |row| row.get::<_, String>(0)) - .unwrap() - .filter_map(|r| r.ok()) - .collect() - }; - - let mut synced = 0; - - for sid in &system_ids { - let sid_slug = sid.replace(' ', "-"); - let wiki_path = std::path::Path::new(wiki_dir) - .join(&sid_slug) - .join("index.md"); - if !wiki_path.exists() { - eprintln!("skip: {} — no wiki page at {}", sid, wiki_path.display()); - continue; - } - - // Query bodies - let mut body_stmt = conn - .prepare( - "SELECT body_id, proper_name, body_type, orbit_index, parent_body_id, - inhabited, population, mass_class, atmosphere, surface_gravity, - orbital_period_days, rotation_period_hours, - biome_summary, hydrosphere, economic_role, settlement_pattern, - industrial_corridor - FROM bodies WHERE system_id = ?1 - ORDER BY CASE WHEN parent_body_id IS NULL THEN orbit_index ELSE 1000 + orbit_index END", - ) - .unwrap(); - - struct Body { - id: String, - name: Option, - btype: String, - orbit: i32, - parent: Option, - inhabited: bool, - population: i64, - mass_class: Option, - atmosphere: Option, - gravity: Option, - orbital_days: Option, - rotation_hours: Option, - biome: Option, - hydro: Option, - econ: Option, - settlement: Option, - industrial: Option, - } - - let bodies: Vec = body_stmt - .query_map(params![sid], |row| { - Ok(Body { - id: row.get(0)?, - name: row.get(1)?, - btype: row.get(2)?, - orbit: row.get::<_, Option>(3)?.unwrap_or(0), - parent: row.get(4)?, - inhabited: row.get::<_, i32>(5)? != 0, - population: row.get::<_, Option>(6)?.unwrap_or(0), - mass_class: row.get(7)?, - atmosphere: row.get(8)?, - gravity: row.get(9)?, - orbital_days: row.get(10)?, - rotation_hours: row.get(11)?, - biome: row.get(12)?, - hydro: row.get(13)?, - econ: row.get(14)?, - settlement: row.get(15)?, - industrial: row.get(16)?, - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect(); - - // Query stations - let mut station_stmt = conn - .prepare( - "SELECT station_id, proper_name, orbits_body_id, station_type, - population, economic_role, governance_type, docking_class, - has_gate_infrastructure, district_count - FROM stations WHERE system_id = ?1 - ORDER BY station_id", - ) - .unwrap(); - - struct Station { - id: String, - name: Option, - orbits: Option, - stype: String, - population: i64, - econ: Option, - governance: Option, - docking: Option, - gate: bool, - districts: i32, - } - - let stations: Vec = station_stmt - .query_map(params![sid], |row| { - Ok(Station { - id: row.get(0)?, - name: row.get(1)?, - orbits: row.get(2)?, - stype: row.get(3)?, - population: row.get::<_, Option>(4)?.unwrap_or(0), - econ: row.get(5)?, - governance: row.get(6)?, - docking: row.get(7)?, - gate: row.get::<_, Option>(8)?.unwrap_or(0) != 0, - districts: row.get::<_, Option>(9)?.unwrap_or(1), - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect(); - - if bodies.is_empty() && stations.is_empty() { - continue; - } - - // Build the Celestial Bodies section - let mut section = String::new(); - section.push_str("## Celestial Bodies\n"); - section - .push_str("\n\n"); - - // Bodies table - if !bodies.is_empty() { - section.push_str("| Orbit | ID | Name | Type | Inhabited | Pop | Mass | Gravity | Year (d) | Day (h) | Atmo | Biome | Hydro | Economy | Settlement | Industrial |\n"); - section.push_str("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n"); - - // First pass: top-level bodies (no parent) - for b in &bodies { - if b.parent.is_some() { - continue; - } - let name = b.name.as_deref().unwrap_or("—"); - let pop = if b.population > 0 { - format_population(b.population) - } else { - "—".to_string() - }; - let grav = b - .gravity - .map(|g| format!("{:.2}g", g)) - .unwrap_or_else(|| "—".to_string()); - let year = b - .orbital_days - .map(|d| format!("{:.0}", d)) - .unwrap_or_else(|| "—".to_string()); - let day = b - .rotation_hours - .map(|h| format!("{:.1}", h)) - .unwrap_or_else(|| "—".to_string()); - section.push_str(&format!( - "| {} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", - b.orbit, - b.id, - name, - b.btype, - if b.inhabited { "yes" } else { "no" }, - pop, - b.mass_class.as_deref().unwrap_or("—"), - grav, - year, - day, - b.atmosphere.as_deref().unwrap_or("—"), - b.biome.as_deref().unwrap_or("—"), - b.hydro.as_deref().unwrap_or("—"), - b.econ.as_deref().unwrap_or("—"), - b.settlement.as_deref().unwrap_or("—"), - b.industrial.as_deref().unwrap_or("—"), - )); - - // Child bodies (moons) - for m in &bodies { - if m.parent.as_deref() == Some(&b.id) { - let mname = m.name.as_deref().unwrap_or("—"); - let mpop = if m.population > 0 { - format_population(m.population) - } else { - "—".to_string() - }; - let mgrav = m - .gravity - .map(|g| format!("{:.2}g", g)) - .unwrap_or_else(|| "—".to_string()); - let myear = m - .orbital_days - .map(|d| format!("{:.0}", d)) - .unwrap_or_else(|| "—".to_string()); - let mday = m - .rotation_hours - .map(|h| format!("{:.1}", h)) - .unwrap_or_else(|| "—".to_string()); - section.push_str(&format!( - "| ↳ {}.{} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", - b.orbit, - m.orbit, - m.id, - mname, - m.btype, - if m.inhabited { "yes" } else { "no" }, - mpop, - m.mass_class.as_deref().unwrap_or("—"), - mgrav, - myear, - mday, - m.atmosphere.as_deref().unwrap_or("—"), - m.biome.as_deref().unwrap_or("—"), - m.hydro.as_deref().unwrap_or("—"), - m.econ.as_deref().unwrap_or("—"), - m.settlement.as_deref().unwrap_or("—"), - m.industrial.as_deref().unwrap_or("—"), - )); - } - } - } - section.push('\n'); - } - - // Stations table - if !stations.is_empty() { - section.push_str("### Stations & Facilities\n\n"); - section.push_str("| ID | Name | Type | Orbits | Population | Economy | Governance | Docking | Gate | Districts |\n"); - section.push_str("|---|---|---|---|---|---|---|---|---|---|\n"); - for s in &stations { - let name = s.name.as_deref().unwrap_or("—"); - let orbits = s.orbits.as_deref().unwrap_or("—"); - let pop = if s.population > 0 { - format_population(s.population) - } else { - "—".to_string() - }; - section.push_str(&format!( - "| `{}` | {} | {} | `{}` | {} | {} | {} | {} | {} | {} |\n", - s.id, - name, - s.stype, - orbits, - pop, - s.econ.as_deref().unwrap_or("—"), - s.governance.as_deref().unwrap_or("—"), - s.docking.as_deref().unwrap_or("—"), - if s.gate { "yes" } else { "no" }, - s.districts, - )); - } - section.push('\n'); - } - - // Read current wiki content - let content = std::fs::read_to_string(&wiki_path).unwrap(); - - // Replace or insert the Celestial Bodies section - let marker_start = "## Celestial Bodies"; - let new_content = if let Some(start_pos) = content.find(marker_start) { - // Find the next ## heading after the section (or end of file) - let after = &content[start_pos + marker_start.len()..]; - let end_offset = after - .find("\n## ") - .map(|p| start_pos + marker_start.len() + p + 1) - .unwrap_or(content.len()); - format!( - "{}{}\n{}", - &content[..start_pos], - section.trim_end(), - &content[end_offset..] - ) - } else { - // Insert before ## Topology if it exists, otherwise before end of file - if let Some(topo_pos) = content.find("\n## Topology") { - let insert_pos = topo_pos + 1; // after the newline - format!( - "{}{}\n\n{}", - &content[..insert_pos], - section.trim_end(), - &content[insert_pos..] - ) - } else { - // No topology section — append at end - format!("{}\n{}", content.trim_end(), section) - } - }; - - std::fs::write(&wiki_path, new_content).unwrap(); - eprintln!("synced: {} → {}", sid, wiki_path.display()); - synced += 1; - } - - #[derive(Serialize)] - struct SyncResult { - systems_synced: usize, - } - println!( - "{}", - serde_json::to_string_pretty(&SyncResult { - systems_synced: synced - }) - .unwrap() - ); -} - -fn format_population(pop: i64) -> String { - if pop >= 1_000_000_000 { - format!("{:.1}B", pop as f64 / 1_000_000_000.0) - } else if pop >= 1_000_000 { - format!("{}M", pop / 1_000_000) - } else if pop >= 1_000 { - format!("{}K", pop / 1_000) - } else { - format!("{}", pop) - } -} - -// cmd_populate removed — replaced by per-system author/commit workflow - -fn cmd_corridor_status(conn: &Connection) { - // Query unfinished systems grouped by geographic_sector and hop_distance_from_gateway. - // "Unfinished" = no rows in bodies for this system_id. - // LEFT JOIN so systems with no gate record are still counted (hop = NULL → "?"). - let mut stmt = conn - .prepare( - "SELECT s.geographic_sector, g.hop_distance_from_gateway, COUNT(*) AS remaining - FROM star_systems s - LEFT JOIN system_gates g ON s.system_id = g.system_id - WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) - GROUP BY s.geographic_sector, g.hop_distance_from_gateway - ORDER BY s.geographic_sector, g.hop_distance_from_gateway", - ) - .unwrap(); - - #[derive(Serialize)] - struct CorridorRow { - sector: Option, - hop: Option, - remaining: i64, - } - - let rows: Vec = stmt - .query_map([], |row| { - Ok(CorridorRow { - sector: row.get(0)?, - hop: row.get(1)?, - remaining: row.get(2)?, - }) - }) - .unwrap() - .filter_map(|r| r.ok()) - .collect(); - - if rows.is_empty() { - #[derive(Serialize)] - struct EmptyResult { - total: i64, - message: &'static str, - corridors: Vec<()>, - } - println!( - "{}", - serde_json::to_string_pretty(&EmptyResult { - total: 0, - message: "All systems have bodies authored.", - corridors: vec![], - }) - .unwrap() - ); - return; - } - - let total: i64 = rows.iter().map(|r| r.remaining).sum(); - - #[derive(Serialize)] - struct CorridorResult { - total: i64, - corridors: Vec, - } - println!( - "{}", - serde_json::to_string_pretty(&CorridorResult { - total, - corridors: rows, - }) - .unwrap() - ); -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - -fn main() { - let cli = Cli::parse(); - let db_path = resolve_db_path(cli.db); - let conn = open_db(&db_path); - - match &cli.command { - Commands::ShowSystem { system_id } => cmd_show_system(&conn, system_id), - Commands::ShowBody { body_id } => cmd_show_body(&conn, body_id), - Commands::ShowStation { station_id } => cmd_show_station(&conn, station_id), - Commands::ListBodies { - system, - r#type, - inhabited, - unnamed, - } => cmd_list_bodies( - &conn, - system.as_deref(), - r#type.as_deref(), - *inhabited, - *unnamed, - ), - Commands::ListStations { system, r#type } => { - cmd_list_stations(&conn, system.as_deref(), r#type.as_deref()) - } - cmd @ Commands::AddBody { .. } => cmd_add_body(&conn, cmd), - cmd @ Commands::AddStation { .. } => cmd_add_station(&conn, cmd), - Commands::Stats => cmd_stats(&conn), - Commands::Author { - system_id, - outdir, - wiki, - } => cmd_author(&conn, system_id, outdir, wiki), - Commands::CommitSystem { path } => cmd_commit_system(&conn, path), - Commands::WipeSystem { system_id } => cmd_wipe_system(&conn, system_id), - Commands::ListSystems { - sector, - hop, - finished, - unfinished, - } => cmd_list_systems(&conn, sector.as_deref(), *hop, *finished, *unfinished), - Commands::Unfinished { sector } => cmd_unfinished(&conn, sector.as_deref()), - Commands::Next { hop, sector } => cmd_next(&conn, *hop, sector.as_deref()), - Commands::SyncWiki { system_id, wiki } => cmd_sync_wiki(&conn, system_id.as_deref(), wiki), - Commands::CorridorStatus => cmd_corridor_status(&conn), - } -} diff --git a/server/src/bin/atlas/author.rs b/server/src/bin/atlas/author.rs new file mode 100644 index 000000000..8b79115fd --- /dev/null +++ b/server/src/bin/atlas/author.rs @@ -0,0 +1,640 @@ +use std::process; + +use rusqlite::{params, Connection}; +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Proposal types (owned by the author pipeline) +// --------------------------------------------------------------------------- + +#[derive(Serialize, Deserialize, Clone)] +pub struct ProposalBody { + pub body_id: String, + pub proper_name: Option, + pub body_type: String, + pub orbit_index: i32, + pub parent_body_id: Option, + pub inhabited: bool, + pub population: Option, + pub mass_class: Option, + pub surface_gravity: Option, + pub orbital_period_days: Option, + pub rotation_period_hours: Option, + pub atmosphere: Option, + pub biome_summary: Option, + pub hydrosphere: Option, + pub economic_role: Option, + pub settlement_pattern: Option, + pub industrial_corridor: Option, + pub notes: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ProposalStation { + pub station_id: String, + pub proper_name: Option, + pub orbits_body_id: String, + pub station_type: String, + pub population: Option, + pub economic_role: Option, + pub docking_class: Option, + pub has_gate_infrastructure: bool, + pub notes: String, +} + +#[derive(Serialize, Deserialize)] +pub struct SystemProposal { + pub system_id: String, + pub proper_name: Option, + pub star_type: Option, + pub spectral_class: Option, + pub wiki_data: WikiData, + pub bodies: Vec, + pub stations: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct WikiData { + pub habitable_count: i32, + pub inhabited_count: i32, + pub has_gas_giant: bool, + pub has_asteroid_belt: bool, + pub has_horizon_station: bool, + pub raw_bodies_line: Option, +} + +// --------------------------------------------------------------------------- +// Author pipeline +// --------------------------------------------------------------------------- + +pub fn parse_wiki_bodies_line(wiki_dir: &str, system_id: &str) -> (Option, i32, i32) { + // Try to find the wiki page for this system + let sid_slug = system_id.replace(' ', "-"); + let wiki_path = std::path::Path::new(wiki_dir) + .join(&sid_slug) + .join("index.md"); + if !wiki_path.exists() { + return (None, 0, 0); + } + let content = std::fs::read_to_string(&wiki_path).unwrap_or_default(); + // Look for "| **Bodies** | X habitable · Y inhabited |" + for line in content.lines() { + if line.contains("**Bodies**") { + let raw = line.to_string(); + let mut hab = 0i32; + let mut inh = 0i32; + // Parse "N habitable" + if let Some(pos) = line.find("habitable") { + let before = &line[..pos]; + let parts: Vec<&str> = before.split_whitespace().collect(); + if let Some(n) = parts.last() { + hab = n.parse().unwrap_or(0); + } + } + // Parse "N inhabited" + if let Some(pos) = line.find("inhabited") { + let before = &line[..pos]; + let parts: Vec<&str> = before.split_whitespace().collect(); + if let Some(n) = parts.last() { + inh = n.parse().unwrap_or(0); + } + } + return (Some(raw), hab, inh); + } + } + (None, 0, 0) +} + +pub fn generate_body_matrix( + system_id: &str, + star_type: Option<&str>, + spectral: Option<&str>, + wiki_hab: i32, + wiki_inh: i32, + db_gas_giant: bool, + db_belt: bool, + has_horizon: bool, +) -> (Vec, Vec) { + let sid = system_id.replace(' ', ""); + let mut bodies = Vec::new(); + let mut stations = Vec::new(); + let mut orbit = 1; + + // Determine planet count from star type if wiki doesn't specify + let spectral_char = spectral.and_then(|s| s.chars().next()).unwrap_or('M'); + + // Base planet count by spectral type. + // Sol has 8. TRAPPIST-1 (M-dwarf) has 7. Minimum 6 for any star. + let total_planets = if wiki_hab > 0 || wiki_inh > 0 { + // Wiki has data — use it as the inhabited/habitable core, pad to realistic count + let known = wiki_hab.max(wiki_inh); + match spectral_char { + 'O' | 'B' | 'A' => (known + 4).max(6), // hot stars — fewer but still 6+ + 'F' => (known + 5).max(8), // bright — wide system, 8+ + 'G' => (known + 5).max(8), // sol-like — 8 is baseline + 'K' => (known + 4).max(7), // cooler — 7+ typical + 'M' => (known + 4).max(6), // compact but TRAPPIST-1 has 7 + _ => (known + 4).max(6), + } + } else { + // No wiki data — generate realistic count + match spectral_char { + 'O' | 'B' | 'A' => 6, + 'F' => 8, + 'G' => 8, + 'K' => 7, + 'M' => 6, + _ => 6, + } + }; + + // Determine if binary — affects naming + let is_binary = star_type == Some("binary"); + + // Place inner barren rocky planets + let inner_barren = if total_planets > wiki_inh + 1 { 1 } else { 0 }; + for _ in 0..inner_barren { + let letter = (b'b' + orbit as u8 - 1) as char; + let body_id = if is_binary { + format!("{}A{}", sid, letter) // circumbinary assumed for now + } else { + format!("{}{}", sid, letter) + }; + bodies.push(ProposalBody { + body_id, + body_type: "planet".into(), + orbit_index: orbit, + parent_body_id: None, + inhabited: false, + proper_name: None, + population: None, + mass_class: Some("terrestrial".into()), + surface_gravity: Some(0.38), + orbital_period_days: Some(88.0), + rotation_period_hours: Some(1408.0), + atmosphere: Some("none".into()), + biome_summary: Some("barren".into()), + hydrosphere: None, + economic_role: None, + settlement_pattern: None, + industrial_corridor: None, + notes: "inner rocky, uninhabited".into(), + }); + orbit += 1; + } + + // Place inhabited planets + for i in 0..wiki_inh { + let letter = (b'b' + orbit as u8 - 1) as char; + let body_id = if is_binary { + format!("{}A{}", sid, letter) + } else { + format!("{}{}", sid, letter) + }; + bodies.push(ProposalBody { + body_id, + body_type: "planet".into(), + orbit_index: orbit, + parent_body_id: None, + inhabited: true, + proper_name: None, + population: None, + mass_class: Some("terrestrial".into()), + surface_gravity: Some(0.9), + orbital_period_days: Some(365.0), + rotation_period_hours: Some(24.0), + atmosphere: Some("standard".into()), + biome_summary: Some("temperate".into()), + hydrosphere: None, + economic_role: None, + settlement_pattern: None, + industrial_corridor: None, + notes: format!("inhabited planet {}/{}", i + 1, wiki_inh), + }); + orbit += 1; + } + + // Place habitable-but-uninhabited planets (hab > inh) + let hab_only = (wiki_hab - wiki_inh).max(0); + for _ in 0..hab_only { + let letter = (b'b' + orbit as u8 - 1) as char; + let body_id = if is_binary { + format!("{}A{}", sid, letter) + } else { + format!("{}{}", sid, letter) + }; + bodies.push(ProposalBody { + body_id, + body_type: "planet".into(), + orbit_index: orbit, + parent_body_id: None, + inhabited: false, + proper_name: None, + population: None, + mass_class: Some("terrestrial".into()), + surface_gravity: Some(0.85), + orbital_period_days: Some(400.0), + rotation_period_hours: Some(26.0), + atmosphere: Some("standard".into()), + biome_summary: Some("temperate".into()), + hydrosphere: None, + economic_role: None, + settlement_pattern: None, + industrial_corridor: None, + notes: "habitable, uninhabited".into(), + }); + orbit += 1; + } + + // Outer rocky/ice planets to fill remaining count + let placed = inner_barren + wiki_inh + hab_only; + let remaining_planets = (total_planets - placed).max(0); + for _ in 0..remaining_planets { + let letter = (b'b' + orbit as u8 - 1) as char; + let body_id = if is_binary { + format!("{}A{}", sid, letter) + } else { + format!("{}{}", sid, letter) + }; + bodies.push(ProposalBody { + body_id, + body_type: "planet".into(), + orbit_index: orbit, + parent_body_id: None, + inhabited: false, + proper_name: None, + population: None, + mass_class: Some("terrestrial".into()), + surface_gravity: Some(0.5), + orbital_period_days: Some(2000.0), + rotation_period_hours: Some(18.0), + atmosphere: Some("thin".into()), + biome_summary: Some("frozen".into()), + hydrosphere: None, + economic_role: None, + settlement_pattern: None, + industrial_corridor: None, + notes: "outer rocky/ice, uninhabited".into(), + }); + orbit += 1; + } + + // Asteroid belt + if db_belt { + bodies.push(ProposalBody { + body_id: format!("{}-belt", sid), + body_type: "asteroid_belt".into(), + orbit_index: orbit, + parent_body_id: None, + inhabited: false, + proper_name: None, + population: None, + mass_class: None, + surface_gravity: None, + orbital_period_days: None, + rotation_period_hours: None, + atmosphere: None, + biome_summary: None, + hydrosphere: None, + economic_role: None, + settlement_pattern: None, + industrial_corridor: None, + notes: "asteroid belt".into(), + }); + orbit += 1; + } + + // Gas giant (with up to 2 moons as default) + if db_gas_giant { + let letter = (b'b' + orbit as u8 - 1) as char; + let gg_id = if is_binary { + format!("{}A{}", sid, letter) + } else { + format!("{}{}", sid, letter) + }; + bodies.push(ProposalBody { + body_id: gg_id.clone(), + body_type: "gas_giant".into(), + orbit_index: orbit, + parent_body_id: None, + inhabited: false, + proper_name: None, + population: None, + mass_class: Some("gas_giant".into()), + surface_gravity: None, + orbital_period_days: Some(4300.0), + rotation_period_hours: Some(10.0), + atmosphere: Some("dense".into()), + biome_summary: None, + hydrosphere: None, + economic_role: None, + settlement_pattern: None, + industrial_corridor: None, + notes: "gas giant".into(), + }); + // 2 default moons + for m in 1..=2 { + bodies.push(ProposalBody { + body_id: format!("{}-{}", gg_id, m), + body_type: "moon".into(), + orbit_index: m, + parent_body_id: Some(gg_id.clone()), + inhabited: false, + proper_name: None, + population: None, + mass_class: Some("dwarf".into()), + surface_gravity: Some(0.1), + orbital_period_days: Some(3.5 * m as f64), + rotation_period_hours: Some(3.5 * 24.0 * m as f64), + atmosphere: Some("none".into()), + biome_summary: Some("barren".into()), + hydrosphere: None, + economic_role: None, + settlement_pattern: None, + industrial_corridor: None, + notes: format!("moon {} of gas giant", m), + }); + } + orbit += 1; + } + + // Oort cloud (always) + let oort_id = format!("{}-oort", sid); + bodies.push(ProposalBody { + body_id: oort_id.clone(), + body_type: "oort_cloud".into(), + orbit_index: orbit, + parent_body_id: None, + inhabited: false, + proper_name: None, + population: None, + mass_class: None, + surface_gravity: None, + orbital_period_days: None, + rotation_period_hours: None, + atmosphere: None, + biome_summary: None, + hydrosphere: None, + economic_role: None, + settlement_pattern: None, + industrial_corridor: None, + notes: "oort cloud".into(), + }); + + // Horizon station + if has_horizon { + stations.push(ProposalStation { + station_id: format!("{}-oort-S1", sid), + proper_name: None, + orbits_body_id: oort_id, + station_type: "horizon".into(), + population: None, + economic_role: Some("transit".into()), + docking_class: Some("major".into()), + has_gate_infrastructure: true, + notes: "horizon station — gate infrastructure".into(), + }); + } + + (bodies, stations) +} + +pub fn cmd_author(conn: &Connection, system_id: &str, outdir: &str, wiki_dir: &str) { + // Get system data from DB + let sys = conn + .query_row( + "SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class, + s.habitable_planet_count, s.inhabited_planet_count, + s.asteroid_belt, s.gas_giant, + g.horizon_station + FROM star_systems s + LEFT JOIN system_gates g ON s.system_id = g.system_id + WHERE s.system_id = ?1", + params![system_id], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, Option>(1)?, + row.get::<_, Option>(2)?, + row.get::<_, Option>(3)?, + row.get::<_, Option>(4)?, + row.get::<_, Option>(5)?, + row.get::<_, Option>(6)?, + row.get::<_, Option>(7)?, + row.get::<_, Option>(8)?, + )) + }, + ) + .unwrap_or_else(|_| { + eprintln!("error: system '{}' not found", system_id); + process::exit(1); + }); + + let (sid, proper_name, star_type, spectral, db_hab, db_inh, db_belt, db_gg, db_horizon) = sys; + + // Parse wiki for body info + let (raw_line, wiki_hab, wiki_inh) = parse_wiki_bodies_line(wiki_dir, &sid); + + // Use wiki data preferentially, fall back to DB + let hab = if wiki_hab > 0 { + wiki_hab + } else { + db_hab.unwrap_or(0) + }; + let inh = if wiki_inh > 0 { + wiki_inh + } else { + db_inh.unwrap_or(0) + }; + let has_belt = db_belt.unwrap_or(0) != 0; + let has_gg = db_gg.unwrap_or(0) != 0; + let has_horizon = db_horizon.unwrap_or(0) != 0; + + let wiki_data = WikiData { + habitable_count: hab, + inhabited_count: inh, + has_gas_giant: has_gg, + has_asteroid_belt: has_belt, + has_horizon_station: has_horizon, + raw_bodies_line: raw_line, + }; + + let (bodies, stations) = generate_body_matrix( + &sid, + star_type.as_deref(), + spectral.as_deref(), + hab, + inh, + has_gg, + has_belt, + has_horizon, + ); + + let proposal = SystemProposal { + system_id: sid.clone(), + proper_name, + star_type, + spectral_class: spectral, + wiki_data, + bodies, + stations, + }; + + // Write proposal JSON + let sid_compact = sid.replace(' ', ""); + std::fs::create_dir_all(outdir).unwrap(); + let path = std::path::Path::new(outdir).join(format!("{}.json", sid_compact)); + let json = serde_json::to_string_pretty(&proposal).unwrap(); + std::fs::write(&path, &json).unwrap(); + + eprintln!("Proposal written to {}", path.display()); + println!("{}", json); +} + +pub fn cmd_commit_system(conn: &Connection, path: &str) { + let content = std::fs::read_to_string(path).unwrap_or_else(|e| { + eprintln!("error: cannot read {}: {}", path, e); + process::exit(1); + }); + let proposal: SystemProposal = serde_json::from_str(&content).unwrap_or_else(|e| { + eprintln!("error: invalid proposal JSON: {}", e); + process::exit(1); + }); + + // Check for existing bodies + let existing: i64 = conn + .query_row( + "SELECT COUNT(*) FROM bodies WHERE system_id = ?1", + params![proposal.system_id], + |r| r.get(0), + ) + .unwrap(); + + if existing > 0 { + eprintln!( + "error: system '{}' already has {} bodies. Use wipe-system first.", + proposal.system_id, existing + ); + process::exit(1); + } + + let tx = conn.unchecked_transaction().unwrap(); + + for body in &proposal.bodies { + tx.execute( + "INSERT INTO bodies (body_id, system_id, parent_body_id, body_type, orbit_index, + proper_name, inhabited, population, mass_class, surface_gravity, + orbital_period_days, rotation_period_hours, atmosphere, + biome_summary, hydrosphere, economic_role, settlement_pattern, + industrial_corridor) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", + params![ + body.body_id, + proposal.system_id, + body.parent_body_id, + body.body_type, + body.orbit_index, + body.proper_name, + body.inhabited as i32, + body.population.unwrap_or(0), + body.mass_class, + body.surface_gravity, + body.orbital_period_days, + body.rotation_period_hours, + body.atmosphere, + body.biome_summary, + body.hydrosphere, + body.economic_role, + body.settlement_pattern, + body.industrial_corridor, + ], + ) + .unwrap_or_else(|e| { + eprintln!("error inserting body '{}': {}", body.body_id, e); + process::exit(1); + }); + } + + for station in &proposal.stations { + tx.execute( + "INSERT INTO stations (station_id, system_id, orbits_body_id, station_type, + proper_name, population, economic_role, docking_class, + has_gate_infrastructure) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + params![ + station.station_id, + proposal.system_id, + station.orbits_body_id, + station.station_type, + station.proper_name, + station.population.unwrap_or(0), + station.economic_role, + station.docking_class, + station.has_gate_infrastructure as i32, + ], + ) + .unwrap_or_else(|e| { + eprintln!("error inserting station '{}': {}", station.station_id, e); + process::exit(1); + }); + } + + // Update star_systems counts from the actual body data + let habitable_count: i32 = proposal + .bodies + .iter() + .filter(|b| { + matches!( + b.atmosphere.as_deref(), + Some("breathable") | Some("standard") + ) && (b.body_type == "planet" || b.body_type == "moon") + }) + .count() as i32; + + let inhabited_count: i32 = proposal.bodies.iter().filter(|b| b.inhabited).count() as i32 + + proposal + .stations + .iter() + .filter(|s| s.population.unwrap_or(0) > 0 || s.station_type == "horizon") + .count() as i32; + + let has_gas_giant: i32 = proposal.bodies.iter().any(|b| b.body_type == "gas_giant") as i32; + + let has_belt: i32 = proposal + .bodies + .iter() + .any(|b| b.body_type == "asteroid_belt") as i32; + + tx.execute( + "UPDATE star_systems SET habitable_planet_count = ?1, inhabited_planet_count = ?2, + gas_giant = ?3, asteroid_belt = ?4 + WHERE system_id = ?5", + params![ + habitable_count, + inhabited_count, + has_gas_giant, + has_belt, + proposal.system_id, + ], + ) + .unwrap(); + + tx.commit().unwrap(); + + #[derive(Serialize)] + struct CommitResult { + system_id: String, + bodies_created: usize, + stations_created: usize, + habitable_count: i32, + inhabited_count: i32, + } + let result = CommitResult { + system_id: proposal.system_id, + bodies_created: proposal.bodies.len(), + stations_created: proposal.stations.len(), + habitable_count, + inhabited_count, + }; + println!("{}", serde_json::to_string_pretty(&result).unwrap()); +} diff --git a/server/src/bin/atlas/common.rs b/server/src/bin/atlas/common.rs new file mode 100644 index 000000000..1b305f7e4 --- /dev/null +++ b/server/src/bin/atlas/common.rs @@ -0,0 +1,244 @@ +use std::path::PathBuf; +use std::process; + +use rusqlite::{params, Connection}; +use serde::Serialize; + +// --------------------------------------------------------------------------- +// Shared output types +// --------------------------------------------------------------------------- + +#[derive(Serialize)] +pub struct BodyRow { + pub body_id: String, + pub system_id: String, + pub parent_body_id: Option, + pub body_type: String, + pub orbit_index: Option, + pub proper_name: Option, + pub mass_class: Option, + pub atmosphere: Option, + pub surface_gravity: Option, + pub biome_summary: Option, + pub hydrosphere: Option, + pub inhabited: bool, + pub population: i64, + pub economic_role: Option, + pub cultural_corridor: Option, + pub industrial_corridor: Option, +} + +#[derive(Serialize)] +pub struct StationRow { + pub station_id: String, + pub system_id: String, + pub orbits_body_id: Option, + pub station_type: String, + pub proper_name: Option, + pub population: i64, + pub economic_role: Option, + pub governance_type: Option, + pub docking_class: Option, + pub has_gate_infrastructure: bool, + pub district_count: i32, +} + +#[derive(Serialize)] +pub struct SystemSummary { + pub system_id: String, + pub proper_name: Option, + pub star_type: Option, + pub geographic_sector: Option, + pub habitable_planet_count: Option, + pub inhabited_planet_count: Option, + pub bodies: Vec, + pub stations: Vec, +} + +// --------------------------------------------------------------------------- +// Database helpers +// --------------------------------------------------------------------------- + +pub fn resolve_db_path(explicit: Option) -> PathBuf { + if let Some(p) = explicit { + return p; + } + // Walk up from CWD looking for server/data/systems.db + let mut dir = std::env::current_dir().expect("Cannot determine CWD"); + loop { + let candidate = dir.join("server").join("data").join("systems.db"); + if candidate.exists() { + return candidate; + } + if !dir.pop() { + break; + } + } + eprintln!("error: cannot find server/data/systems.db — pass --db explicitly"); + process::exit(1); +} + +pub fn open_db(path: &PathBuf) -> Connection { + let conn = Connection::open(path).unwrap_or_else(|e| { + eprintln!("error: cannot open {}: {}", path.display(), e); + process::exit(1); + }); + conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;") + .unwrap(); + conn +} + +// --------------------------------------------------------------------------- +// Shared query helpers +// --------------------------------------------------------------------------- + +pub fn query_bodies( + conn: &Connection, + system: Option<&str>, + body_type: Option<&str>, + inhabited_only: bool, + unnamed_only: bool, +) -> Vec { + let mut sql = String::from( + "SELECT body_id, system_id, parent_body_id, body_type, orbit_index, + proper_name, mass_class, atmosphere, surface_gravity, + biome_summary, hydrosphere, inhabited, population, + economic_role, cultural_corridor, industrial_corridor + FROM bodies WHERE 1=1", + ); + let mut param_values: Vec> = Vec::new(); + + if let Some(s) = system { + sql.push_str(" AND system_id = ?"); + param_values.push(Box::new(s.to_string())); + } + if let Some(t) = body_type { + sql.push_str(" AND body_type = ?"); + param_values.push(Box::new(t.to_string())); + } + if inhabited_only { + sql.push_str(" AND inhabited = 1"); + } + if unnamed_only { + sql.push_str(" AND proper_name IS NULL"); + } + sql.push_str(" ORDER BY system_id, orbit_index"); + + let params_ref: Vec<&dyn rusqlite::types::ToSql> = + param_values.iter().map(|p| p.as_ref()).collect(); + let mut stmt = conn.prepare(&sql).unwrap(); + stmt.query_map(params_ref.as_slice(), |row| { + Ok(BodyRow { + body_id: row.get(0)?, + system_id: row.get(1)?, + parent_body_id: row.get(2)?, + body_type: row.get(3)?, + orbit_index: row.get(4)?, + proper_name: row.get(5)?, + mass_class: row.get(6)?, + atmosphere: row.get(7)?, + surface_gravity: row.get(8)?, + biome_summary: row.get(9)?, + hydrosphere: row.get(10)?, + inhabited: row.get::<_, i32>(11)? != 0, + population: row.get::<_, Option>(12)?.unwrap_or(0), + economic_role: row.get(13)?, + cultural_corridor: row.get(14)?, + industrial_corridor: row.get(15)?, + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect() +} + +pub fn query_stations( + conn: &Connection, + system: Option<&str>, + station_type: Option<&str>, +) -> Vec { + let mut sql = String::from( + "SELECT station_id, system_id, orbits_body_id, station_type, + proper_name, population, economic_role, governance_type, + docking_class, has_gate_infrastructure, district_count + FROM stations WHERE 1=1", + ); + let mut param_values: Vec> = Vec::new(); + + if let Some(s) = system { + sql.push_str(" AND system_id = ?"); + param_values.push(Box::new(s.to_string())); + } + if let Some(t) = station_type { + sql.push_str(" AND station_type = ?"); + param_values.push(Box::new(t.to_string())); + } + sql.push_str(" ORDER BY system_id, station_id"); + + let params_ref: Vec<&dyn rusqlite::types::ToSql> = + param_values.iter().map(|p| p.as_ref()).collect(); + let mut stmt = conn.prepare(&sql).unwrap(); + stmt.query_map(params_ref.as_slice(), |row| { + Ok(StationRow { + station_id: row.get(0)?, + system_id: row.get(1)?, + orbits_body_id: row.get(2)?, + station_type: row.get(3)?, + proper_name: row.get(4)?, + population: row.get::<_, Option>(5)?.unwrap_or(0), + economic_role: row.get(6)?, + governance_type: row.get(7)?, + docking_class: row.get(8)?, + has_gate_infrastructure: row.get::<_, Option>(9)?.unwrap_or(0) != 0, + district_count: row.get::<_, Option>(10)?.unwrap_or(1), + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect() +} + +pub fn query_stations_for_body(conn: &Connection, body_id: &str) -> Vec { + let mut stmt = conn + .prepare( + "SELECT station_id, system_id, orbits_body_id, station_type, + proper_name, population, economic_role, governance_type, + docking_class, has_gate_infrastructure, district_count + FROM stations WHERE orbits_body_id = ?1 ORDER BY station_id", + ) + .unwrap(); + stmt.query_map(params![body_id], |row| { + Ok(StationRow { + station_id: row.get(0)?, + system_id: row.get(1)?, + orbits_body_id: row.get(2)?, + station_type: row.get(3)?, + proper_name: row.get(4)?, + population: row.get::<_, Option>(5)?.unwrap_or(0), + economic_role: row.get(6)?, + governance_type: row.get(7)?, + docking_class: row.get(8)?, + has_gate_infrastructure: row.get::<_, Option>(9)?.unwrap_or(0) != 0, + district_count: row.get::<_, Option>(10)?.unwrap_or(1), + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect() +} + +// --------------------------------------------------------------------------- +// Shared formatting +// --------------------------------------------------------------------------- + +pub fn format_population(pop: i64) -> String { + if pop >= 1_000_000_000 { + format!("{:.1}B", pop as f64 / 1_000_000_000.0) + } else if pop >= 1_000_000 { + format!("{}M", pop / 1_000_000) + } else if pop >= 1_000 { + format!("{}K", pop / 1_000) + } else { + format!("{}", pop) + } +} diff --git a/server/src/bin/atlas/main.rs b/server/src/bin/atlas/main.rs new file mode 100644 index 000000000..fd35bf9fc --- /dev/null +++ b/server/src/bin/atlas/main.rs @@ -0,0 +1,275 @@ +//! Atlas CLI — query and manage celestial bodies and stations in systems.db. +//! +//! # Usage +//! +//! ```sh +//! # Via wrapper script (recommended): +//! tooling/atlas list-bodies --system "GJ 15A" +//! tooling/atlas list-bodies --type planet --inhabited +//! tooling/atlas show-system "GJ 15A" +//! tooling/atlas show-body "GJ 15Ab" +//! tooling/atlas add-body --system "GJ 15A" --type planet --orbit 1 --id "GJ 15Ab" +//! tooling/atlas stats +//! tooling/atlas corridor-status # remaining systems by corridor/hop +//! tooling/atlas populate # bulk classifier pass +//! +//! # Direct: +//! cargo run --bin atlas -- [args] +//! ``` + +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +mod author; +mod common; +mod mutate; +mod show; +mod stats; +mod sync_wiki; +mod systems; + +use common::{open_db, resolve_db_path}; + +// --------------------------------------------------------------------------- +// CLI structure +// --------------------------------------------------------------------------- + +#[derive(Parser)] +#[command( + name = "atlas", + about = "Query and manage celestial bodies and stations in systems.db" +)] +struct Cli { + /// Path to the SQLite database (default: auto-detect from worktree) + #[arg(long, global = true)] + db: Option, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Show full system hierarchy (star → bodies → stations) + ShowSystem { + /// System ID (e.g., "GJ 15A") + system_id: String, + }, + /// Show a single body's details + ShowBody { + /// Body ID (e.g., "GJ 15Ab") + body_id: String, + }, + /// Show a single station's details + ShowStation { + /// Station ID (e.g., "GJ 15Ab-S1") + station_id: String, + }, + /// List bodies with optional filters + ListBodies { + #[arg(long)] + system: Option, + #[arg(long, value_name = "TYPE")] + r#type: Option, + #[arg(long)] + inhabited: bool, + #[arg(long)] + unnamed: bool, + }, + /// List stations with optional filters + ListStations { + #[arg(long)] + system: Option, + #[arg(long, value_name = "TYPE")] + r#type: Option, + }, + /// Add a body record + AddBody { + #[arg(long)] + id: String, + #[arg(long)] + system: String, + #[arg(long, value_name = "TYPE")] + r#type: String, + #[arg(long)] + orbit: Option, + #[arg(long)] + name: Option, + #[arg(long)] + parent: Option, + #[arg(long)] + mass_class: Option, + #[arg(long)] + atmosphere: Option, + #[arg(long)] + gravity: Option, + #[arg(long)] + biome: Option, + #[arg(long)] + inhabited: bool, + #[arg(long)] + population: Option, + }, + /// Add a station record + AddStation { + #[arg(long)] + id: String, + #[arg(long)] + system: String, + #[arg(long)] + orbits: Option, + #[arg(long, value_name = "TYPE")] + r#type: String, + #[arg(long)] + name: Option, + #[arg(long)] + population: Option, + #[arg(long)] + docking: Option, + #[arg(long)] + gate: bool, + }, + /// Database statistics + Stats, + /// Generate a body/station proposal for a single system (writes JSON file for review) + Author { + /// System ID (e.g., "GJ 71") + system_id: String, + /// Output directory for proposal JSON (default: docs/atlas/proposals/) + #[arg(long, default_value = "docs/atlas/proposals")] + outdir: String, + /// Path to wiki directory (default: wiki/star-systems/) + #[arg(long, default_value = "wiki/star-systems")] + wiki: String, + }, + /// Commit an approved proposal JSON to the database + CommitSystem { + /// Path to the proposal JSON file + path: String, + }, + /// Wipe all bodies and stations for a single system + WipeSystem { + /// System ID + system_id: String, + }, + /// List systems with optional filters + ListSystems { + /// Filter by geographic sector (e.g., "west_reach", "east_reach") + #[arg(long)] + sector: Option, + /// Filter by hop distance + #[arg(long)] + hop: Option, + /// Only show systems that have bodies authored + #[arg(long)] + finished: bool, + /// Only show systems that have NO bodies yet + #[arg(long)] + unfinished: bool, + }, + /// List systems that have no bodies yet + Unfinished { + /// Filter by geographic sector + #[arg(long)] + sector: Option, + }, + /// List unfinished systems at a specific hop distance (or next available hop) + Next { + /// Hop distance (omit to find the lowest hop with unfinished systems) + hop: Option, + /// Filter by geographic sector + #[arg(long)] + sector: Option, + }, + /// Sync body/station data into wiki page for a system (or all systems) + SyncWiki { + /// System ID (omit for all systems with bodies) + system_id: Option, + /// Path to wiki directory (default: wiki/star-systems/) + #[arg(long, default_value = "wiki/star-systems")] + wiki: String, + }, + /// Show remaining unfinished systems grouped by geographic sector and hop distance + CorridorStatus, +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +fn main() { + let cli = Cli::parse(); + let db_path = resolve_db_path(cli.db); + let conn = open_db(&db_path); + + match &cli.command { + Commands::ShowSystem { system_id } => show::cmd_show_system(&conn, system_id), + Commands::ShowBody { body_id } => show::cmd_show_body(&conn, body_id), + Commands::ShowStation { station_id } => show::cmd_show_station(&conn, station_id), + Commands::ListBodies { + system, + r#type, + inhabited, + unnamed, + } => systems::cmd_list_bodies( + &conn, + system.as_deref(), + r#type.as_deref(), + *inhabited, + *unnamed, + ), + Commands::ListStations { system, r#type } => { + systems::cmd_list_stations(&conn, system.as_deref(), r#type.as_deref()) + } + Commands::AddBody { + id, + system, + r#type, + orbit, + name, + parent, + mass_class, + atmosphere, + gravity, + biome, + inhabited, + population, + } => mutate::cmd_add_body( + &conn, id, system, r#type, *orbit, name, parent, mass_class, atmosphere, *gravity, + biome, *inhabited, *population, + ), + Commands::AddStation { + id, + system, + orbits, + r#type, + name, + population, + docking, + gate, + } => mutate::cmd_add_station( + &conn, id, system, orbits, r#type, name, *population, docking, *gate, + ), + Commands::Stats => stats::cmd_stats(&conn), + Commands::Author { + system_id, + outdir, + wiki, + } => author::cmd_author(&conn, system_id, outdir, wiki), + Commands::CommitSystem { path } => author::cmd_commit_system(&conn, path), + Commands::WipeSystem { system_id } => mutate::cmd_wipe_system(&conn, system_id), + Commands::ListSystems { + sector, + hop, + finished, + unfinished, + } => systems::cmd_list_systems(&conn, sector.as_deref(), *hop, *finished, *unfinished), + Commands::Unfinished { sector } => systems::cmd_unfinished(&conn, sector.as_deref()), + Commands::Next { hop, sector } => systems::cmd_next(&conn, *hop, sector.as_deref()), + Commands::SyncWiki { system_id, wiki } => { + sync_wiki::cmd_sync_wiki(&conn, system_id.as_deref(), wiki) + } + Commands::CorridorStatus => systems::cmd_corridor_status(&conn), + } +} diff --git a/server/src/bin/atlas/mutate.rs b/server/src/bin/atlas/mutate.rs new file mode 100644 index 000000000..080dec933 --- /dev/null +++ b/server/src/bin/atlas/mutate.rs @@ -0,0 +1,109 @@ +use std::process; + +use rusqlite::{params, Connection}; +use serde::Serialize; + +pub fn cmd_add_body( + conn: &Connection, + id: &str, + system: &str, + r#type: &str, + orbit: Option, + name: &Option, + parent: &Option, + mass_class: &Option, + atmosphere: &Option, + gravity: Option, + biome: &Option, + inhabited: bool, + population: Option, +) { + conn.execute( + "INSERT INTO bodies (body_id, system_id, parent_body_id, body_type, orbit_index, + proper_name, mass_class, atmosphere, surface_gravity, + biome_summary, inhabited, population) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + params![ + id, + system, + parent, + r#type, + orbit, + name, + mass_class, + atmosphere, + gravity, + biome, + inhabited as i32, + population.unwrap_or(0), + ], + ) + .unwrap_or_else(|e| { + eprintln!("error: {}", e); + process::exit(1); + }); + + println!(r#"{{"ok": true, "body_id": "{}"}}"#, id); +} + +pub fn cmd_add_station( + conn: &Connection, + id: &str, + system: &str, + orbits: &Option, + r#type: &str, + name: &Option, + population: Option, + docking: &Option, + gate: bool, +) { + conn.execute( + "INSERT INTO stations (station_id, system_id, orbits_body_id, station_type, + proper_name, population, docking_class, has_gate_infrastructure) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + id, + system, + orbits, + r#type, + name, + population.unwrap_or(0), + docking, + gate as i32, + ], + ) + .unwrap_or_else(|e| { + eprintln!("error: {}", e); + process::exit(1); + }); + + println!(r#"{{"ok": true, "station_id": "{}"}}"#, id); +} + +pub fn cmd_wipe_system(conn: &Connection, system_id: &str) { + let stations_deleted: usize = conn + .execute( + "DELETE FROM stations WHERE system_id = ?1", + params![system_id], + ) + .unwrap(); + let bodies_deleted: usize = conn + .execute( + "DELETE FROM bodies WHERE system_id = ?1", + params![system_id], + ) + .unwrap(); + + #[derive(Serialize)] + struct WipeResult { + system_id: String, + bodies_deleted: usize, + stations_deleted: usize, + } + let result = WipeResult { + system_id: system_id.to_string(), + bodies_deleted, + stations_deleted, + }; + println!("{}", serde_json::to_string_pretty(&result).unwrap()); +} diff --git a/server/src/bin/atlas/show.rs b/server/src/bin/atlas/show.rs new file mode 100644 index 000000000..08fbe1877 --- /dev/null +++ b/server/src/bin/atlas/show.rs @@ -0,0 +1,127 @@ +use std::process; + +use rusqlite::{params, Connection}; +use serde::Serialize; + +use crate::common::{ + query_bodies, query_stations, query_stations_for_body, BodyRow, StationRow, SystemSummary, +}; + +pub fn cmd_show_system(conn: &Connection, system_id: &str) { + let mut stmt = conn + .prepare( + "SELECT system_id, proper_name, star_type, geographic_sector, + habitable_planet_count, inhabited_planet_count + FROM star_systems WHERE system_id = ?1", + ) + .unwrap(); + + let sys: Option = stmt + .query_row(params![system_id], |row| { + Ok(SystemSummary { + system_id: row.get(0)?, + proper_name: row.get(1)?, + star_type: row.get(2)?, + geographic_sector: row.get(3)?, + habitable_planet_count: row.get(4)?, + inhabited_planet_count: row.get(5)?, + bodies: Vec::new(), + stations: Vec::new(), + }) + }) + .ok(); + + let Some(mut sys) = sys else { + eprintln!("error: system '{}' not found", system_id); + process::exit(1); + }; + + sys.bodies = query_bodies(conn, Some(system_id), None, false, false); + sys.stations = query_stations(conn, Some(system_id), None); + + println!("{}", serde_json::to_string_pretty(&sys).unwrap()); +} + +pub fn cmd_show_body(conn: &Connection, body_id: &str) { + let row = conn + .query_row( + "SELECT body_id, system_id, parent_body_id, body_type, orbit_index, + proper_name, mass_class, atmosphere, surface_gravity, + biome_summary, hydrosphere, inhabited, population, + economic_role, cultural_corridor, industrial_corridor + FROM bodies WHERE body_id = ?1", + params![body_id], + |row| { + Ok(BodyRow { + body_id: row.get(0)?, + system_id: row.get(1)?, + parent_body_id: row.get(2)?, + body_type: row.get(3)?, + orbit_index: row.get(4)?, + proper_name: row.get(5)?, + mass_class: row.get(6)?, + atmosphere: row.get(7)?, + surface_gravity: row.get(8)?, + biome_summary: row.get(9)?, + hydrosphere: row.get(10)?, + inhabited: row.get::<_, i32>(11)? != 0, + population: row.get::<_, Option>(12)?.unwrap_or(0), + economic_role: row.get(13)?, + cultural_corridor: row.get(14)?, + industrial_corridor: row.get(15)?, + }) + }, + ) + .unwrap_or_else(|_| { + eprintln!("error: body '{}' not found", body_id); + process::exit(1); + }); + + // Also fetch stations orbiting this body + let stations = query_stations_for_body(conn, body_id); + + #[derive(Serialize)] + struct BodyDetail { + #[serde(flatten)] + body: BodyRow, + stations: Vec, + } + + let detail = BodyDetail { + body: row, + stations, + }; + println!("{}", serde_json::to_string_pretty(&detail).unwrap()); +} + +pub fn cmd_show_station(conn: &Connection, station_id: &str) { + let row = conn + .query_row( + "SELECT station_id, system_id, orbits_body_id, station_type, + proper_name, population, economic_role, governance_type, + docking_class, has_gate_infrastructure, district_count + FROM stations WHERE station_id = ?1", + params![station_id], + |row| { + Ok(StationRow { + station_id: row.get(0)?, + system_id: row.get(1)?, + orbits_body_id: row.get(2)?, + station_type: row.get(3)?, + proper_name: row.get(4)?, + population: row.get::<_, Option>(5)?.unwrap_or(0), + economic_role: row.get(6)?, + governance_type: row.get(7)?, + docking_class: row.get(8)?, + has_gate_infrastructure: row.get::<_, Option>(9)?.unwrap_or(0) != 0, + district_count: row.get::<_, Option>(10)?.unwrap_or(1), + }) + }, + ) + .unwrap_or_else(|_| { + eprintln!("error: station '{}' not found", station_id); + process::exit(1); + }); + + println!("{}", serde_json::to_string_pretty(&row).unwrap()); +} diff --git a/server/src/bin/atlas/stats.rs b/server/src/bin/atlas/stats.rs new file mode 100644 index 000000000..e92458ef3 --- /dev/null +++ b/server/src/bin/atlas/stats.rs @@ -0,0 +1,90 @@ +use rusqlite::Connection; +use serde::Serialize; + +#[derive(Serialize)] +struct StatsOutput { + systems: i64, + bodies: i64, + bodies_by_type: Vec, + inhabited_bodies: i64, + stations: i64, + stations_by_type: Vec, + systems_with_bodies: i64, + systems_without_bodies: i64, +} + +#[derive(Serialize)] +struct TypeCount { + r#type: String, + count: i64, +} + +pub fn cmd_stats(conn: &Connection) { + let systems: i64 = conn + .query_row("SELECT COUNT(*) FROM star_systems", [], |r| r.get(0)) + .unwrap(); + let bodies: i64 = conn + .query_row("SELECT COUNT(*) FROM bodies", [], |r| r.get(0)) + .unwrap(); + let inhabited_bodies: i64 = conn + .query_row("SELECT COUNT(*) FROM bodies WHERE inhabited = 1", [], |r| { + r.get(0) + }) + .unwrap(); + let stations: i64 = conn + .query_row("SELECT COUNT(*) FROM stations", [], |r| r.get(0)) + .unwrap(); + let systems_with_bodies: i64 = conn + .query_row("SELECT COUNT(DISTINCT system_id) FROM bodies", [], |r| { + r.get(0) + }) + .unwrap(); + + let mut bodies_by_type = Vec::new(); + { + let mut stmt = conn + .prepare("SELECT body_type, COUNT(*) FROM bodies GROUP BY body_type ORDER BY body_type") + .unwrap(); + let rows = stmt + .query_map([], |row| { + Ok(TypeCount { + r#type: row.get(0)?, + count: row.get(1)?, + }) + }) + .unwrap(); + for r in rows.flatten() { + bodies_by_type.push(r); + } + } + + let mut stations_by_type = Vec::new(); + { + let mut stmt = conn + .prepare("SELECT station_type, COUNT(*) FROM stations GROUP BY station_type ORDER BY station_type") + .unwrap(); + let rows = stmt + .query_map([], |row| { + Ok(TypeCount { + r#type: row.get(0)?, + count: row.get(1)?, + }) + }) + .unwrap(); + for r in rows.flatten() { + stations_by_type.push(r); + } + } + + let stats = StatsOutput { + systems, + bodies, + bodies_by_type, + inhabited_bodies, + stations, + stations_by_type, + systems_with_bodies, + systems_without_bodies: systems - systems_with_bodies, + }; + println!("{}", serde_json::to_string_pretty(&stats).unwrap()); +} diff --git a/server/src/bin/atlas/sync_wiki.rs b/server/src/bin/atlas/sync_wiki.rs new file mode 100644 index 000000000..86e1ebed3 --- /dev/null +++ b/server/src/bin/atlas/sync_wiki.rs @@ -0,0 +1,319 @@ +use rusqlite::{params, Connection}; +use serde::Serialize; + +use crate::common::format_population; + +pub fn cmd_sync_wiki(conn: &Connection, system_id: Option<&str>, wiki_dir: &str) { + // Get list of systems to sync + let system_ids: Vec = if let Some(sid) = system_id { + vec![sid.to_string()] + } else { + let mut stmt = conn + .prepare("SELECT DISTINCT system_id FROM bodies ORDER BY system_id") + .unwrap(); + stmt.query_map([], |row| row.get::<_, String>(0)) + .unwrap() + .filter_map(|r| r.ok()) + .collect() + }; + + let mut synced = 0; + + for sid in &system_ids { + let sid_slug = sid.replace(' ', "-"); + let wiki_path = std::path::Path::new(wiki_dir) + .join(&sid_slug) + .join("index.md"); + if !wiki_path.exists() { + eprintln!("skip: {} — no wiki page at {}", sid, wiki_path.display()); + continue; + } + + // Query bodies + let mut body_stmt = conn + .prepare( + "SELECT body_id, proper_name, body_type, orbit_index, parent_body_id, + inhabited, population, mass_class, atmosphere, surface_gravity, + orbital_period_days, rotation_period_hours, + biome_summary, hydrosphere, economic_role, settlement_pattern, + industrial_corridor + FROM bodies WHERE system_id = ?1 + ORDER BY CASE WHEN parent_body_id IS NULL THEN orbit_index ELSE 1000 + orbit_index END", + ) + .unwrap(); + + struct Body { + id: String, + name: Option, + btype: String, + orbit: i32, + parent: Option, + inhabited: bool, + population: i64, + mass_class: Option, + atmosphere: Option, + gravity: Option, + orbital_days: Option, + rotation_hours: Option, + biome: Option, + hydro: Option, + econ: Option, + settlement: Option, + industrial: Option, + } + + let bodies: Vec = body_stmt + .query_map(params![sid], |row| { + Ok(Body { + id: row.get(0)?, + name: row.get(1)?, + btype: row.get(2)?, + orbit: row.get::<_, Option>(3)?.unwrap_or(0), + parent: row.get(4)?, + inhabited: row.get::<_, i32>(5)? != 0, + population: row.get::<_, Option>(6)?.unwrap_or(0), + mass_class: row.get(7)?, + atmosphere: row.get(8)?, + gravity: row.get(9)?, + orbital_days: row.get(10)?, + rotation_hours: row.get(11)?, + biome: row.get(12)?, + hydro: row.get(13)?, + econ: row.get(14)?, + settlement: row.get(15)?, + industrial: row.get(16)?, + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect(); + + // Query stations + let mut station_stmt = conn + .prepare( + "SELECT station_id, proper_name, orbits_body_id, station_type, + population, economic_role, governance_type, docking_class, + has_gate_infrastructure, district_count + FROM stations WHERE system_id = ?1 + ORDER BY station_id", + ) + .unwrap(); + + struct Station { + id: String, + name: Option, + orbits: Option, + stype: String, + population: i64, + econ: Option, + governance: Option, + docking: Option, + gate: bool, + districts: i32, + } + + let stations: Vec = station_stmt + .query_map(params![sid], |row| { + Ok(Station { + id: row.get(0)?, + name: row.get(1)?, + orbits: row.get(2)?, + stype: row.get(3)?, + population: row.get::<_, Option>(4)?.unwrap_or(0), + econ: row.get(5)?, + governance: row.get(6)?, + docking: row.get(7)?, + gate: row.get::<_, Option>(8)?.unwrap_or(0) != 0, + districts: row.get::<_, Option>(9)?.unwrap_or(1), + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect(); + + if bodies.is_empty() && stations.is_empty() { + continue; + } + + // Build the Celestial Bodies section + let mut section = String::new(); + section.push_str("## Celestial Bodies\n"); + section + .push_str("\n\n"); + + // Bodies table + if !bodies.is_empty() { + section.push_str("| Orbit | ID | Name | Type | Inhabited | Pop | Mass | Gravity | Year (d) | Day (h) | Atmo | Biome | Hydro | Economy | Settlement | Industrial |\n"); + section.push_str("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n"); + + // First pass: top-level bodies (no parent) + for b in &bodies { + if b.parent.is_some() { + continue; + } + let name = b.name.as_deref().unwrap_or("—"); + let pop = if b.population > 0 { + format_population(b.population) + } else { + "—".to_string() + }; + let grav = b + .gravity + .map(|g| format!("{:.2}g", g)) + .unwrap_or_else(|| "—".to_string()); + let year = b + .orbital_days + .map(|d| format!("{:.0}", d)) + .unwrap_or_else(|| "—".to_string()); + let day = b + .rotation_hours + .map(|h| format!("{:.1}", h)) + .unwrap_or_else(|| "—".to_string()); + section.push_str(&format!( + "| {} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", + b.orbit, + b.id, + name, + b.btype, + if b.inhabited { "yes" } else { "no" }, + pop, + b.mass_class.as_deref().unwrap_or("—"), + grav, + year, + day, + b.atmosphere.as_deref().unwrap_or("—"), + b.biome.as_deref().unwrap_or("—"), + b.hydro.as_deref().unwrap_or("—"), + b.econ.as_deref().unwrap_or("—"), + b.settlement.as_deref().unwrap_or("—"), + b.industrial.as_deref().unwrap_or("—"), + )); + + // Child bodies (moons) + for m in &bodies { + if m.parent.as_deref() == Some(&b.id) { + let mname = m.name.as_deref().unwrap_or("—"); + let mpop = if m.population > 0 { + format_population(m.population) + } else { + "—".to_string() + }; + let mgrav = m + .gravity + .map(|g| format!("{:.2}g", g)) + .unwrap_or_else(|| "—".to_string()); + let myear = m + .orbital_days + .map(|d| format!("{:.0}", d)) + .unwrap_or_else(|| "—".to_string()); + let mday = m + .rotation_hours + .map(|h| format!("{:.1}", h)) + .unwrap_or_else(|| "—".to_string()); + section.push_str(&format!( + "| ↳ {}.{} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", + b.orbit, + m.orbit, + m.id, + mname, + m.btype, + if m.inhabited { "yes" } else { "no" }, + mpop, + m.mass_class.as_deref().unwrap_or("—"), + mgrav, + myear, + mday, + m.atmosphere.as_deref().unwrap_or("—"), + m.biome.as_deref().unwrap_or("—"), + m.hydro.as_deref().unwrap_or("—"), + m.econ.as_deref().unwrap_or("—"), + m.settlement.as_deref().unwrap_or("—"), + m.industrial.as_deref().unwrap_or("—"), + )); + } + } + } + section.push('\n'); + } + + // Stations table + if !stations.is_empty() { + section.push_str("### Stations & Facilities\n\n"); + section.push_str("| ID | Name | Type | Orbits | Population | Economy | Governance | Docking | Gate | Districts |\n"); + section.push_str("|---|---|---|---|---|---|---|---|---|---|\n"); + for s in &stations { + let name = s.name.as_deref().unwrap_or("—"); + let orbits = s.orbits.as_deref().unwrap_or("—"); + let pop = if s.population > 0 { + format_population(s.population) + } else { + "—".to_string() + }; + section.push_str(&format!( + "| `{}` | {} | {} | `{}` | {} | {} | {} | {} | {} | {} |\n", + s.id, + name, + s.stype, + orbits, + pop, + s.econ.as_deref().unwrap_or("—"), + s.governance.as_deref().unwrap_or("—"), + s.docking.as_deref().unwrap_or("—"), + if s.gate { "yes" } else { "no" }, + s.districts, + )); + } + section.push('\n'); + } + + // Read current wiki content + let content = std::fs::read_to_string(&wiki_path).unwrap(); + + // Replace or insert the Celestial Bodies section + let marker_start = "## Celestial Bodies"; + let new_content = if let Some(start_pos) = content.find(marker_start) { + // Find the next ## heading after the section (or end of file) + let after = &content[start_pos + marker_start.len()..]; + let end_offset = after + .find("\n## ") + .map(|p| start_pos + marker_start.len() + p + 1) + .unwrap_or(content.len()); + format!( + "{}{}\n{}", + &content[..start_pos], + section.trim_end(), + &content[end_offset..] + ) + } else { + // Insert before ## Topology if it exists, otherwise before end of file + if let Some(topo_pos) = content.find("\n## Topology") { + let insert_pos = topo_pos + 1; // after the newline + format!( + "{}{}\n\n{}", + &content[..insert_pos], + section.trim_end(), + &content[insert_pos..] + ) + } else { + // No topology section — append at end + format!("{}\n{}", content.trim_end(), section) + } + }; + + std::fs::write(&wiki_path, new_content).unwrap(); + eprintln!("synced: {} → {}", sid, wiki_path.display()); + synced += 1; + } + + #[derive(Serialize)] + struct SyncResult { + systems_synced: usize, + } + println!( + "{}", + serde_json::to_string_pretty(&SyncResult { + systems_synced: synced + }) + .unwrap() + ); +} diff --git a/server/src/bin/atlas/systems.rs b/server/src/bin/atlas/systems.rs new file mode 100644 index 000000000..c51957ddf --- /dev/null +++ b/server/src/bin/atlas/systems.rs @@ -0,0 +1,382 @@ +use rusqlite::{params, Connection}; +use serde::Serialize; + +use crate::common::{query_bodies, query_stations}; + +pub fn cmd_list_bodies( + conn: &Connection, + system: Option<&str>, + body_type: Option<&str>, + inhabited: bool, + unnamed: bool, +) { + let bodies = query_bodies(conn, system, body_type, inhabited, unnamed); + println!("{}", serde_json::to_string_pretty(&bodies).unwrap()); +} + +pub fn cmd_list_stations(conn: &Connection, system: Option<&str>, station_type: Option<&str>) { + let stations = query_stations(conn, system, station_type); + println!("{}", serde_json::to_string_pretty(&stations).unwrap()); +} + +pub fn cmd_list_systems( + conn: &Connection, + sector: Option<&str>, + hop: Option, + finished: bool, + unfinished: bool, +) { + // Build query dynamically based on filters + let mut conditions: Vec = Vec::new(); + let mut param_values: Vec> = Vec::new(); + let mut idx = 1; + + if let Some(s) = sector { + conditions.push(format!("s.geographic_sector = ?{idx}")); + param_values.push(Box::new(s.to_string())); + idx += 1; + } + if let Some(h) = hop { + conditions.push(format!("g.hop_distance_from_gateway = ?{idx}")); + param_values.push(Box::new(h)); + idx += 1; + } + let _ = idx; // suppress unused warning + if finished { + conditions.push("s.system_id IN (SELECT DISTINCT system_id FROM bodies)".to_string()); + } + if unfinished { + conditions.push("s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)".to_string()); + } + + let where_clause = if conditions.is_empty() { + String::new() + } else { + format!(" WHERE {}", conditions.join(" AND ")) + }; + + let sql = format!( + "SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class, + g.gate_topology, s.geographic_sector, + g.hop_distance_from_gateway, + s.habitable_planet_count, s.inhabited_planet_count, + e.population + FROM star_systems s + JOIN system_gates g ON s.system_id = g.system_id + LEFT JOIN system_economy e ON s.system_id = e.system_id + {where_clause} + ORDER BY g.hop_distance_from_gateway, s.system_id" + ); + + let mut stmt = conn.prepare(&sql).unwrap(); + let params_refs: Vec<&dyn rusqlite::types::ToSql> = + param_values.iter().map(|p| p.as_ref()).collect(); + + #[derive(Serialize)] + struct ListSystem { + system_id: String, + proper_name: Option, + star_type: Option, + spectral_class: Option, + gate_topology: Option, + geographic_sector: Option, + hop_distance: Option, + habitable_planet_count: Option, + inhabited_planet_count: Option, + population: Option, + } + + let systems: Vec = stmt + .query_map(params_refs.as_slice(), |row| { + Ok(ListSystem { + system_id: row.get(0)?, + proper_name: row.get(1)?, + star_type: row.get(2)?, + spectral_class: row.get(3)?, + gate_topology: row.get(4)?, + geographic_sector: row.get(5)?, + hop_distance: row.get(6)?, + habitable_planet_count: row.get(7)?, + inhabited_planet_count: row.get(8)?, + population: row.get(9)?, + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect(); + + #[derive(Serialize)] + struct ListResult { + count: usize, + #[serde(skip_serializing_if = "Option::is_none")] + sector: Option, + #[serde(skip_serializing_if = "Option::is_none")] + hop: Option, + systems: Vec, + } + + let result = ListResult { + count: systems.len(), + sector: sector.map(|s| s.to_string()), + hop, + systems, + }; + println!("{}", serde_json::to_string_pretty(&result).unwrap()); +} + +pub fn cmd_unfinished(conn: &Connection, sector: Option<&str>) { + let sql = if sector.is_some() { + "SELECT s.system_id, s.proper_name, s.star_type + FROM star_systems s + WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) + AND s.geographic_sector = ?1 + ORDER BY s.system_id" + } else { + "SELECT s.system_id, s.proper_name, s.star_type + FROM star_systems s + WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) + ORDER BY s.system_id" + }; + + let mut stmt = conn.prepare(sql).unwrap(); + + #[derive(Serialize)] + struct UnfinishedSystem { + system_id: String, + proper_name: Option, + star_type: Option, + } + + let rows: Vec = if let Some(s) = sector { + stmt.query_map(params![s], |row| { + Ok(UnfinishedSystem { + system_id: row.get(0)?, + proper_name: row.get(1)?, + star_type: row.get(2)?, + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect() + } else { + stmt.query_map([], |row| { + Ok(UnfinishedSystem { + system_id: row.get(0)?, + proper_name: row.get(1)?, + star_type: row.get(2)?, + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect() + }; + + #[derive(Serialize)] + struct UnfinishedResult { + count: usize, + sector: Option, + systems: Vec, + } + let result = UnfinishedResult { + count: rows.len(), + sector: sector.map(|s| s.to_string()), + systems: rows, + }; + println!("{}", serde_json::to_string_pretty(&result).unwrap()); +} + +pub fn cmd_next(conn: &Connection, hop: Option, sector: Option<&str>) { + // Find the target hop — either specified or the lowest with unfinished systems + let target_hop: i32 = if let Some(h) = hop { + h + } else { + let sql = if sector.is_some() { + "SELECT MIN(g.hop_distance_from_gateway) + FROM star_systems s + JOIN system_gates g ON s.system_id = g.system_id + WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) + AND s.geographic_sector = ?1" + } else { + "SELECT MIN(g.hop_distance_from_gateway) + FROM star_systems s + JOIN system_gates g ON s.system_id = g.system_id + WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)" + }; + if let Some(s) = sector { + conn.query_row(sql, params![s], |r| r.get::<_, Option>(0)) + } else { + conn.query_row(sql, [], |r| r.get::<_, Option>(0)) + } + .unwrap() + .unwrap_or(-1) + }; + + if target_hop < 0 { + println!( + r#"{{"hop": null, "count": 0, "systems": [], "message": "all systems have bodies"}}"# + ); + return; + } + + #[derive(Serialize)] + struct NextSystem { + system_id: String, + proper_name: Option, + star_type: Option, + spectral_class: Option, + gate_topology: Option, + geographic_sector: Option, + habitable_planet_count: Option, + inhabited_planet_count: Option, + population: Option, + } + + let sql = if sector.is_some() { + "SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class, + g.gate_topology, s.geographic_sector, + s.habitable_planet_count, s.inhabited_planet_count, + e.population + FROM star_systems s + JOIN system_gates g ON s.system_id = g.system_id + LEFT JOIN system_economy e ON s.system_id = e.system_id + WHERE g.hop_distance_from_gateway = ?1 + AND s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) + AND s.geographic_sector = ?2 + ORDER BY s.system_id" + } else { + "SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class, + g.gate_topology, s.geographic_sector, + s.habitable_planet_count, s.inhabited_planet_count, + e.population + FROM star_systems s + JOIN system_gates g ON s.system_id = g.system_id + LEFT JOIN system_economy e ON s.system_id = e.system_id + WHERE g.hop_distance_from_gateway = ?1 + AND s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) + ORDER BY s.system_id" + }; + + let mut stmt = conn.prepare(sql).unwrap(); + + let systems: Vec = if let Some(s) = sector { + stmt.query_map(params![target_hop, s], |row| { + Ok(NextSystem { + system_id: row.get(0)?, + proper_name: row.get(1)?, + star_type: row.get(2)?, + spectral_class: row.get(3)?, + gate_topology: row.get(4)?, + geographic_sector: row.get(5)?, + habitable_planet_count: row.get(6)?, + inhabited_planet_count: row.get(7)?, + population: row.get(8)?, + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect() + } else { + stmt.query_map(params![target_hop], |row| { + Ok(NextSystem { + system_id: row.get(0)?, + proper_name: row.get(1)?, + star_type: row.get(2)?, + spectral_class: row.get(3)?, + gate_topology: row.get(4)?, + geographic_sector: row.get(5)?, + habitable_planet_count: row.get(6)?, + inhabited_planet_count: row.get(7)?, + population: row.get(8)?, + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect() + }; + + #[derive(Serialize)] + struct NextResult { + hop: i32, + sector: Option, + count: usize, + systems: Vec, + } + + let result = NextResult { + hop: target_hop, + sector: sector.map(|s| s.to_string()), + count: systems.len(), + systems, + }; + println!("{}", serde_json::to_string_pretty(&result).unwrap()); +} + +pub fn cmd_corridor_status(conn: &Connection) { + // Query unfinished systems grouped by geographic_sector and hop_distance_from_gateway. + // "Unfinished" = no rows in bodies for this system_id. + // LEFT JOIN so systems with no gate record are still counted (hop = NULL → "?"). + let mut stmt = conn + .prepare( + "SELECT s.geographic_sector, g.hop_distance_from_gateway, COUNT(*) AS remaining + FROM star_systems s + LEFT JOIN system_gates g ON s.system_id = g.system_id + WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies) + GROUP BY s.geographic_sector, g.hop_distance_from_gateway + ORDER BY s.geographic_sector, g.hop_distance_from_gateway", + ) + .unwrap(); + + #[derive(Serialize)] + struct CorridorRow { + sector: Option, + hop: Option, + remaining: i64, + } + + let rows: Vec = stmt + .query_map([], |row| { + Ok(CorridorRow { + sector: row.get(0)?, + hop: row.get(1)?, + remaining: row.get(2)?, + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect(); + + if rows.is_empty() { + #[derive(Serialize)] + struct EmptyResult { + total: i64, + message: &'static str, + corridors: Vec<()>, + } + println!( + "{}", + serde_json::to_string_pretty(&EmptyResult { + total: 0, + message: "All systems have bodies authored.", + corridors: vec![], + }) + .unwrap() + ); + return; + } + + let total: i64 = rows.iter().map(|r| r.remaining).sum(); + + #[derive(Serialize)] + struct CorridorResult { + total: i64, + corridors: Vec, + } + println!( + "{}", + serde_json::to_string_pretty(&CorridorResult { + total, + corridors: rows, + }) + .unwrap() + ); +} From 7f231850358a8b3175f827144070a71bc92b59d6 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 5 Apr 2026 09:38:15 +0200 Subject: [PATCH 2/5] chore(meta): update changelog Co-Authored-By: Claude Opus 4.6 --- server/CHANGELOG.md | 1217 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1217 insertions(+) create mode 100644 server/CHANGELOG.md diff --git a/server/CHANGELOG.md b/server/CHANGELOG.md new file mode 100644 index 000000000..08113b76b --- /dev/null +++ b/server/CHANGELOG.md @@ -0,0 +1,1217 @@ +# Changelog + +All notable changes to The Settled Reach project will be documented in this file. + +Format based on [Keep a Changelog](https://keepachangelog.com/). + +## [Unreleased] + +## [v0.1.30] — 2026-04-05 + +### Added +- `corridor-status` subcommand for atlas CLI — shows remaining unfinished systems grouped by geographic sector and hop distance (#744) +- Star map insert module — concentric hop-ring view of 301 systems, sector-colored, click-to-select with info panel, pan/zoom (#674) +- BoneAttachment3D overhead anchor above Head bone for future floating UI elements (#712) +- CharacterVisualDescriptor wired into startup IPC and snapshot restore for save/load persistence (#718) +- Display-only hair highlight swatch (auto-derived from primary tint) in character creation (#719) +- Asset manifest fully populated (11 body types, 14 hair, 4 heads, 4 eyebrows, 8 clothing) with regeneration script (#720) +- Sprint 30 acceptance test suite (27 tests across all 5 tickets) + +### Fixed +- `habitable_planet_count` filter now accepts both "breathable" and "standard" atmosphere values — previously all committed systems reported 0 habitable planets (#762). Systems committed before this fix may have stale `habitable_planet_count = 0`; re-commit to update. +- `corridor-status` uses LEFT JOIN so systems without gate records are included in counts +- `generate_body_matrix` now emits `atmosphere: "standard"` (was "breathable") to match committed-system conventions +- DirAccess asset scanning replaced with manifest JSON — fixes character creation in exported PCK builds (#720) +- Star map set_insert_active() no longer auto-shows the modal panel (#674) +- Star map insert state propagation wired into main.gd (#674) + +## [v0.1.29] — 2026-04-03 + +### Added +- Complete body catalogs for all 301 star systems — every system now has proper_name, bodies, and stations in systems.db +- 24 new system proposals authored across east_reach (6) and deep_frontier (18) corridors +- Bulk-synced 233 proposals into systems.db that were missing from prior sessions +- Named all remaining unsettled systems with GJ designations (301/301 complete) +- Silence topics and narrative hooks for Deep Frontier corridor systems +- Named 5 unnamed stations/bodies: Morwenna, Havelmark, Portela, Grindvik, Sturen Platforms + +### Fixed +- 7 duplicate body IDs in proposals (planet and gas giant sharing same letter suffix) +- star_type/spectral_class mismatches corrected across 36 atlas proposals +- GJ1002 copy-paste planets differentiated with unique orbital parameters +- GJ880 malformed spectral_class and Leirvik station population +- GJ3325 malformed spectral_class +- 40 proposals brought to minimum planet count and given asteroid belts +- atlas-verify now allows named uninhabited bodies (lore names) +- GJ-111 spectral_class corrected to F5/F6V (matching wiki) +- GJ-3943 spectral_class corrected to K5V+M3V (valid binary notation) +- GJ-903 wiki page completed (was empty stub) +- Deleted superseded tooling/atlas-helpers.sh + +## [v0.1.28] — 2026-03-23 + +### Added +- Character creation screen with live 3D preview, 5-tab panel (Body, Hair, Clothing, Accessories, Debug), manifest-driven content +- Face-based body segmentation pipeline — exclusive assignment per face, no overlap, 19 segments (head, neck, torso_upper, torso, hips, arms, hands, legs, feet, eyes, eyebrows) +- 6 body types: average/muscular/teen m/f from Quaternius Source tier .blends +- Solidified hair (20mm) and clothing (25mm) for depth-correct layering over body +- Skin tinting with per-body-type embedded textures and white recolor mask +- Eye color with iris-only mask derived from T_Eye_Split.png green channel +- Eyebrow tinting from hair color via body segment shader +- Quaternius peasant outfit set (tunic, pants, shoes) across all body types +- Asset manifest (manifest.json) controlling all available content — replaces filesystem scanning +- Pre-push lint hook: GDScript parse + gdlint + gdformat (advisory) + Rust clippy/fmt +- Screenshot test automation with JSON config, 4-cardinal captures, quit-after-screenshot +- Debug tab with per-segment visibility toggles, All ON/OFF buttons +- Scroll zoom (cursor-toward on zoom in, head-bone targeting) +- Dynamic UI: tabs hidden when no content, clothing slots hidden when empty, facial hair hidden for female/teen/child +- Gender-aware randomizer +- 26 Q-records filed (Q-063 through Q-088) covering plugins, architecture patterns, and future features +- 15 tickets filed (#722-736) for tooling, UI, distribution, and QA improvements +- Araminta agent updated with Poly Haven + Quaternius asset sourcing rules + +### Fixed +- **ALPHA output in toon shaders caused all depth sorting failures** — removing ALPHA from toon.gdshader and toon_masked.gdshader fixed hair/head clipping, clothing/body clipping, and all z-fighting issues simultaneously +- Character editor: color swatch stale closure, modal OK child traversal, D-165 HSL palette, fallback asset IDs, overhead camera pitch, hair highlight display-only +- Archetype selection screen removed (v0.2 pivot — no Smuggler/Detective) +- GDScript strict typing errors (Variant inference on Dictionary.get(), JSON.parse_string()) +- TabContainer children vanishing on scene instantiation — moved to programmatic creation +- Hair meshes exported with proper skinning (was export_skins=False) +- Hair mask PNGs were solid black (Blender image API bug) — regenerated as white +- Rust formatting issues caught by new pre-push hook +- D-148 overhead camera angle convention clarified (0° → -80°) + +### Changed +- Body segmentation: torso split into torso_upper (spine_03 + clavicle) and torso (spine_01 + spine_02), hips (pelvis) as independent segment +- Clothing no longer hides body segments — solidify handles visual coverage, segment hiding reserved for amputation/prosthetics +- Randomise → Randomize (US English convention) +- pr-push skill: mandatory runtime smoke test before pushing +- pr-review skill: parse check before spawning reviewers, process gap flagging + +## [v0.1.27] — 2026-03-17 + +### Added +- 29 zone-type behavior templates completing the full library of 31 zone types per D-142. 2,210 culture-neutral behavior primitives across rural, industrial, port, extraction, commercial, administrative, research, medical, military, security, entertainment, residential, detention, archaeological, wilderness, and diplomatic zones +- Generator-compatible overheard conversation RON format replacing the deprecated named-NPC YAML. 16 sample conversations parameterized by role pair and zone type with knowledge_payload for investigative value +- Cross-culture name pool collision checking in validate-ron (`--check-name-collisions` mode) + +- Drifter's Guide to the Reach — 100% coverage (301/301 systems). 262 new GTTR entries across all 5 regions plus updated regional index pages with hop-grouped tables +- GJ 6711 / Abzu workshop — 6 rounds, 24 documents: initial briefs (4 agents), cross-pollination syntheses, sealed envelope (observation is load-bearing), stress test challenges against existing canon, final syntheses with Abzu naming, mechanics design (watch investment counter, six player verbs, bleed tile states, Adams & Ford entry filing), carry-out proposals (unresolved — reward design TBD) +- Star map topology and real-coordinate SVG visualizations +- Compact of Westphalia faction page — mutual recognition treaty, ~30-40 west_reach systems, four core principles, rotating council, internal treaty-vs-government tension +- Batch 18 wiki pages — 18 systems reassigned from deep_frontier into named corridors (Waterkant, Breëvlei, Stilwater, Mossbank, Ribeirão, Nascente, Dernier Quai, Marktfeld, Bestevaer, Lichtung, Knotenpunkt, Posto Avançado, Weitblick, Último Farol, plus 4 unsettled) +- Batch 17 wiki pages — Xa Vời, Eisfeld, Confluent (60+ wine châteaux), Dunkelholz, Echternach (Luxembourgish), Bout du Chemin (French), Grenzstein, plus GJ 4056 (unsettled) +- Corporation doc for Vins de Grand Vide — négociant cooperative, three-tier classification (Grand Vide Classé / Vins de Corridors / Vin de Table du Vide), 6 named estates, 350-year commercial archive +- Batch 16 wiki pages — 13 south_reach systems at hops 7-8 including Espinho, Okahandja, Encrucijada, Quilombo, Velha Guarda, Fragua, Puerto Último, plus GJ 902 (unsettled, reserved for base building DLC per D-145) +- Batch 15 wiki page — Shimanami (east_reach hop 8) +- Batch 14 wiki pages — Wagtoring, Dunmore, Caledonia's End (north_reach hops 7-8) +- Batch 13 wiki pages — Stillvakt, Haltefenn, Vindkast, Kopparhytta, Steinfeld, Brückenau (west_reach hops 7-8) +- Corporation docs for Nordmark Skog (timber, Stillvakt) and Talbräu (lager, Brückenau) +- D-145: base building DLC — GJ 902 habitable moon as potential player settlement site +- South_reach wiki pages (batch 12) — Matamba, Inhambane, Isibaya, Kaapse Baai, Mwangaza, Dzimbahwe, Vuurkloof, Várzea, Nowa Huta, plus GJ 695A (unsettled) +- Corporation docs for Ferreira Monteiro (trade arbitration, Matamba) and Stalownia Kowalski (heavy equipment, Nowa Huta) +- East_reach wiki pages (batch 11) — Kaur's Observatory, Seongho, Tình Yên, Purnima, Marunong, Clearwater Station, Tam Giang, Jeonnam, Dagat, Suối Vàng +- Inner hub wiki pages (hops 3-4) — Crown's Hollow, Cairnside, Schuilhoek, Travessia, Nová Tržnice +- Corporation doc for Mercado Travessia (grocery chain, Travessia) +- Corporation docs for Adams & Ford Publishing, Calloway Distillery, thrds (updated index) +- 300-system CSV framework (systems-framework.md) — 66 columns covering colonization waves, cultural archetypes, tone framework, economic distribution, and Van Maanen's Star validation (D-095) +- Star map gate topology — 300-node network with 334 edges across 6 sectors, generated and hand-tuned for natural corridor structure in frontier space +- Star map generation pipeline — seed-based generator, sculpting script, core sector patcher, topology tuner with bridge-safe connectivity guarantees +- d2 sector diagrams — 6 sector maps + overview visualization of gate network +- Multi-table star systems DB schema with GJ catalog IDs as primary keys — 8 tables covering identity, gates, history, economy, factions, culture +- Full wiki prose for 10 core systems — Sirius, Ran, Tau Ceti, Sol, Arbour, Groombridge, Struve, Cygni B, ACB, Bastion +- Corporation wiki pages — Gate Corporation, Mastroianni Vehicle Group, Prometheus Labs +- Wiki category index pages for star-systems, corporations, factions, technology, contraband, concepts +- 10 next-tier systems named and assigned roles — Renaissance, Nova Roma, Prometheus, Proxima, Rigil Kentaurus, Barnard's Star, Lacaille, Cairn, Meridian, Aurelius +- `hop_distance_from_gateway` column in system_gates table + +### Changed +- Renamed GJ-7547 from Wag-'n-Bietjie to Skemeraand ("twilight evening") — better tonal fit for hop 22 position +- Rebuilt catalog.md and star-systems/index.md — now covers all 301 systems (227 named) organized by sector and hop distance +- 14 systems reassigned from deep_frontier into named corridors at hops 5-7 (5 north, 3 south, 3 east, 3 west) +- Renamed Carrefour to Confluent; expanded to 60+ named châteaux with 16 individually described estates +- D-095 aperture range amended from 4-8 to 1-8 — single-aperture dead-end systems valid for isolated frontier outposts +- D-095 amended: inter-system gates are alien-built (aperture count alien-determined), intra-system span gates are human-built (Institute reverse-engineering, Gate Corporation license) +- All system identifiers migrated from arbitrary S-numbers to GJ astronomical catalog IDs +- Q-039 (gate topology generation) resolved by 301-system star map + +### Removed +- Redundant `astronomical_id` column from star systems schema (system_id IS the GJ number) + +## [v0.1.26] — 2026-03-13 + +### Added +- ContentType::Factual — lines with numbers, denials, causal chains bypass LLM and serve base text directly (#650, D-138) +- Voice pipeline observer integration — enrichment systems rewrite dialogue/conversation text with voiced versions before snapshot assembly (#652) +- SQLite settings storage — per-player persistent settings via rusqlite (bundled), IPC protocol v20 with ChangeSettings/RequestAllSettings/DeleteSetting commands (#627) +- Composable behavior engine — three-layer action+modifier+context primitives replace flat culture×zone×role behavior strings (#633, D-139, Q-057 resolved) +- Stronger few-shot examples for Friendly and RoutineDeviation tells (#651) +- AI-Enhanced Dialogue toggle — settings panel toggle with layered hardware detection (RAM/TPT/degradation), battery auto-suspend with player override, warning label (#646, D-138) +- PlatformInfo autoload — client-side OS abstraction centralizing all platform queries: power state, memory, CPU, GPU, display, locale, file paths, diagnostics helper (#659, D-141) +- Vael and Osse culture profiles with voice personas, behavior modifiers, and explicit NEVER blocks (#653) +- Behavior modifiers for all three cultures — 7-category contract: work_pace, physical_manner, social_signal, task_completion, environmental_scan, offduty_posture, authority_response (#634) +- Zone-type template architecture — behavior primitives moved from per-location files to reusable zone-type templates (content/global/zone-types/). 31 zone types planned for v1.0 (#661, D-142) +- POI three-tier system — large POIs as zone types, abandoned flag for decay variants, poi_overlay for small landmarks (D-142) +- Authoring guides for base text elevation, culture creation, and content directory structure +- D-140: dialogue re-voicing quality constraints — Paula's six rules +- D-142: zone-type template architecture for scalable NPC behavior across 300+ systems + +### Removed +- v0.1 content loading system — server/src/content/ module (8200 lines), tooling/content-converter/, tooling/validate-content, content-ron/, content/_meta/ (#655, D-122) +- AiDialogueDetector — duplicate of HardwareDetector, replaced by PlatformInfo abstraction (#659) +- v0.1 hand-authored Van Maanen's Star dialogue, monologue, and NPC profiles — 64 files superseded by generated NPCs (#656, D-122) +- Detective mission system — investigation knowledge, lattice-commission faction, design docs, workshop archives (#657, D-117) + +### Fixed +- Name pool cross-contamination — zero overlaps across Van Maanen's Star, Vael, and Osse cultures +- D-141 → D-142 reference correction in zone-type templates +- Modifier coverage expanded to 2+ per category for all cultures; authority_response differentiated Van Maanen's Star/Osse +- Gendered pronouns removed from culture-neutral zone-type templates +- Ungrounded lore terms (Syndic, Meridian registration) replaced with generic descriptors in Osse culture +- Stale notes in content-structure-canonical.md and base-text-authoring-guide.md corrected +- Legacy annotation added to overheard.yaml (#664 tracks replacement) +- Dead dual_lens properties stripped from environmental YAML + +### Changed +- Protocol version bumped to 20 — ObserverSnapshot includes settings_response field (#627) +- All 18 agent briefings updated for v0.2 pivot — removed detective/smuggler/hand-authored references, aligned with generator-first approach (#658) +- 5 agent profiles (miri, ozzie, paula, inigo, hoshe) updated to remove stale v0.1 framing (#658) +- Q-015 closed as obsolete (D-122 eliminates hand-authored FRIEND content) +- D-018 perception model: franchise-specific example replaced with generic framing + +## [v0.1.25] — 2026-03-07 + +### Fixed +- Name pool first-pick bias — generator spike produced "Dav" as NPC 1 across all seeds; now uses derived RNG per zone+culture (#628) +- Behavior dedup — same behavior string no longer assigned to multiple NPCs in one zone run (#629) + +### Added +- Zone identity specs renamed to location-specific: van-maanens-star-rural-zone.ron and van-maanens-star-industrial-zone.ron — acknowledges these are culture×zone content, not reusable templates (#630, Q-057) +- ~108 new NPC behavior pool entries across all roles in both zone files — trader stage directions, foreman humanity behaviors, dock_worker/technician off-shift/break room behaviors (#630) +- Q-057 open question: composable behavior generation — decompose hand-authored pools into role actions + culture modifiers + context tags (#633, #634) +- Relationship-to-behavior pipeline — NPC behavior lines now reflect social connections (rivals ignore each other, friends gravitate, subordinates defer) (#631) +- Want/State layer — NPCs have internal motives (Bored, Alert, Suspicious, AvoidingSomeone, LookingForInfo) that leak through observable micro-tells (#632) +- LLM voice pipeline — Spike 1 (sr-voice CLI) and Spike 2 (full pipeline integration) complete. Gemma 2B Q4_K_M via stdin/stdout JSONL pipes, composition engine with double-prompt technique, 39 quality test cases (#638-644, D-138) + +## [v0.1.24] — 2026-03-06 + +### Added +- Character archetype select screen — two-card UI (Smuggler/Detective) between New Game and session start, keyboard+mouse selection, ESC cancels (#588, D-027) +- Triangle activation consumer — urgent monologue chime fires once per triangle per session when triangle_crisis_events received (#590, D-039) +- News ticker HUD — scrolling marquee visible in The Last Shift zone, hidden elsewhere, reads current_ticker from snapshot (#592, D-039) +- Triangle activation proximity monologue lines — 5 smuggler lines (Kael Davan) and 5 detective lines (Sera Venn/Torek Lintar) that fire when observing triangle anchor NPCs post-activation (#597, D-035, D-039) + +### Changed +- Protocol version bumped to 19 — StartupMessage includes character_archetype, snapshot includes triangle_crisis_events and current_ticker (#588, #590, #592) + +## [v0.1.23] — 2026-03-04 + +### Added +- TileKind enum (Floor/Wall/Void/Restricted) on WalkabilityMap with per-tile type data alongside walkability (#576, D-094) +- Location YAML tile format — hand-author tiles as string arrays (F/W/V/R characters), loaded into WalkabilityMap on production startup (#577) +- Chunk streaming system — ChunkLoadRadius and cadence-gated load/unload around player position, v0.1 covers full district (#578, D-012) +- EngagementRecord component — per-NPC observation time, conversation count, and monologue trigger count tracked by perception/dialogue/monologue systems (#570) +- MovementHistoryBuffer resource — 3000-tick ring buffer of player positions with co-presence proximity query (#571) +- Storyteller lifecycle rules — single activation per session, no concurrency, terminal resolution constants (#572) +- Storyteller activation_pass() — gate/proximity/engagement scoring/routing/module selection/TriangleActivatedEvent on 10-tick cadence (#579) +- Debug console server — 10 DebugCommandKind variants (AdvanceTicks, SkipToContamination, TeleportToPosition, InspectNpc, ListTriangles, etc.) with DebugResponsePayload on ObserverSnapshot (#580) +- Debug console client — tilde-toggle UI panel with command input, output log, settings toggle, and full DebugCommandKind dispatch via protocol v18 (#581) +- Entity-bound dialogue speaker colors — NPC colors assigned by entity ID (not screen position) with per-conversation lifecycle and round-robin palette (#573) +- Sova Transit District tile maps — 5 locations authored: The Terminal (44×28), The Last Shift (34×22), Maintenance Corridors (58×6), Gate Ground (40×34), Gate Gallery (32×10) (#582, #583) + +### Fixed +- LOS boundary walls — 1-tile wall margin beyond vision cone included in visible_tiles as BoundaryWall sector, walls at fog edge now render instead of bleeding into fog (#584) +- LOS boundary walls client — BoundaryWall tiles render through fog without marking explored, 4 new fog tests verify lifecycle (#585) +- Entity renderer test failures — updated 7 stale ColorRect/position assertions for Sprite2D migration, fixed SoundIndicatorRenderer class cache (#574) +- Dialogue speaker color contrast — re-enforce contrast floor after desaturation for passive (overheard) lines +- PROTOCOL_VERSION 17 → 18 mismatch — client rejected every server snapshot +- Debug console D-088 pause — sim now pauses while console is open, matching dialogue/settings overlay behavior +- Debug console settings toggle reads live state instead of ConfigFile, preventing checkbox divergence + +### Changed +- PROTOCOL_VERSION bumped 17 → 18 (debug_response field on ObserverSnapshot, DebugCommand PlayerAction variant) + +## [v0.1.22] — 2026-03-03 + +### Added +- Visual test harness — `make screenshot`, `make test-visual`, `make visual-update` for automated visual regression testing with golden PNGs across 11 scenarios (fog, HUD, dialogue, minimap) +- Visual movie mode — `make visual-movie` captures interaction flows as frame sequences with contact sheet generation +- World seed protocol — StartupMessage carries world_seed from client to server after handshake, enabling deterministic NPC population seeding (D-010, D-029) +- EntanglementConfig — per-seed NPC population ratios (flat/mundane/intrigue) sampled from seeded RNG with D-029 bounds, ensuring same seed = same world (#175, #178) +- Fog debug mode — toggle FogState.debug_exploration to render raw exploration texture as colored overlay for diagnostic use +- D-110 through D-112: z-level addressing, subterranean architecture, no instancing decisions +- Q-051: speech bubble indicator over speaking NPCs +- Sprint 22 "Wire" briefings (server, client, visual, CI, planning, joint) + +### Added (server) +- Production NPC pool generation — 23 authored Sova NPCs spawn with EntanglementTag (Flat/Intrigue) based on triangle membership (#176, D-029) +- Authored triangle instantiation — 5 Sova triangles (3 active forks, 2 passive tensions) loaded from content YAML with deterministic IDs (#188, D-087) +- Contamination activation mechanic — timer-based storyteller fires after 30 game-minutes, pressures active triangles, emits ContaminationEvent (#254) +- Modifications data model stub — Vec on chunk entities, round-trips through save/load for future construction DLC (#567, D-112) +- Zone Gate gauntlet room — two-zone test room with door boundary, zone crossing detection system (#512) +- Fuzzy map tests — 50-seed randomized testing of procedural maps against 4 structural invariants (#509) + +### Fixed +- Fog shader: silent compilation failure in OpenGL3 compat mode — removed `return` statements from fragment() which are not supported, causing fog overlay to render as no-op (root cause of Sprint 22 fog regression) +- Fog system: blocky stair-stepped edges at vision cone boundary — doubled Gaussian blur step size for D-066 compliant 6-8 tile smooth gradient (#569) +- Fog system: zero visibility in explored areas — switched bounds calculation from visible_tiles (empty in live server mode) to visible_positions, and removed shader guard that cut off gradient bleed into unexplored tiles (#569) +- Fog shader alpha tuned to D-059 spec: light fog 0.25-0.35 (was 0.25-0.55), deep fog 0.55-0.70 (was 0.78-0.90) — world content now visible through fog instead of hidden behind it (#563) + +### Changed +- Fog shader now distinguishes light fog (near cone, neutral dark) from deep fog (far from cone, zone temperature tint) with separate Perlin noise breathing cycles (8-10s / 15-20s) +- Zone temperature tint populated per-tile from server zone_id: bar=warm amber-dark, hub=cool blue-dark, corridor=neutral dark (D-059/D-046/D-077) +- Simplified vision cone from 3-sector (forward/peripheral/blind) to forward-only 120° arc — server sends only forward-cone tiles, client renders explored tiles behind the player with light fog overlay +- Simplified fog shader from 5-layer to 3-layer model (clear, explored, unexplored) +- Fog texture resize now preserves exploration data — tiles behind the player stay as light fog instead of reverting to unexplored black +- Updated D-015/D-017 perception decisions to reflect simplified cone model +- Moved connector scripts from db/connectors/ to tooling/db/ (#274) — backwards-compat symlink removed in #568 + +### Removed +- db/connectors symlink — all references now use tooling/db/ directly (#568) + +## [v0.1.20] — 2026-02-25 + +### Added +- Social site template schema — RoleSchema (#163), SpaceSpec (#164), TriangleDef (#106) with YAML deserialization, sample templates at server/data/templates/ +- Single-ownership model — TemplateOwnership component, TemplateReferenceMap resource, cross-template reference links preserved across save/load and tier eviction (#165, D-025) +- Triangle generation — intra-template constraint satisfaction assigns NPCs to triangle roles, minimum 2 triangles per template with fallback on imperfect seeds (#107) +- Triangle escalation system — tick_triangle_escalation runs per game-minute, tension increments toward ToleranceThreshold, TriangleCrisisEvent emitted on Active phase entry, ResolveTriangle stub command (#250, D-087) +- Protocol v16 — TriangleCrisisEventWire on ObserverSnapshot for future client rendering of triangle crises +- D-093: Sova Transit District spatial layout — 4 social sites (Terminal, Bar, Gate Cluster, Sector 3), 2 encounter nodes, zone palette, gate cluster 7-zone spec, z-level scheme (z=0 maintenance, z=1 main, z=2 observation gallery), 3 investigation paths, corridor widths +- D-094: Spatial hierarchy — chunk (64×64 sim) → block (128×128 sim) → district (4×4 blocks, 256×256 visual), supersedes D-014 estimate +- D-095: Horizon stations and transport lore — span gates (human-built, dual-use), horizon stations (alien-built, 4-8 apertures), "The Ring" per-system naming, sequential hop travel, The Loop internal tram +- Generator architecture workshop brief (ticket #562) — top-down pipeline for district generation, targeting Q-036 resolution +- SnapshotEventRouter — callable-based snapshot dispatch replaces inline if-has blocks in main.gd (#559) +- YamlParser shared utility — unified YAML parsing for UI strings and checklist conditions (#560) + +### Fixed +- Wire triangle crisis event queue into observer snapshot — clients now receive TriangleCrisisEventWire via protocol v16 (was always empty) +- Persist TriangleState in SaveStateV1 — triangle phase and tension survive save/load cycles +- Validate dangling with_role references in TriangleDef constraint validation +- Replace O(n²) fallback NPC assignment with BTreeSet; prevent same NPC assigned to two roles in one triangle +- Replace O(N*M) scan in apply_resolve_triangle with BTreeMap index for O(1) per-command lookup +- Add From impls for RoleId, TriangleId, StableId, TriangleCrisisEventWire — eliminate fragile .0 newtype access +- Consolidate near-identical unit tests with integration counterparts + +### Changed +- Sova station profile updated — horizon gates located at The Van Maanen Ring (800 AU), not on Station Sova; Admin Hub houses transit processing facility only +- game_state.gd: stationary_ticks and zone_id now read from server snapshot with deprecated client-side fallbacks (#557, D-020) +- dialogue_box.gd: decoupled from GameState and AudioManager via signals — zero direct autoload references (#558, D-020) +- main.gd: snapshot dispatch via SnapshotEventRouter, dialogue signal coordinator handlers (#559, #558) +- ui_strings.gd and checklist_evaluator.gd: delegate to YamlParser, ~140 lines of duplication removed (#560) + +## [v0.1.19] — 2026-02-25 + +### Added +- Sprint 20: Shape planned — 11 tickets (server 6, client 4, planning 1) covering template/triangle schemas, client refactors, and district layout design discussion +- Planning team ticket type in sprint-plan skill — supports design discussions with purpose-assembled agent panels, Qatux and SI for bookkeeping +- Client PR #70 merged — save/load client UI, F5/F6 quicksave/quickload (#554) +- Server PR #68 merged — Sprint 19 save/load, tier eviction, test infra (7 tickets, 2714 lines) +- Client PR #67 merged — Sprint 19 test infra, session management, debug overlay (5 tickets, 2547 lines) +- CI PR #69 merged — Sprint 19 test runners, IPC fixtures, protocol handshake, benchmark (4 tickets, 1297 lines) +- Test runner scripts — 7 bash scripts (run-rust, run-godot, run-ipc-fixtures, run-ipc-protocol, run-ipc-integration, run-ipc-benchmark, run-all) with structured JSON output (#270, D-030) +- IPC serialization fixtures — 5 msgpack fixtures with Rust generator, cross-language GDScript validation (22 assertions) (#271, D-030) +- Protocol handshake client — HANDSHAKING state in SimBridge, HandshakeMessage decode with 5s timeout (#556, D-020) +- IPC round-trip benchmark — p50/p95/p99 latency reporting, 5ms threshold (#342, D-020) +- Protocol version handshake — `HandshakeMessage` as first IPC frame before tick loop, forward-compatible input handling (#555, D-020) +- Protocol v15 — `save_result` field on ObserverSnapshot for client save/load confirmation +- State serialization primitives — `serialize_npc_to_frozen`/`deserialize_npc_from_frozen` with full D-024 10-axis coverage for tier eviction freeze/thaw (#96, D-026) +- Scope tag system — `ScopeTagKind` (Neighborhood, ActiveQuest, Colleague, KnownContact), `ScopePinned` marker, automatic assignment from KnowledgeGraph and RelationshipGraph (#98, D-026) +- Timestamp-based eviction — `LastInteractionTick` LRU tracking, `SimSpacePressure` resource, BinaryHeap eviction respecting scope-pinned entities, Active cap 80 (#97, D-026) +- Save/load ECS extraction — `save_to_file`/`load_from_file` via MessagePack, `SaveGame`/`LoadGame` IPC commands, `SaveLoadResultWire` on ObserverSnapshot (#553, D-085) +- ScopePinned eviction regression test — adversarial at-scale test proving pinned NPCs survive eviction even with oldest ticks +- Test infrastructure — Layer 3 integration test entry point, three-layer architecture per D-030 (#200) +- Information boundary negative tests — 4 tests proving no passive KG leakage, LOS fog holds, tier boundary holds, per-NPC save isolation (#272, D-010) +- gdUnit4 CI runner script — headless test execution via `run_gdunit4.gd` with exit code for CI (#205) +- Scene testing utilities — SceneHelper class with node existence, signal, and path helpers for gdUnit4 (#206) +- GameState apply_snapshot tests — 14 tests covering v2+ fields: game_time, facing, interactions, monologue, stance, inventory (#206) +- Game session management — per-game save directories under `user://saves/-/` per D-085, SessionManager autoload, main menu scene (#258) +- Debug visualization overlay — F3-toggled dev overlay with LOS rays, vision cone arcs, NPC path trails, knowledge confidence tags, tick timing sparkline (#348) +- SimBridge→TestHarness extraction — test simulation logic separated into dedicated RefCounted class with backward-compat proxy API +- Workshop outcomes files — formal closure for content-gap-analysis, KG-information-boundaries, v01-content-scoping, v01-gap-analysis, wiki-review +- D-087 through D-092 — recovered decisions from v01-content-scoping and wiki-review workshops (triangle config, pause system, content scope, voice registers, anchor lines, complicity theme) +- Q-030 through Q-039 — open questions from workshop backlog (seed schema, style guide, cultural ingredients, NPC architecture, PC archetypes, sacred/profane framework, district skeleton, generator pipeline, authored content estimate, gate topology) +- Decision ID claim system — `db/connectors/decision` CLI with `next`, `claim`, `check-dupes` commands to prevent cross-worktree D/Q/R ID collisions, pre-commit duplicate check +- D-085: Per-game save directory structure — every new game creates `user://saves//`, F5 quicksave, F6 quickload +- Q-029: Save file format design — long-term considerations for versioning, compression, integrity, metadata headers +- D-086: Renumbered insert icon system (was D-084 on visual branch) to resolve cross-worktree ID collision +- Save/load wireframe updated for D-085 — LOAD tab shows games grouped by directory with expand/collapse, QUICKSAVE slot, F5/F6 hints +- Sprint 19: Persist planned — 16 tickets (server 7, client 5, CI 4) covering save/load, tier eviction/scope, test infrastructure +- Character creation & game setup workshop brief — covers creation model, seed boundary, gate activation, quest seeding, game toggles (resolves Q-011) +- Protocol v14 — `poi_list`, `examine_result`, `player_knowledge` ObserverSnapshot wire types with live KG serialization (#151, #174, #264) +- Minimap rendering — circular 160px diegetic insert overlay with POI dots (colored by category), border arrows for distant POIs, player-centered fixed-north (#151) +- Dialogue UI hardening — confrontation italic voice (D-063), examine result overlay with 5s auto-dismiss and confidence coloring (#174) +- Knowledge/journal panel — right-side insert panel (J key), facts grouped by entity, contradicted entries in amber with strikethrough, stale entries dimmed, mutual exclusion with dialogue (#264) +- Sprint 18 client test suite — 50 gdUnit4 tests for dialogue (D-062, D-063, D-064) and journal (KG parsing, scene structure, UIStrings), plus test plan document +- D-084: dual-namespace line ID scheme for auto-generated NPCs — role pool (shared, unchanged) + instance override (opt-in, seeded counter). Resolves Q-028 (#544) +- Tier 1 drama module schema (`content/schemas/drama_module.schema.yaml`) — entry conditions, NPC requirements, event sequences, outcomes, pool format (#158) +- Smuggling ring v0.1 stub module (`content/modules/tier1/smuggling_ring_v0_1.yaml`) — vertical slice Tier 1 module with 6 NPC roles, dual event sequences, 5 outcomes (#158) +- Line ID authoring guide (`docs/design/line-id-authoring-guide.md`) — dual-namespace conventions for hand-authored and auto-generated NPC content +- Tier 1 module authoring guide (`docs/design/tier1-module-authoring.md`) — field reference, NPC pattern/motivation tables, design principles, pre-submission checklist +- Background tier state machines — schedule, mood, relationships, job tick once per game-minute for Background NPCs (#95, D-026) +- NPC vision system — symmetric shadowcasting for Active-tier NPCs, NpcMemory with last-known-position and zone inference (#115, D-011) +- NPC player-awareness behavior — PlayerAwareness component tracks LOS duration, suspicion accumulation, routine deviation triggers (#244) +- Skill system & combat flag — SkillSet component (BTreeMap), CombatCapability marker from combat_trained skill (#91, D-024) +- Player-action social propagation — three-order trust ripple (100%/40%/20%) through RelationshipGraph with cycle prevention (#249, D-029) +- Examine mechanic — process_examine_interaction with character-filtered observation text, KG DirectObservation write, examine_result in ObserverSnapshot (#242) +- Character goal/pressure framework — CharacterPressure component (exposure/institutional/relationship), wired to snapshot HUD data (#248) +- Save state data model — SaveStateV1 struct with MessagePack serialization, roundtrip tests for entity/KG/relationship/clock state (#256) +- Tell state derivation wired into ObserverSnapshot — integration tests for Nervous tell on Major secret + high stress (#337) +- Sprint 18: Touch planned — 14 tickets (server 9, client 3, copy 2) covering examine mechanic, NPC awareness, social propagation, minimap, dialogue UI, save state model +- `.claude/rules/` directory — modular auto-loaded instructions (tea-cli, git-safety, project-structure, team-patterns, local-services) +- KnowledgeGrant untagged enum with Fact and Entity variants, ContentEntityRegistry for NPC spawn-time entity resolution (D-079, #545) +- KnowledgeGranted event processing — grants fire at dialogue line selection, runtime NPC KG guardrail (D-079, #546) +- ContradictionClaim struct with 600-tick window detection in observe_entity, epistemic neutrality for both sources (D-083, #547) +- NPC-to-NPC knowledge transfer system — trust-gated fact exchange, confidence capping at KnowsOf, ToldBy source construction (D-080, #548) +- tell_state KG awareness — NPC relationship reads from KG for other-entity state, MVP information boundary (D-082, #549) +- Contradiction monologue with pre-resolved entity names, PersonOfInterest relationship shift, THE FRIEND arc event chain (D-083, #550) +- Unprompted disclosure system — DisclosureCandidates component, 7 trigger gates, three-layer rate limiting, two-stage trait filter (D-081, #551) +- Trait modifier system — Cautious/Gossipy/Loyal/Talkative filter predicates via content-authorable config (D-081, #173) +- POI data model and proximity-based discovery system via KnowledgeGranted events (#148, #149) +- Protocol versioning tests — version round-trip, mismatch detection, serde_default migration pattern, full variant coverage (#232) +- Team monitoring rules — heartbeat rule for stuck agent detection, bottleneck detection pattern +- `tooling/tea-comment` — single-command wrapper for posting Gitea PR/issue comments with multi-line bodies +- D-086: Insert icon system — custom SVG icons over icon fonts, authored to insert geometric constraints with lattice_profile weight scaling +- Insert/HUD wireframe and visual spec (#314) — dual character variants (smuggler social network view, detective investigation overlay) with pixel-precise layout, entity markers, time display, border arrows, commission grid, and all interaction states +- Contradiction monologue lines — 16 hand-authored lines (8 detective, 8 smuggler) for Sera/Kael FRIEND arc, Phase 2 blindsiding + Phase 3 pattern recognition, cognitive-dissonance-not-accusation tone per D-083 (#552) +- Diegetic tutorial monologue — 20 lines (10 per character) teaching movement, fog, sound, NPC interaction, and insert/HUD through character voice, fire-once on first-time events (#330) +- Diegetic time display on insert HUD — station local time (HH:MM), day phase with cycle-tinted color, day number on InsertOverlay (#263) +- Relationship color accent on E-Talk overlay — 3px left-edge bar using D-033 palette signals NPC relationship at a glance (#537) +- `Constants.format_game_time()` helper for converting game-minutes to HH:MM station time +- `/sprint-status` cleanup sweep skill — consistent health report with tickets by status, PR cross-reference, bookkeeping issue detection, and open work by team +- `sprint sweep` CLI subcommand — structured JSON output for sprint health checks (grouped tickets, per-team summary, issue detection) +- Knowledge Flow & NPC Boundaries workshop — 5 D-records (D-079–D-083) covering grant architecture, NPC-to-NPC propagation, unprompted disclosure, NPC information boundaries MVP, contradiction detection pipeline +- 7 knowledge graph implementation tickets (#545–#551) with full dependency chain and line estimates +- Contradiction monologue content ticket (#552) for Sera/Kael FRIEND arc +- Sprint 17 completion proofs: contradiction detection fires, NPC-to-NPC knowledge transfers +- Entity renderer migrated from ColorRect placeholders to Sprite2D with D-019 angle sprites — self_modulate for D-033 tinting, 8→4 octant direction mapping, feet-anchored y-sort (#540) + +### Fixed +- Client protocol version bumped to 15 to match server (was still at 14 after server PR #68 added save_result field) +- gen_fixtures.rs version comments changed from hardcoded 14 to PROTOCOL_VERSION constant +- run-ipc-benchmark dead --iterations flag removed (Rust compile-time constant governs rounds) + +### Changed +- Team boundary framing — replaced worktree-centric language with `$WORKTREE_TEAM` env var identity across CLAUDE.md and skills (sprint-start, sprint-plan, pr-review) to prevent agents from following `.git` pointers across boundaries +- CLAUDE.md compacted from 188 to 67 lines — CLI references, endpoints, and patterns moved to `.claude/rules/` +- `/sprint-status` delegates to haiku subagent — keeps sweep JSON, template read, and PR list out of main context window +- `sprint sweep` JSON trimmed — removed unused fields (`ok`, `sprint.status`, `priority`, `ticket_id`), shortened issue detail strings +- Sprint status output template condensed — rendering rules moved to skill definition, bookkeeping table simplified to 2 columns +- Model selection documented in CLAUDE.md — `/model sonnet[1m]` and `/model opus[1m]` for 1M context sessions +- Sprint 17 briefings updated with workshop results — server (14 tickets), copy (2 tickets), client (2), visual (1) +- Q-024 (gossip timing), Q-025 (KG memory), Q-026 (contradiction detection) closed +- Sprint 16 closed (8/8 done) +- 3D sprite render pipeline — Camera3D at D-019 angle (-72.5° from horizontal), three-point studio lighting rig, orthographic projection, resolution chain 1024→256→64 +- Generic NPC capsule model (24×32px footprint per D-044) and structural wall model for pipeline validation +- Test sprites: 8 runtime 64px sprites (NPC + wall × 4 directions) deployed to client/assets/sprites/ +- Pipeline documentation (renderer/README.md) — camera spec, lighting rig, resolution chain, model authoring guide +- DialogueResponse verb handler — players pick dialogue options and receive follow-up lines via full D-028 four-layer pipeline (#539) +- Trust-gated gossip verification — integration tests confirm Secret/Real/Surface tier gating per D-075 (#171) +- Line variety tracker wiring — DialogueCooldownTracker prevents repeat lines within 600-tick window (#338) +- DialogueResponse cross-language fixture for GDScript testing +- Sprint team lifecycle through PR review — teams stay alive for commit → push → review → fix loop → approve → shutdown +- Zone_id extraction in game_state.gd optimized from O(N) tile scan to O(1) dictionary lookup — builds _tile_by_coord from member visible_tiles covering both test and live paths (#543) +- Shared run_dialogue_pipeline() helper eliminates ~60 lines of duplication between Talk and DialogueResponse systems +- Dialogue and monologue line IDs migrated from location-scoped (the-terminal_d_039) to NPC-scoped (kael-davan_d_001) namespace — each NPC has an independent sequence per D-035 (#542) +- DialogueCooldownTracker documented as per-player-global by design (NPC-scoped line IDs per D-035 prevent collision) +- CONFRONTATION_LINES marked TODO for migration to D-028/D-035 content pipeline +- pr-push and pr-review skills updated with team lifecycle awareness + +### Fixed +- PR #59 review: stale mood vocabulary updated in line-pool-format.md, style-guide, and content-directory-structure.md to post-Sprint 14 values +- PR #59 review: orphaned location-scoped IDs in maintenance-tech.yaml comments and smuggler-inventory.yaml cross-references updated to NPC-scoped +- PR #59 review: Lera Sessik tenure corrected from "twelve years" to "eighteen years", NPC header fixed +- PR #59 review: ring-operative.yaml fact_id corrected from `location.surveillance_gaps` to `investigation.surveillance_gaps` +- Dialogue systems moved from BridgePlugin to NpcPlugin — game logic registers where it belongs (#538) +- Schedule ambiguity: emit_observation_events now has explicit .before(advance_tick) constraint +- process_dialogue_response updates ActiveDialogue tick and InteractionMemory on follow-up +- DialogueResponse range check added (CLOSE_RANGE, matching Talk/Confront pattern) +- Weighted selection fallback replaced with unreachable!() — dead code removed +- assert!(false) → panic!() in serialization tests (clippy) +- SetFacing and TeleportToHub added to roundtrip test coverage + +## [v0.1.15] — 2026-02-23 + +### Added +- Sprint 16 "Converse" briefings — 8 tickets across server/client/copy/visual teams +- 19 UI wireframes — HUD, dialogue, monologue, popups, menus in v0.1 and v1.0 variants with D-record cross-references +- d2-diagram skill — text-to-diagram generation with project defaults (theme 200, dagre, PNG) +- frame0-wireframe skill — UI wireframing via Frame0 HTTP API, replaces MCP dependency with bash+curl +- 16 decision diagrams — architecture, data-flow, entity, state, and UI categories covering all project decisions + +### Changed +- frame0-wireframe skill rewritten — JSON-as-truth workflow with frame0-sync.py, batch export, renderer-only guidance +- pr-review skill — all reviewer agents now use worktree paths instead of git show +- Dialogue panel is always visible as permanent insert UI element (D-061) +- Makefile: check-protocol target verifies server/client protocol versions match before build +- D-035 amended: line ID namespace changed from location-scoped to NPC-scoped (Sprint 15) +- Tilemap z-layer filtering — FloorTiles renders z=0 only, z=1/z>1 reserved for future layer nodes (#71, D-049) +- Entity 24x32 footprint per D-044 visual hierarchy — split ENTITY_SIZE into WIDTH/HEIGHT with separate offsets (#72) +- Follow target stub on GameState — `follow_target_id` field ready for server #241 Follow verb +- Manual exponential camera smoothing — CAMERA_SMOOTHING_SPEED constant (8.0), same lerp pattern as entity renderer (#117) +- 31 new Sprint 15 validation tests — camera smoothing, UI framework z-layers, entity footprint, Sprint 14 regressions +- SpatialIndex trait with naive Vec implementation — entities_in_range, entities_at, update methods with Manhattan distance (#340) +- NPC generation pipeline — procedural seeding of all 10 D-024 axes via SimRng with constraint validation (#92) +- Personality and tell system — 5 tell categories (Nervous, Angry, Friendly, Guarded, RoutineDeviation) derived from NPC axis values each tick (#90) +- Tolerance threshold monitoring — ToleranceBreachEvent on stress exceeding per-NPC threshold, mood FSM integration (#105) +- Routine deviation detection — RoutineDeviationEvent on wrong location/activity for day phase, absence detection, pathfinding-aware (#243) +- Follow mechanic — Follow verb, proximity/LOS tracking, double-frequency observation events, NPC suspicion accumulation, configurable thresholds (#241) +- Monologue event triggers — observe_npc, hear_sound, observe_anomaly, witness_interaction, post_conversation with D-035 context tags (#119) +- Protocol v13 — tell_state on VisibleEntity, follow_state on ObserverSnapshot, Follow verb + +## [v0.1.14] — 2026-02-21 + +### Added +- Unified dialogue log — player-NPC and overheard NPC-NPC conversations in one chronological scrolling panel (#535, D-061/D-078) +- F3 debug overlay — real-time game state display with tick, FPS, position, entity counts, dialogue/monologue status (#511) +- Monologue display — multi-line priority queue with character colours, italic BBCode, stagger animation (#122) +- Protocol v9 — conversation_events, conversation_ended, dialogue_response fields with carry-forward logic +- Dialogue theme system — configurable NPC name colour palette, entry timing, passive opacity via dialogue-theme.yaml +- Monologue display system visual spec — typography, positioning, stacking, priority, fade animation, character color differentiation, 80-char line constraint (#315) +- Entity color system spec — D-033 relationship-to-player mapping, transition animations, color blindness assessment (#304) +- Text display hierarchy spec — 4 content pipelines (dialogue, monologue, observation, environmental) with z-layers and positioning (#316) +- Sound indicator visual design — fog-edge pulse for D-018 three-range sound model with direction encoding and range differentiation (#317) +- THE FRIEND visual treatment spec — 3-phase earned visual detail for Kael Davan and Sera Venn (#318) +- Environmental text visual standards — signage, terminal, and news ticker rendering with bilingual Concordat/Van Maanen's Star treatment (#334) +- Tell visual/behavioral expression spec — 5 tell categories mapped to 6 Tier 2 behaviors (#251) +- Monologue line pool maxLength raised from 160 to 256 chars (soft guidance ≤160) +- NPC name masking infrastructure — entity-anchored dialogue log with server-side role labels, retroactive name update on learning, NpcColorIndex for stable color assignment +- Dialogue option keyboard selection (1/2/3 number keys) and numbered option labels +- Interaction list chrome — background panel, mouse hover highlighting, click-to-interact, pointing hand cursor + +### Changed +- D-061 updated to document unified conversation log architecture from Sprint 14 +- Dialogue options switched from RichTextLabel to Label for reliable VBoxContainer sizing + +### Fixed +- Visual grammar dialogue max-width corrected from "~70% screen width" to 640px per D-076 +- BBCode injection in dialogue log formatting — server-sourced strings now escaped with [lb] +- Per-frame dialogue log rebuild replaced with dirty flag (performance) +- dialogue_active lifecycle — now cleared after panel fade completes per D-064 +- PAUSE/UNPAUSE routed through main.gd input recording for bug report replay (#507) +- WASD input freeze after filing bug report — LineEdit focus not released before queue_free() across CanvasLayers +- WASD not reactivating after Talk — dialogue_active held for entry_lifetime instead of cleared immediately +- Recognition chime spam — entity IDs now tracked permanently per room instead of expiring +- Audio path warning — res://audio/ corrected to res://assets/audio/ in AudioManager +- world_radial.tscn anchors_preset warning — changed from 15 to 0 +- bug_report_dialog.gd push_warning changed to print for informational message + +## [v0.1.13] — 2026-02-20 + +### Added +- D-078: Overheard NPC conversation — passive dialogue panel with server-authoritative stochastic word occlusion +- Sprint 14 "Live" briefings — 22 tickets across server (7), client (3), copy (6), visual (6) + +## [v0.1.12] — 2026-02-19 + +### Added +- Tier marker components (#93), active tier simulation (#94), tier transition logic (#99) +- Information tag schema (#138), component-level access control (#139) +- Line previewer CLI (#193) +- Sound event system — server pipeline (#124) +- Close-range stereo audio — client positional 2D (#125) +- Medium-range visual indicators — fog-edge directional arrows (#126) +- HashMap ban in simulation crate via clippy (#343) +- Tracing crate infrastructure — JSON format, tick duration logging (#344) +- System dependency graph debug command — `--dump-schedule` CLI flag (#346) +- rng_seed field on ObserverSnapshot for deterministic replay (#527) +- v0.1 Visual Grammar Document (#303) +- Placeholder art specification (#252) +- Spatial layouts: Logistics Hub (#311), Bar (#312), Smuggling corridors (#313) +- Cultural generation guide — 5-dimension framework for Sova Transit District cultural voice (#189) +- Sova Texture Appendix — 20-term slang glossary, sensory profile, Meridian self-censorship rules (#302) +- Contraband specification — unlicensed lattice components, supply chain, street terminology (#321) +- Sova Station Profile — 6 districts, governance, off-station references (#320) +- Span Gate Transit Schedule — hourly schedule, maintenance windows, ring operational calendar (#336) +- Meridian Coverage Map — 10 named zones from Commission-grade to dead air (#335) +- Character definition schema and both character builds — smuggler + detective (#179, #180, #181) +- Divergent starting knowledge and relationships per character (#182, #183) +- Detective institutional chain of command (#322) +- Contradiction arc design document — reusable FRIEND pattern (#332) +- Mirror moment design document — 7 core dual-perspective observation triggers (#329) +- First 5 minutes experience design — systemic opening per character (#259) +- Opening hook content per character (#260) +- Knowledge vocabulary for v0.1 content — entity/world categories, prerequisite format (#368) +- Knowledge state vocabulary — author-facing quick reference (#309) +- Knowledge fact catalogs — 10 YAML files in content/global/knowledge/, 73 canonical facts +- D-075 endorsement — archetype dimension review recorded in decisions/content.md +- Flat NPC memorable trait pass — Pael, Ren, Tev with noise-floor profiles (#307) +- Environmental text content — 20 items across Terminal, Bar, and Corridors with dual-lens notes (#262) +- Diegetic insert flavor text — per-character labels and notification strings (#331) +- News ticker / Meridian feed — 30 lines including batch 44xx recall dual-lens moment (#306) +- Workplace content pack — The Terminal: 5 NPC dialogue files (#190) +- Bar content pack — The Last Shift: 3 NPC dialogue files (#191) +- Smuggling ring content pack — maintenance corridors: coded vocabulary, dual registers (#192) +- Generation pass expansion — 80 ambient variant lines across all 9 dialogue files (#194) +- Sprint 13 "Sound" briefings — 9 tickets across server, client, audio, visual teams; full audio architecture + gauntlet expansion + monologue display spec + +### Fixed +- Entity renderer field name bug — `id` vs `entity_id` (#345) +- Dialogue max-width pixel value — 640px per D-076 (#447) +- Routine tests missing ActiveSim — 3 of 5 tests passed trivially without the required tier marker +- `_observer_pos` misleading unused prefix renamed to `observer_pos` (used for sound event filtering) +- Stale protocol version doc comment "Current: 9" corrected to 10 +- FactionOnly non-numeric `faction_id` attribute now logs a tracing::warn instead of silently denying +- SOUND_EVENT_ASSETS walk-speed key mismatch — `sfx_footstep_metal` corrected to `sfx_footstep_metal_walk` + +### Changed +- Removed orphaned `SimulationTier`/`LastInteraction`/`ScopeTag`/`ScopeKind` types from tier.rs (unused outside own tests) +- Sound pipeline documented as intentionally empty in v0.1 (no producers yet, full pipeline wired) +- Observer test setup now inserts SoundEventQueue resource for integration coverage +- Added FactionOnly positive test case and Medium-range occlusion TODO +- Sound indicator colors sourced from Constants instead of duplicated hex literals +- play_loop() null guard on stream.duplicate() +- Camera zoom fallback uses Constants.CAMERA_DEFAULT_ZOOM + +## [v0.1.11] — 2026-02-19 + +### Added +- Sprint 12 "Build" briefings — 50 tickets across server, client, copy, visual, ci teams; production-layer foundations + all v0.1 copy authoring +- `.tmp/` gitignored repo directory for agent temp files — avoids Bash permission prompts during PR review comment posting +- `sed -n` blanket permission in shared settings + +### Changed +- All skills renamed to domain-action convention (e.g. `commit`→`git-commit`, `review-pr`→`pr-review`, `gen-audio`→`audio-gen`, `render-sprite`→`sprite-gen`) — 12 renames total +- `pr-review` skill uses Write tool into `.tmp/` instead of Bash heredocs to `/tmp/` + +## [v0.1.10] — 2026-02-19 + +### Added +- `project.yaml` — technical project descriptor with version, architecture, simulation, and content model as the canonical version source of truth +- Scratchpad: asset generation pipeline idea (registry, status tracking, prompt versioning, pre-sprint cohesion) +- Scratchpad: remote terminal proxy idea for mobile monitoring of Claude Code permission prompts and interactive elements +- `make perf-baseline` — full plugin stack tick benchmark (50 measured ticks, 5 warmup) capturing per-tick timing, entity counts, process RSS, and shadowcast benchmarks; outputs structured JSON to `tests/perf/baseline.json` with `--compare` mode for regression detection (>20% threshold, D-026 budget check) +- Michroma font integration (#517) — Michroma-Regular.ttf as game font with +1px tracking FontVariation, global Theme with cyan-white (#E0F7FA) implant text color, IMPLANT_TEXT_COLOR/DIM/PULSE constants +- Mouse-relative facing and movement (#526, D-054) — mouse position determines facing direction (client-side float), WASD remapped to cursor-relative (W=toward, S=away, A/D=strafe), SET_FACING action sends octant to server, smooth facing indicator rotation +- Room reset client UX (#502) — amber reset_plate tile type, 0.15s screen flash on room reset, 'Reset Room' interaction verb +- Auto-checklist progress tracking (#503) — ChecklistEvaluator parses room YAML and evaluates 7 condition types against GameState with latching, ChecklistOverlay renders progress in gauntlet mode only, 48 new tests +- 4 ambient zone loops: station base, workplace, bar, corridor — SAO-generated organic soundscape with crossfade loop points (#327) +- 2 footstep SFX: metal walk and run — SAO hybrid with best-transient extraction (#327) +- `audio-batch` command — batch audio generation from JSON manifests, supports SAO and harmonic synthesis, with `--dry-run`, `--only`, and `--skip-existing` flags +- `--post` and `--output-ogg` flags on `audio-generate` — chain post-processing (trim, normalize, convert) into a single command + +### Changed +- `push-pr` skill now runs `/commit` first when uncommitted changes are detected +- Insert open/close now sends explicit PauseSimulation/ResumeSimulation (#518, D-058) — replaces toggle-style pause with idempotent pair +- Interaction list colors reference Constants.IMPLANT_TEXT_COLOR instead of hardcoded values +- World radial menu uses theme font instead of ThemeDB.fallback_font +- Monologue chimes replaced with production-quality manual synthesis — insert-tech aesthetic per D-074, pure sine harmonics with mathematical envelopes (#327) + +### Fixed +- Bidirectional relationship check (#515) — Check 9 tested `target in npc_rels` which missed NPCs with no relationship entries; changed to `target in self.npcs` + +## [v0.1.9] — 2026-02-18 + +### Fixed +- `make game` now builds client before launching — was missing `build-client` dependency, causing class_name registration failures after `make clean` +- `build-client` uses `--import --quit` instead of just `--quit` — ensures `.godot/` cache and `global_script_class_cache.cfg` are created from scratch +- `make clean` preserves `client/.godot/` directory (clears contents only) to avoid Godot startup issues + +### Added +- `--description TEXT` flag for `ticket create` CLI — previously required raw SQL workaround to set ticket descriptions + +### Changed +- UI audio assets revised — monologue chimes re-generated (0.8s, insert-tech aesthetic), fog_recognition re-generated (was silent), all 8 assets normalized to 44.1kHz stereo LUFS -16 (#453) +- review-pr skill: explicit verdict rules — critical/warning → REQUEST_CHANGES, suggestion-only → APPROVE + +### Added +- `make golden-diff` + `make golden-update` targets (#486) — developer workflow for golden file comparison and regeneration; safe restore on cargo failure +- Gauntlet checklist YAML schema (#497) — 7 condition types evaluable from ObserverSnapshot, per-room checklists for 3 rooms, `make checklist-validate` and `make checklist-generate` targets, wired into `pre-pr-content` gate +- Gauntlet room timer + personal bests (#496) — GauntletHUD shows TIMER: MM:SS (PB: MM:SS), starts on room entry, resets on room change, persists stats to user://dev/gauntlet-stats.json, session summary on disconnect, hidden in non-gauntlet mode +- WRONG button F12 MVP (#495) — bug report capture: pause sim, show modal prompt, save snapshot.json + render.txt + description.txt to user://bug-reports/, Esc to cancel +- GameState.room_id and gauntlet_mode fields — parsed from ObserverSnapshot, enabling gauntlet UI +- BUG_REPORT action in InputMapper (F12 binding) with SimBridge wire guard (client-only) +- 24 new anti-tedium tests — GauntletHUD lifecycle (16: timer, PB, visibility, room change, session tracking) + BugReportDialog (8: pause/unpause, wire guard, text render, edge cases) +- whatsinagame starter kit — reusable multi-agent team bootstrap for any project (3-tier profiles, 18 skills, 16 agent archetypes, stakeholder personas, ticketing DB, decision tracking) +- Domain-action naming convention for skills documented in create-skill guide +- `gauntlet` feature flag (default-on) — allows stripping Gauntlet test world from release builds with `--no-default-features` +- `EXPECTED_ENTITY_COUNT` and `RESET_PLATE_STABLE_IDS` constants — entity counts derived from StableId ranges instead of hardcoded values +- Debounce exact-boundary test for room reset (tick 9 rejected, tick 10 accepted) +- Reset plate StableId verification in `stable_id_ranges_match_spec` +- Runtime content test (`content_runtime.rs`) separated from structural loading tests +- Client P2 tests (#492) — 16 gdUnit4 tests for camera (smoothing, zoom, viewport, follow, no-pan), entity alpha/color (peripheral, forward, NPC color, player constant), UI (monologue, interaction, inventory, dialogue, pause, fog blob, fog z_index) +- Client P3 tests (#493) — 12 gdUnit4 tests for z-layer ordering (floor/ysort/fog/UI), entity lerp (snap, converge, LERP_SPEED=12.0), Tyre additions (recognition, facing, delta scaling, blob removal) +- Anti-tedium regression tests (#494) — 5 tests: F12 no-crash guard, no queued input, gauntlet UI hidden in default/normal snapshot/multi-tick modes + +### Changed +- Room reset API consolidated to `plan_reset` only — `execute_reset` removed (was a maintenance trap; production uses Commands via `plan_reset`) +- `room_at()` documented with z-range and corridor overlap assumptions +- TCP runtime test now has 10s read timeout and registers player in EntityRegistry + +### Removed +- Dead `RoomMember` component from reset.rs (defined but never used) +- Protocol v8: dialogue_response field decoding (DialogueResponseEvent with line_id, text, speaker_entity_id) from server #305/D-028 +- Test client binary scaffolding (#480) — standalone crate at `tooling/test-client/` with CLI (--connect, --replay, --text, --json, --quiet, --golden, --ticks), exit codes (0/1/2), golden file JSON diff, JSONL replay loader +- Snapshot text renderer (#481) — `format_snapshot_text(&ObserverSnapshot)` pub-exported from server crate, entity labels as kind:entity_id sorted by distance, room name stub, 10 unit tests +- Sprint 9 (Gauntlet) briefing files — server, client, CI, audio, joint — 23 tickets across 4 teams +- Weapon aim lock audio (`sfx_weapon_aim_lock.ogg`) — clinical targeting confirmation tone for weapon aim state (#440) +- Stance change audio (`sfx_stance_change.ogg`) — subtle mechanical click for stance toggle feedback (#440) + +### Fixed +- MessagePack int_64 encoder dead code branch (#516) — `-(1 << 63)` overflowed making int_64 branch unreachable; negative values beyond int_32 now correctly encode as 0xd3 instead of 0xcf +- Cross-encoder fixture pipeline hardened — encode failures now exit non-zero instead of writing empty .msgpack files (#475 review) +- GDScript fixture test no longer silently skips on missing/empty fixture dir — asserts instead (#475 review) +- GDScript fixture generator covers all PlayerAction variants (added MoveSouth, MoveEast, MoveWest, Unpause, ToggleStanceDown, WalkAway) +- `make pre-pr` now checks GDScript fixture staleness alongside Rust fixtures +- Protocol version bumped from 7 to 8 to match server — fixes 5 test failures from version mismatch +- Interact action encoding changed from unit variant to struct variant to match server's PlayerAction::Interact { target_entity_id, verb } +- Monologue duplication test (test_monologue_not_duplicated_after_consumption) fixed — was using poll_snapshot() which doesn't consume _last_snapshot in test mode +- `make pre-pr` target — full pre-PR verification chain: lint → build → test → content validation → fixture staleness (#460, #465) +- Branch-specific pre-PR variants: `make pre-pr-server`, `make pre-pr-client`, `make pre-pr-content` +- Content cross-reference validation (9 checks) — canonical_id uniqueness, relationship targets, location slugs, dialogue locations, fact_ids, triangle membership, npc_count, dialogue line_ids, bidirectional relationships (#464) + +### Fixed +- Dialogue schema missing `focused` (mood) and `greeting` (situation) values added in Sprint 7-8 content +- Speaker wire ID silent fallback — dialogue now warns and skips when target entity missing from registry (was silently using 0) +- Cross-plugin system ordering — trigger_recognition_monologue now runs after detect_anomalies (latent determinism bug) +- Walk-away ordering — process_walk_away now runs after process_talk_interaction (prevents same-tick race) +- ActiveDialogue overwrite — new Talk while in existing dialogue now emits IncompleteInteraction before replacing +- Server-side Talk range check — handle_talk now enforces CLOSE_RANGE before setting TalkRequest (was client-only) +- ExamineNpc label collision — VerbKind::ExamineNpc now uses "Examine NPC" label (was "Observe", same as generic Observe) +- Dead conditional in main.rs collapsed (both branches were identical) +- WalkAway variant added to all_player_action_variants_roundtrip serialization test + +### Changed +- DialogueCooldownTracker.used changed from Vec to BTreeMap for O(log n) lookup (D-041 compliance) +- MonologueState.shown_ids changed from Vec to HashSet for O(1) contains check (was O(n) per tick) +- Secret trust tier documented as unreachable with TODO for Phase 2 KG-gated unlock + +### Added +- Determinism test: different_seed_produces_different_replay — exercises SimRng via dialogue weighted selection +- Dialogue selection pipeline (#305, D-028) — 4-layer filtering engine: access tier from KG relationship, situation derivation from game state, trust tier, weighted topic+mood scoring via SimRng. Full Talk verb → selected line → ObserverSnapshot pipeline with cooldown tracking +- ContentSlug component (#452) — stable content identity from YAML (e.g. "kael-davan") independent of runtime Entity handles, for knowledge graph interaction memory across save/load +- Walk-away KG recording (#427, D-064) — IncompleteInteraction knowledge events with Talk/Confront type, ActiveDialogue tracking, walk-away detection clears dialogue and records in KG +- Anomaly detection for urgent recognition (#450, D-060) — AnomalyMarker flags PersonOfInterest/Contradicted entities for 0.3s cognitive delay instead of 0.6s normal +- Recognition monologue during cognitive delay (#451, D-060) — monologue fires at delay START (grey blob phase), not completion. v0.1 fallback lines, anomaly prioritization, cooldown tracking +- Server --test-mode, --port, --seed CLI flags (#459) — LISTENING:{port} stdout signal, OS-assigned ports, deterministic seed override, stderr-only tracing +- Determinism gauntlet test (#466) — 20-tick replay determinism regression test with movement, stance, pause/unpause exercise +- Pause guard test suite (#461-463, #468) — 7 tests covering movement, unpause, roundtrip, stance, interact, batch, tick_rate during pause +- EntityRegistry lifecycle tests (#469) — stale mapping, re-register, unknown unregister edge cases +- Boundary value encode/roundtrip tests (#471) — 41 values across all MessagePack integer format boundaries +- Encoding asymmetry tests (#473) — Rust decoder accepts GDScript-style signed encodings for unsigned fields +- Boundary fixture generation (#472) — 14 raw + 5 snapshot fixtures at integer format boundaries +- Malformed batch rejection test (#479) — truncated, garbage, and mixed payloads rejected atomically +- Per-fix determinism unit tests (#467) — equidistant NPC ordering, visible tile sorting, same-tile mover resolution + +### Fixed +- Determinism: visible_ids HashSet → BTreeSet for stable iteration order (#456) +- Determinism: visible entities in snapshot sorted by entity_id (#457) +- Determinism: movers sorted by Entity bits in validate_movement (#458) +- Pause guard blocks all actions except Pause/Unpause while paused (previously only blocked movement) + +### Changed +- Protocol version bumped from v7 to v8 (dialogue_response field in ObserverSnapshot) + +### Added +- AudioManager autoload (#255, D-068/D-069/D-073) — 5-bus architecture (Music, Ambient, WorldSFX, PlayerActions, UISounds), directory-scan asset registry, spatial/non-spatial playback, audio dip profiles (dialogue, confrontation, listening_focus) with low-pass filter sweep, zone crossfade stub +- Dialogue response selection (#435, D-061/D-062) — structured options with response_id, priority sorting, max 3 visible, invisible locked options, RichTextLabel for BBCode support +- Walk-away mechanic (#437, D-064) — WASD triggers WalkAway input during dialogue, 300ms fade, dialogue_active flag gates movement, re-show on server interrupt +- Confrontation text styling (#436, D-063) — italic first-person options, 1.5s monologue beat with dialogue dim to 70%, audio dip via AudioManager, walk-away cancels in-flight beat +- MessagePack boundary value tests (#470) — 41 values (25 positive, 16 negative), encode-only header verification, roundtrip, Rust-style unsigned decode overlap tests +- Client P0 regression tests (#477) — monologue carry-forward (Bug #5), camera stability during pause (Bug #2) +- Client P1 tests (#478) — fog shader state (4), entity lifecycle (2), pending recognition blob (1) + +### Changed +- Fog byte magic numbers replaced with named constants (#476) — VIS_HIDDEN/PERIPHERAL/FORWARD, EXP_UNEXPLORED/EXPLORED/VISIBLE in FogState +- Dialogue options now carry structured {text, response_id, priority, confrontation} instead of plain strings +- Protocol v7 dialogue decode validates and skips malformed options + +### Added +- QA test architecture workshop complete — 3-round, 7-agent workshop producing 60 tickets (epic #455): Gauntlet test world (7 rooms, 48 entities), test client binary (tooling/test-client/), determinism fixes, content validation, make pre-pr pipeline, 38 client tests, anti-tedium features, human tester workflow +- Workshop skill updated — agents now write output files to disk instead of sending messages, fixing documenter access +- Camera anchor test suite — 10 gdUnit4 tests verifying camera init, smoothing cycle, and player tracking +- Entity position lerping — framerate-independent exponential smoothing so entities slide between tiles instead of snapping + +### Fixed +- Server never sends snapshots — blocking TCP read in receive_bridge_inputs stalled the entire bevy Update schedule; switched to non-blocking I/O with WouldBlock handling +- Camera doesn't center on player at startup — Camera2D smoothed_camera_pos starts at (0,0); now disable smoothing during init, snap to player, re-enable after first anchored frame +- Player moves while game is paused — movement commands now discarded when TickRate is Paused (pause/unpause still process) +- Spacebar only pauses, doesn't toggle — added UNPAUSE action with toggle logic based on tick_rate state +- MessagePack encodes tick 128 as -128 — off-by-one in signed int boundary checks (<=128 instead of <128) across int8/16/32/64 branches +- Monologue/dialogue lost on snapshot overwrite — one-shot events now carried forward when a newer snapshot replaces an unconsumed one +- Fog shader white screen on load failure — ColorRect defaults to transparent, shader load failure logged instead of crashing +- Fog desync during camera smooth pan — fog rect now tracks camera position instead of player position + +### Changed +- Server game loop throttled to ~20 ticks/sec (50ms frames) — non-blocking TCP loop no longer spins; remaining frame budget available for NPC AI +- Hold-to-move input model — movement polled each frame with stance-based throttle (Sprint=200ms, Walk=400ms, Careful=600ms, Crouch=800ms) and composite diagonals (W+D → northeast) +- D-053 updated with client throttle rates and input model documentation + +### Added +- Dialogue box UI skeleton (#434, D-061) — bottom screen, max 20% height, ~65% width, NPC speech + max 3 response options, insert-styled colors, WASD walk-away with 300ms fade, no close button, diegetic on InsertOverlay z-layer 6 +- Fog entity visualization (#431, D-059/D-060) — cognitive delay rendering: sonar-style sound pings (3 concentric rings, 1.5s fade), unrecognized grey blobs with 0.8s breathing pulse, D-033 color transition at 50% recognition progress, ±0.5 tile position drift, FogEntities node at z:950 + +### Changed +- Client protocol version bumped from 6 to 7 (pending_recognitions decode for cognitive delay) +- GameState: current_dialogue and pending_recognitions fields wired from ObserverSnapshot v7 +- Scene tree: DialogueBox added to InsertOverlay, FogEntities at z:950 between fog shader and InsertOverlay +- Test mode: mock dialogue (Kael NPC, 3 options) and mock cognitive delay entity (6-tick recognition cycle) + +### Added +- Archetype evidence presentation spec (#443, D-065/D-034/D-033) — detective case file vs smuggler notebook design document: item definitions, knowledge graph presentation, contradiction markers, THE FRIEND arc walkthroughs, systems interaction map, authoring guidelines +- Cognitive delay system (#423, D-060) — CognitiveDelay component buffers perception events before emitting KnowledgeEvents (0.6s base / 0.3s urgent at 10 tps), pending_recognitions in ObserverSnapshot v7 for client fog entity visualization, cancellation on entity LOS exit +- ListeningFocus eavesdrop system (#426, D-053) — stationary_ticks tracking for eavesdrop positioning bonus, 30-tick threshold (20 for Careful stance), Sprint blocks accumulation, registered after validate_movement +- YAML content loader with hot-reload (#326, D-028) — LinePool system parsing dialogue/monologue YAML into BTreeMap-indexed pools, 4-layer query filtering (access > situation > trust > topic+mood), timestamp-polling hot-reload (dev-only), graceful failure preserves previous content +- Line pool format specification (#308) — formal spec at docs/architecture/line-pool-format.md defining YAML structure, tag enums, 4-layer filtering pipeline, prerequisite-to-KG mapping, ID format, and Rust loader interface +- InteractionMemory KG schema design (#442, D-064) — design doc at docs/architecture/interaction-memory-schema.md extending FactKnowledge with interaction tracking, 5-state InteractionState enum, monologue prerequisite extension, NpcTolerance reconciliation +- Audio discussion decisions D-067 through D-074: recognition chime timing, 5-bus architecture, audio dip profiles, confrontation as cognitive vulnerability, monologue chime placeholder strategy, universal conversation murmur, zone crossfade, hybrid audio generation +- Asset pipeline documentation system (docs/assets/) with category-based index, sonic palette, and generation templates for audio, visual, and video pipelines +- Stable Audio Open connector and post-processing wrappers (audio-generate, audio-health, audio-post) with timeout handling for 11GB VRAM constraint +- gen-audio skill with prompt assembly system (sonic palette prefixes + category templates + asset descriptions) +- 6 interaction UI audio assets (#440): cursor_hover, weapon_aim, implant_open, fog_recognition, sfx_monologue_chime, sfx_monologue_chime_urgent — generated via SAO, needs duration trimming (#453) +- Dialogue/confrontation ambient dip implementation spec with full Godot AudioBus tween code +- Synthesis tooling (tooling/synth_ui_sounds.py) for programmatic insert-tech sound generation + +### Changed +- Protocol version bumped from 6 to 7 (pending_recognitions field in ObserverSnapshot) +- MessagePack fixtures regenerated for protocol v7 +- Monologue trigger system uses .values() iterator (clippy fix) +- Monologue schema: relationship prerequisite now requires target and state fields +- 389 tests total (70 new) — cognitive delay pipeline, line pool loader, ListeningFocus, content watching, serialization +- Renamed asset-gen skill to gen-image for consistent gen-* naming pattern +- Client protocol v6 bridge — decode player_stance (4 variants) and player_inventory from ObserverSnapshot, TOGGLE_STANCE_UP/DOWN input actions, 25 gdUnit4 tests +- Three-scope z-layer rendering pipeline (D-049) — world z:0-900 inside CanvasGroup, insert overlay CanvasLayer 10, UI CanvasLayer 20, modal CanvasLayer 30. Y-sort contract enforced, reserved VFX/airborne/lower-floor ranges documented +- Cursor state machine (#429, D-056) — 4 states (Default/EntityHover/ObjectHover/WeaponAim), 150ms transitions, insert-styled geometric shapes +- Fog shader rebuild (#430, D-059) — 5-layer fragment shader with animated Perlin noise, CanvasGroup compositing, FogState autoload for visibility/exploration textures +- Entity interaction list (#432, D-057) — vertical multi-verb menu, insert-styled, sprint suppression, diegetic toggle +- World radial menu (#433, D-058) — 2 spokes (Observe + Insert), drag-release and click-click input, 60-degree acceptance zones +- Inventory UI (#438, D-065) — 3x3 grid, 40x40px slots, 1-9 hotkey selection +- Stance indicator (#439, D-053) — color-coded HUD text, C/X keybinds +- Architecture docs: z-layer gap analysis, fog shader spec, flying taxi feasibility analysis + +### Changed +- Scene tree restructured: Entities z_index 3->0 (critical y-sort fix), FloorObjects->10, YSortGroup->100, Overhead->300, FogOverlay->900, ModalLayer added +- constants.gd rewritten with three-scope z numbering and full reserved range documentation +- Fog renderer replaced: TileMapLayer-based fog_renderer.gd deleted, replaced by shader-based fog_shader.gd + fog.gdshader + +### Added +- ObserverSnapshot v6 wire protocol (#449) — player_stance (MovementStance) and player_inventory (Vec\) fields with serde defaults for backward compatibility +- Stance system (#417) — Sprint/Walk/Careful/Crouch movement stance with tick-based speed (1/2/3/4 ticks per move), monologue rate multipliers (40%/100%/150%/100%), PlayerMoveCooldown component, ToggleStanceUp/Down player actions +- TilePresence posture layers (#420) — Standing/Prone/Seated/Fixture occupancy layers enabling same-tile coexistence (e.g. seated NPC + standing player), layer-based collision in validate_movement +- ObjectType component (#421) — Readable/Container/Terminal/Door/Pickup/Furniture types with Phase 1 verb sets computed from type + proximity range +- Phase 2 verb filter (#422) — KG-gated observer-side verb processing: POI priority flips (D-060), Confront injection at KnowsDetails+ confidence, contradiction marking, archetype-specific label relabeling (Smuggler sees Move/Stash, Detective sees Scan/Flag on containers) +- CharacterArchetype component — Smuggler/Detective archetype for Phase 2 verb label differentiation (D-057) +- VerbKind::Confront — Phase 2 only verb injected when observer has KnowsDetails+ on an NPC at close range +- Smuggler inventory system (#424) — CarriedBy(StableId) component, Take/Place verbs, 9-slot (3x3 grid) capacity, auto-slot assignment, info boundary enforcement (carried items invisible to other observers) +- MovementProfile component (#418) — per-archetype default stance (smuggler=Walk, detective=Walk), applied on spawn, factory methods for future archetypes +- Sprint interaction buffer suppression (#419, D-055) — sprint stance explicitly clears interaction buffer, no verbs computed or sent during sprint, anomaly monologue pipeline unaffected +- Sprint anomaly double-take monologue (#428, D-055) — SprintAnomalyQueue component detects Contradicted entities during sprint, fires delayed retroactive monologue after ~1.5s ("Wait — something wasn't right back there"), first-in-wins queue semantics, 3 hardcoded v0.1 lines + +### Changed +- Protocol version bumped from 5 to 6 (stance, inventory, ObjectType, verb system fields) +- MessagePack fixtures regenerated for protocol v6 +- Input processing queries expanded for stance and cooldown components with backward-compatible Option wrapping +- Observer pipeline queries expanded for Stance and CharacterArchetype components +- NearbyInteraction carries object_type and contradicted fields for Phase 2 context +- BridgePlugin system ordering: process_sprint_anomaly_monologue runs after trigger_monologue, compute_observer_snapshot runs after anomaly processing +- Player spawn includes MovementProfile, Stance, PlayerMoveCooldown, and SprintAnomalyQueue components +- 331 tests total (131 new) — comprehensive QA coverage across stance, occupancy, Phase 2 verbs, sprint suppression, inventory, anomaly monologue, and wire format + +### Added +- D-066: Dual-scale grid — 0.5m simulation tiles for stealth granularity, 1m visual tiles for proportional art (2x retina factor). All world geometry 2x2 sim tile minimum so cover/LOS maps 1:1 with visuals. Amends OQ-01. +- Sprint CLI (`db/connectors/sprint`) — unified sprint lifecycle management with 5 subcommands: status, start, stop, start-work, prepare. Auto-detects sprint from DB state and team from git branch. Guards prevent activating unplanned sprints. +- Shared permission settings in `.claude/settings.json` — git, ticket/sprint CLI, make, tea, and core skills pre-approved across all worktrees. Deny rules block destructive operations. +- Decisions D-053 through D-065 from Control & Interaction Workshop — formalized interaction verb system, contextual actions, NPC awareness model, and related design decisions +- Sprint 6 Touch briefings for server, client, copy, and joint teams +- Control & Interaction Workshop outputs — full workshop notes and outcomes +- Smuggler inventory item specs for transit district (#441) +- Start-workshop skill for multi-agent design workshops + +### Fixed +- Added worktree boundary rules to CLAUDE.md — agents must stay within the git root, no navigating to sibling worktrees or above the repo +- Plan-sprint skill now enforces worktree-relative paths in generated briefings +- Worktree-update skill now discovers branches dynamically via `git worktree list` instead of relying on hardcoded branch names — fixes missed branches like `planning` + +### Changed +- Start-sprint and plan-sprint skills updated to use sprint CLI instead of manual multi-query workflows +- Permission syntax migrated from deprecated `:*` suffix to modern space-wildcard format across all worktrees +- Internal monologue trigger system (#414) — enter_location fires on first tick, time_idle fires after 100 ticks of no movement, 300-tick cooldown, dedup within session, random line selection from content pools via ChaCha20 RNG +- MonologueEvent in ObserverSnapshot v5 — current_monologue field carries id, text, and display duration across the IPC bridge +- Client monologue display wiring — protocol v5 decoding, GameState extraction, HUD display pass-through + +### Fixed +- NPC spawn missing Interactable component (#413) — NPCs spawned from content and proof room now have Interactable, enabling E-prompt detection +- PlayerAction::Interact was a no-op (#415) — changed from unit to struct variant with target_entity_id and verb fields, server logs interaction data + +### Changed +- Protocol version bumped from 4 to 5 (MonologueEvent field, Interact variant change) +- MessagePack fixtures regenerated for protocol v5 + +### Changed +- UIStrings YAML parser now handles arbitrary nesting depth and inline comments — adapts to copy team's restructured ui-strings.yaml with multi-level sections (relationship_states, health_values) +- HUD uses new YAML keys: `hud.perception_mode_prefix`, `hud.time_prefix`, `hud.health` (empty prefixes display values directly) +- Interaction prompt keybind hint ("E") hardcoded instead of loaded from YAML — keybinding is not copywriter text + +### Added +- Pre-commit FactId validation hook (#393) — grep-based check validates fact_id references in content YAML against canonical knowledge catalogs; advisory mode when catalogs are stubs, enforcing mode when populated +- Pre-commit hook infrastructure — `.config/hooks/` with modular dispatcher, `make setup-hooks` target, `core.hooksPath` config for worktree-safe hook installation +- `make check-fact-ids` target for manual fact_id validation +- UIStrings autoload with YAML-based UI string loading (#409) — minimal YAML parser, `get_text()` lookup with fallback-to-key +- HUD and interaction prompt labels now loaded from `client/data/ui-strings.yaml` instead of hardcoded strings +- Character voice speech patterns (#310) — sentence-level execution spec for smuggler and detective covering contractions, punctuation, stress markers, vocabulary, verbal tics, and authoring checklist +- NPC authoring style guide (#379) — 954-line handbook: tier budgets, 9 NPC patterns, dialogue/monologue rules, tag taxonomy, dual-lens coordination, Van Maanen's Star/Sova culture, FRIEND phase mapping, validation checklist +- UI microcopy (#409) — 72 YAML strings for client integration: interaction verbs, relationship states, HUD labels, perception modes, notifications, knowledge panel, tutorial prompts +- Kael Davan FRIEND pack (#297) — 86 hand-authored lines across 3 locations, 5-phase relationship arc with contradiction scene, dual-lens notes +- Sera Venn FRIEND pack (#298) — 75 hand-authored lines at The Last Shift, trust-gated gossip, avoidance contradiction, contaminated trust arc +- PC-as-NPC content (#401) — 70 authored items enabling second-playthrough recognition (D-039 wow moment #4) + +### Changed +- Moved ui-strings.yaml from content/campaigns/_meta/ to client/data/ for direct Godot client loading + +### Fixed +- Fact ID format collision across FRIEND content — normalized 73 flat IDs to dotted category.topic format, fixing broken detective evidence chain from Sera to Kael observations +- Entity ref format inconsistency — normalized underscore format to npc:hyphenated across ~15 monologue references +- Phase tag inconsistency between FRIEND packs — standardized to phase-N format, added missing phase-2 tags to Kael ring ops lines +- Detective monologue gap for Kael — added 7 observation lines (7 → 15 total), added mood tags and bar_evening situation to Kael bar content +- Hoshe QA briefing updated with integration test coverage priorities — test harnesses, dedicated client-server test map, edge case focus +- Sprint 5 "Live" team briefings — copy (6 tickets), client (2), CI (1), joint coordination for content-at-scale sprint targeting FRIEND packs, voice patterns, NPC style guide, PC-as-NPC authoring, UI microcopy, FactId validation +- Live server mode (`make game`) — single command builds server, launches client with TCP connection, auto-kills server on exit; `make stop` helper for manual cleanup +- `SR_LIVE=1` environment variable switches SimBridge from test mode to real TCP server connection +- TileKind in wire protocol (#412) — server sends Floor/Wall/Door/Object per visible tile, client renders walls and floors in live mode +- Input roundtrip integration test (#411) — spawns real server, connects via TCP, validates full movement and interact pipeline +- Debug logging for received player inputs on server (visible with `RUST_LOG=debug`) + +### Fixed +- Interaction prompt target+verb data now attached to Interact action in game loop (#405) — was TODO stub, server receives `{target_entity_id, verb}` payload +- Player entity detection uses `kind.variant == "Player"` instead of hardcoded `entity_id == 1` — fixes "player not found" warnings when connected to real server (which assigns different IDs) +- Movement keys now work in live mode — client was sending millisecond timestamps as input tick, server only processes ticks <= current frame counter; now uses server tick from latest snapshot +- Entity-to-tile alignment in live mode — server sends tile-center render coords (tile 16 → 16.5), entity renderer now floors to tile index before positioning + +### Added +- Content loader Phase 2 (#408) — 2-phase spawn pipeline loads real YAML content into ECS entities: enums, entity attributes, pools, templates, triangles, NPC profiles with Want/Tolerance/Contentment/Personality/Tells/Skills axes plus cross-reference resolution for Secrets, Relationships, Information, and DailyRoutine +- Global enum YAML files (#387) — 9 enum definitions (situations, topics, moods, triggers, access-tiers, trust-tiers, activities, patterns, motivations) from D-035 taxonomy +- Entity attributes YAML (#388) — 16 canonical knowledge graph attribute keys from D-024 with A7 workshop updates +- Seed-time pools (#389) — 5 single-candidate pools for deterministic v0.1 testing +- Social site templates (#390) — 3 templates (logistics-hub, bar, smuggling-ring) with role slot definitions per D-025 +- Triangle YAML files (#391) — 5 v0.1 triangles (3 active fork, 2 passive) per D-024 workshop synthesis +- Seed configuration schema design (#394) — design document defining game-start randomization: FRIEND selections, pool draws, template assignments, entanglement config, ChaCha20 RNG protocol +- YAML to RON converter tool (#403) — build-time converter in tooling/content-converter/, runs via `make content-ron` +- Line previewer CLI (#407) — 4 subcommands (dialogue, monologue, coverage, sequence) for content authors to test line selection without running the full game +- Happiness added to WantKind enum — Harek remapped from Safety to Happiness + +### Fixed +- Dialogue-pool schema corrected to use arrays for situation/topic/mood per D-035 (were incorrectly single strings) +- NPC want.primary changed from narrative strings to WantKind enum keywords — fixes silent Want component drop at spawn time +- Schema enum constraint added to npc-profile.schema.json for want.primary validation + +### Added +- Sprint 4 "Feel" team briefings — copy (8 tickets), server (9), client (1 carry-over), CI (1), joint coordination +- Interaction prompt system (#405) — server-driven "E - Talk" prompt decoding v4 nearby_interactions with nested VerbOption structs, fade animation, extensible get_interaction_target/get_selected_verb interface for future radial verb menu +- Art direction & mood board workshop (3 rounds + closing) — 4-agent team establishes visual identity, 16 art direction principles, 9 mood board images, 10 candidate decisions (D-042–D-051) +- 3D-to-2D sprite render pipeline (`client/tooling/sprite_renderer/`) — Godot @tool scene renders textured 3D models at "the angle" (-72.5deg ortho) from 4 cardinal directions at 1024/256/64 resolutions with outline applied at working resolution +- `/render-sprite` skill — CLI wrapper for the render pipeline with headless import step +- Pipeline POC: Era 1 institutional wall + bar green wall textures generated via Nano Banana and rendered through full pipeline +- PerceptionQuery trait and ActivePerceptionMode resource — abstraction layer for D-017 perception mode swapping (NaturalVision default implementation) +- VisibilityGeometry intermediate resource decoupling FOV computation from entity filtering +- Client-side PROTOCOL_VERSION enforcement — snapshot decoder rejects version mismatches with error log +- POI verb priority test in observer pipeline — asserts both verb kind and priority values end-to-end + +### Changed +- Observer pipeline decomposed into two-stage system: compute_visibility_geometry (geometry) → compute_observer_snapshot (entity filtering + assembly) +- POI verb priority adjustment moved from simulation phase (interaction.rs) to perception phase (observer) — fixes D-010 information boundary violation +- compute_nearby_interactions no longer reads KnowledgeGraph — determines verb availability by proximity only, verb priority adjusted by observer +- compute_nearby_interactions scheduling moved from SimulationPlugin to BridgePlugin for explicit ordering with geometry and observer systems +- Client test snapshot updated to v4 format (Protocol.PROTOCOL_VERSION, tick_rate replaces paused) + +### Added +- Content validation tooling — `make validate-content` validates campaign YAML files against JSON schemas, maps files by directory context +- Entity::to_bits() roundtrip test — guards against bevy version changes silently breaking wire IDs +- TickRate switch mid-accumulation test — verifies Half→Full→Paused→Half transitions preserve accumulator state +- Tick rate scaling system (#406, D-052) — Full/Half/Paused rates with fractional accumulation, SetTickRate player action, replaces binary pause flag +- Proximity detection and interaction verbs (#404, D-060) — compute_nearby_interactions system with Manhattan distance ranges (close ≤2, mid ≤5), context-sensitive verb computation (Talk, Observe, Examine), PersonOfInterest priority flip, NearbyInteraction in ObserverSnapshot v4 +- Content directory skeleton (#385, D-057) — district-as-atomic-pack layout with Sova Transit first district, 17 NPC stubs, 3 locations, 5 triangles, dialogue/monologue pools, factions, knowledge catalogs, enum definitions +- Content schema definitions (#386) — 8 JSON Schema files (draft 2020-12) for district, location, npc-profile, dialogue-pool, monologue-pool, triangle, routine, fact-catalog validation +- PROTOCOL_VERSION constant in bridge types — versioning strategy documented (subprocess IPC, serde defaults for field evolution) +- Campaign, system, station JSON schemas for hierarchical content validation + +### Fixed +- Test suite aligned with server v4 protocol enforcement — all hand-built snapshots include version field, verb priorities 1-indexed, ExamineNpc label corrected to "Observe" +- E2E proof tests resilient to entity ordering — player found by kind instead of array position, wall-hides test checks specific NPC position instead of total count, supports 3-NPC proof room layout +- Wire entity_id now uses StableId consistently across observer, observation, interpretation, and interaction systems (was Entity::to_bits() in some paths) +- Restored system/station/district hierarchical fields in district metadata (incorrectly removed during canonical_id cleanup) +- Unregistered entities in observer/interaction now log tracing::error instead of silently falling back to Entity::to_bits() + +### Changed +- NearbyInteractionBuffer refactored from global Resource to per-entity Component on PlayerCharacter — multiplayer-ready (D-009) +- Observer module split into mod.rs (244 lines) + tests.rs (480 lines) — reduces module complexity +- Content directory restructured from flat districts/ to hierarchical campaigns/main/systems/van-maanens-star/stations/sova/districts/transit/ — path mirrors canonical IDs, glob-based discovery, multi-campaign/DLC ready +- Content manifest (content.yaml) rewritten for glob-based district discovery +- District identity fields (system, station, district) now derived from directory path — removed from district.yaml required fields +- NPC canonical_id schema accepts district-scoped IDs (npc:transit.kael-davan) for cross-district uniqueness +- ObserverSnapshot protocol bumped to v4 — adds nearby_interactions field, tick_rate replaces paused field in GameTime +- NearbyInteractionBuffer.interactions is now private with take() accessor (no per-frame clone) +- NearbyInteraction.distance changed from f32 to u32 (matches manhattan distance) +- Missing PlayerCharacter in input processing now panics instead of silent no-op +- Verb sort uses (priority, kind) tuple for deterministic ordering at equal priority +- Unregistered entity in knowledge events triggers debug_assert + error (was warn) +- District schema: canonical_id is now optional (derived from directory path at load time) +- Skills trimmed for CLAUDE.md deduplication — search-docs (-48%), ticket (-17%), review-pr (-35%) now reference CLAUDE.md for basics instead of repeating them +- Review-pr reviewer profiles extracted to `references/reviewer-profiles.md` for progressive disclosure + +### Removed +- Dead generate_snapshot function in bridge/mod.rs — superseded by compute_observer_snapshot +- content/global/regions/ directory — region data absorbed into system.yaml metadata + +### Added +- Dual Lens Authoring Guide — 7-chapter reference for writing content that works for both smuggler and detective perspectives (D-027, D-028, D-032, D-034, D-035) +- THE MIRROR pattern spec — transparency-as-contrast NPC design with Naia Tamm reference implementation and generator template +- Smuggler voice card — register parameters, 5 voice anchors, 4 anti-patterns, paired comparison examples, display constraints, authoring checklist +- Smuggler moral arc spec — 4-phase trajectory (Comfort, Doubt, Reckoning, Compromise), FactId gates, monologue trigger rules, Kael intersection mapping +- PC-as-NPC unified spec — starting knowledge/relationship graphs, tell inversion, orientation monologue, 9-step conversion checklist, v0.1 smuggler + detective briefs +- Triangle 1 Hub Power Volume Escalation fork — 3-path smuggler decision (escalate/stabilize/mediate), ~41 authored dialogue lines, NPC state change tables +- Content directory structure design doc — runtime content/ layout, canonical ID format, 8 JSON Schema specs, 3-tier validation pipeline, migration path from wiki +- Interaction verb spec — 7 v0.1 verbs (Move, Look, Monologue, Examine Object, Examine NPC, Talk, Overhear), priority resolution, server pipeline architecture +- v0.1 wow moments checklist — maps all 6 D-039 moments to content deliverables, tickets, dependencies, completion status +- Nils Davan off-stage NPC stub — ring coordinator, GHOST + HANDLER pattern, lattice message design, relationship map +- NPC pattern/motivation mapping applied to all 18 NPC wiki pages with composition reads +- Structured NPC data model (#86) — replaced stub string/f32 fields with typed enums and integer types for D-010 determinism (WantKind, SecretSeverity, Skill, PersonalityTrait, CombatStyle, RelationshipKind) +- Global RelationshipGraph resource (#87) — BTreeMap with tuple key for efficient prefix queries and reverse lookups +- A* pathfinding system (#237) — PathRequest/ComputedPath/PathBlocked components with cardinal-neighbor A* and manhattan heuristic +- NPC path following system (#238) — MovementSpeed throttling, per-tick path advancement with MoveIntent creation +- Daily routine system (#88) — NpcPlugin with PreviousDayPhase resource and check_phase_transition system issuing PathRequests at day-phase boundaries +- Multiple NPC spawning (#84) — 3 distinct NPCs (dock worker, field tech, guard) with full component bundles and RelationshipGraph edges +- Observation event generator (#239) — RoutineDeviation, Absence, and NewEntity triggers from comparing visible snapshot against NPC routines and knowledge state +- Review-pr skill routes reviewers by branch type — server/client get Hoshe+Tyre, copy gets Hoshe+Paula+Miri, visual gets Hoshe+Araminta, audio gets Hoshe+Ozzie +- Start-sprint skill spawns team agents from sprint briefings — parses the Agents line, creates a team with tasks from tickets, and launches all listed agents as background teammates + +### Fixed +- IPC error handling (#341) — DeserializationWithDump/MutexPoisoned error variants, hex dump logging on deserialization failure, graceful error classification in bridge I/O systems +- NpcPlugin system ordering — routine phase transitions now run before pathfinding so PathRequests are picked up same frame +- Stale doc comment in observation event generator — system runs before knowledge updates, not after +- Clippy warnings from Rust 1.93 — derive Default, is_multiple_of, collapsible if + +### Changed +- Hael renamed to Naia Tamm across all wiki files (16 files updated) +- Canonical full names applied to 11 single-name NPCs (Voss→Arvo Voss, Devra→Devra Talsen, etc.) +- Location shortcodes standardized in monologue guide (hub_m_ → terminal_m_ per D-036) +- Entity attributes updated to 16 canonical keys — 4 new role-perspective keys (risk_assessment, loyalty_assessment, position_integrity, moral_weight), secret_held→leverage_held rename +- Drin Rosta expanded from Tier 3 to Tier 2 — full 10-axis profile, 6 voice lines, Triangle 2 + Triangle 5 roles +- NPC index roster table expanded with Pattern and Motivation columns +- Sprint 3 "Know" team briefings regenerated from database — all four files (server, client, joint, copy) now match actual sprint 3 ticket assignments +- Ticketing database moved to shared worktree location (`../settledreach.db`) — eliminates binary merge conflicts across branches +- All Python connectors use script-relative path resolution instead of `$REPO_ROOT` env var or git rev-parse +- `$REPO_ROOT` environment variable removed — all scripts, skills, and docs use relative paths +- Merged copy branch — 39 tickets, wiki review + content scoping workshops, game world glossary +- Merged maintenance branch — worktree-update skill + +### Added +- `make db-backup` / `make db-install` — database backup to `docs/backups/` (main-only) and restore for new clones +- Worktree-update skill backs up the shared database after merges on main + +### Removed +- `db/commonwealth.db` from git tracking (replaced by shared `../settledreach.db`) +- `$REPO_ROOT` env var from all 9 worktree `settings.local.json` files + +### Added +- Worktree-update skill — non-destructive branch sync with PR detection and conflict safety +- Game world wiki — 45-file glossary covering Sova Transit District: 17 NPCs, 5 triangles, 3 social sites, 7 factions, knowledge vocabulary +- Wiki Review workshop (4 rounds + lead interview) — 300-world generator model, cultural ingredients menu, three-system NPC architecture, Sacred/Profane/Middle Kingdom framework +- v0.1 Content Scoping workshop (2 rounds + closing) — 16 EntityKnowledge keys, mechanical NPC mapping, YAML content format, 7-verb interaction model, server-authoritative pause, 20 decisions (D-042–D-061) +- 39 implementation tickets (#371–#409) from content scoping workshop — copy 21, server 13, client 2, ci 1 +- Control & Interaction workshop brief (queued) +- Large content push team pattern in CLAUDE.md +- Inigo sound designer agent — soundscape design, ambient layers, diegetic cues, D-018 audio propagation +- Team-agent mapping in sprint planning — each team has defined default agents for briefing assignment +- Sprint skills recognize all team branches (server, client, copy, audio, visual, ci) +- Sprint 3 "Know" team briefings — server (6 tickets), client (2 tickets), joint (2 split tickets), copy (1 carry-over) +- Copy team sprint briefing for Sprint 2 (#368 knowledge vocabulary) +- Sprint 2 proof: fog of perception E2E tests (#357) — 3 tests verifying all 7 acceptance criteria through real server pipeline (movement, tiles, fog, wall hiding, corner reveal) +- Server proof room — wall at (16,14) between player at (16,16) and NPC at (16,13) for LOS testing +- Dynamic test snapshot in SimBridge — tracks player position from queued inputs, Bresenham LOS, Manhattan-distance visibility for standalone demo mode +- 4 Bresenham LOS unit tests — clear path, wall blocked, diagonal, same position (PR #13 review) + +### Fixed +- Type safety in GameState visible_tiles loop — validates Dictionary with x/y keys before access (PR #11 review) +- Consistent reset_test_state() usage across all test files (PR #13 review) +- E2E connection loop now detects server process death early (PR #13 review) +- Corner reveal test verifies NPC position at (16.5, 13.5) (PR #13 review) +- Entity renderer skips redundant modulate.a writes when alpha unchanged (PR #11 review) + +### Added +- Observer snapshot knowledge integration (#366) — VisibleEntity carries relationship state (D-033 color) and observation type (Visible/Remembered), remembered entities appear as fog ghosts at last known position +- Knowledge graph system (#361, #362, #363, #365) — per-entity KnowledgeGraph component (D-041), StableEntityId + EntityRegistry, KnowledgeEventQueue, decay system, 4-level confidence hierarchy +- Direct observation knowledge flow (#364) — perception emits DirectObservation/LeftLOS events to knowledge graph, entities entering/leaving LOS tracked +- Protocol v2 decoder — extracts game_time, player_facing, visible_tiles, and per-entity visibility sectors from ObserverSnapshot v2 +- D-033 entity color palette (#130) — relationship-based colors (teal/green/amber/red), Phase 1 defaults by entity kind +- Peripheral vision dimming — entities in peripheral vision rendered at 50% alpha (D-015) +- Player facing direction indicator — Polygon2D triangle on player entity showing 8-directional facing +- GameState v2 fields — game_time, player_facing, visibility_sectors stored from snapshot data +- Test snapshot updated to v2 format with visibility sectors, game_time, and player_facing +- Observer visibility query (#112) — replaces unfiltered generate_snapshot with LOS-filtered compute_observer_snapshot combining shadowcasting + vision cone +- Vision cone system (#111) — forward/peripheral/blind sectors per D-015, Facing component updated on movement +- Symmetric shadowcasting (#110, #359) — Albert Ford algorithm with rational fraction slopes, benchmarked 1.2-10.5x faster than recursive, symmetry guaranteed (D-035) +- ObserverSnapshot v2 schema (#358, #25) — version field, GameTime, FacingDirection, VisibleTile, VisibilitySector types, visibility tag on entities +- D-035 decision record — symmetric shadowcasting selected over recursive (resolves Q-018) +- Tile rendering engine (#129) — programmatic TileSet with floor/wall/door/object placeholders, renders from snapshot tile data +- Fog overlay rendering (#131) — three visibility states (visible/fog-edge/hidden) via TileMapLayer overlay +- Camera lock to character (#116) — Camera2D smoothing at 2x zoom, locked to player position (D-015) +- Test room environment — 8x8 room with corridor and Manhattan-distance visibility for development without server +- `ticket team` command and `--team` filter — comma-separated team assignment for tickets (server, client, joint, content) + +### Changed +- All instruction files (CLAUDE.md, skills, agent files) now use `$REPO_ROOT` env var instead of `git rev-parse --show-toplevel` — pre-set per worktree via `.claude/settings.local.json` +- `start-sprint` skill now requires plan mode — agent must create and get approval for a concrete sprint plan before starting implementation + +### Fixed +- Entity renderer protocol field mismatches — "id"→"entity_id", "type"→"kind.variant", "position"→x/y fields now match Protocol.decode_entity() output +- Entity centering — entities (24x24) now centered within 32px tiles instead of top-left aligned +- `start-sprint` skill uses `git rev-parse --show-toplevel` for worktree-safe absolute paths — fixes "No such file or directory" errors on team branches + +### Changed +- Background clear color set to near-black for unexplored areas (was default Godot gray) +- Scene render order: Tiles → FogOverlay → Entities (fog covers tiles, entities render on top) +- FogOverlay node type changed from Node2D to TileMapLayer for tile-based fog rendering +- GameState now stores visible_tiles and visible_positions from snapshots +- Test snapshot includes player entity (kind "Player"), second NPC entity, tile data, and visibility data + +### Added +- Sprint 2 "See" briefings (server, client, joint) — fog of perception through the bridge +- `/plan-sprint` skill — automates sprint planning workflow and briefing file generation +- `ticket show` multi-ID support and `--brief` flag for compact human-readable output +- Q-018 through Q-023 — 6 open questions from architecture audit (shadowcasting, entity ID stability, collision resolution, tick overflow, pathfinding cache, debug visualization) +- 5 architecture spike workshop briefs (knowledge graph, observer pipeline, NPC AI state machines, save/load, map authoring) +- 17 tickets from architecture audit (#339-#355) — 7 Sprint 1 tasks, 5 Sprint 2+ tasks, 5 workshop epics +- End-to-end connection test (#81) — GDScript test spawning Rust server, connecting via LocalBridge, sending MoveNorth input, verifying player movement in snapshot response (D-030 Layer 3) +- Batch input encoding (Vec\ wire format) — Protocol.encode_player_inputs() batches all inputs per tick into one framed message matching server expectations +- EntityKind::Player fixture — snapshot_player.msgpack for cross-language testing, multi-entity fixture updated to include all 4 entity kinds +- 7 new tests (4 batch encoding, 1 framed batch roundtrip, 1 Player fixture decode, 1 E2E connection), 43 total client tests passing +- LocalBridge GDScript TCP transport (#79) — 4-byte big-endian length-prefix framing matching Rust server, StreamPeerTCP wrapper with partial read handling +- ServerProcess subprocess manager — spawns/stops Rust server via OS.create_process(), auto-cleanup on destruction +- SimBridge live transport integration — _process() polling loop for TCP receive/send, connection state machine (DISCONNECTED → CONNECTING → CONNECTED → ERROR) +- 8-directional input support — 4 diagonal movement variants (NE, SE, SW, NW) in InputMapper, SimBridge wire mapping, and project.godot input actions +- 12 LocalBridge tests (framing roundtrips, cross-layer Protocol+framing, diagonal wire mapping) +- 4 diagonal movement cross-language fixtures (Rust → GDScript, D-030 Layer 1) +- 33 total client tests passing (up from 20) +- TcpBridge transport for Godot client connection — TCP localhost IPC alongside existing Unix socket LocalBridge +- Input processing system (process_player_input) — drains InputQueue, converts PlayerActions to MoveIntent components, handles pause/unpause +- Snapshot generation system (generate_snapshot) — builds ObserverSnapshot from ECS state with render coordinate conversion +- Bridge I/O systems (receive_bridge_inputs, send_bridge_snapshot) — wire bridge to ECS pipeline with graceful disconnect detection +- Full game loop in main.rs — TCP accept, tick loop with ServerRunning resource, CLI/env addr config +- PlayerCharacter marker component, Player EntityKind variant, SnapshotBuffer resource +- E2E game_loop integration test verifying player movement through full pipeline +- 8 new tests (3 TCP bridge + 4 input processing + 1 E2E game loop), total 53 +- Architecture audit framework (docs/audits/) — adversarial two-round review pattern by Tyre + Troblum +- Sprint 1 architecture review — full decision + code audit, GREEN architecture, AMBER implementation plan +- v0.1 content gap analysis workshop — 6 agents, 2 rounds, 9 content layers, 8 new decisions (D-032 through D-039) +- D-032: Separate monologue pools per playable character (hard partition, not filter) +- D-033: Entity color represents relationship to player character (asymmetric per character) +- D-034: THE FRIEND NPC pattern — production-level emotional centerpiece per character (Kael Davan, Sera Venn) +- D-035: Converged tag taxonomy for dialogue/monologue line pools (6 structural + 3 selection tags) +- D-036: Sova Transit District / Van Maanen's Star as v0.1 setting (first named star system) +- D-037: Contraband specification — unlicensed lattice components (moral ambiguity by design) +- D-038: Audio in v0.1 scope — 8 AI-generated files via Stable Audio Open +- D-039: All 6 wow moments promoted to v0.1 must-have scope +- LocalBridge IPC over Unix domain sockets (#78) — length-prefixed MessagePack framing, SimBridge implementation, BridgeResource ECS wrapper +- Tile collision system (#236) — TilePosition component, chunk-based WalkabilityMap (D-012), MoveIntent + validate_movement system +- TilePosition ↔ f32 render coordinate conversion (to_render_coords, from_render_coords) bridging simulation and wire format +- Chunk load/unload support in WalkabilityMap — HashMap with 32x32 tile chunks +- 8-directional movement — diagonal PlayerAction variants (MoveNortheast/Northwest/Southeast/Southwest) + TilePosition::all_neighbors() +- Entity-entity collision in validate_movement — spatial occupancy check prevents multiple entities on same tile +- 25 new tests (4 framing + 2 IPC integration + 17 movement unit + 2 movement integration), total 45 +- MessagePack serialization for GDScript (ticket #77) — Protocol codec decoding ObserverSnapshot/PlayerInput from Rust wire format, encoding PlayerInput for server +- Godot4MessagePack library (pure GDScript) for MessagePack encode/decode +- Rust fixture generator (gen_fixtures.rs) producing canonical .msgpack test fixtures with rmp_serde +- 8 cross-language protocol tests verifying Rust↔GDScript MessagePack compatibility (D-030 Layer 1) +- SimBridge wired to Protocol codec with receive_bytes()/drain_outbound() for transport layer +- `db/connectors/ticket` CLI — ergonomic ticket management (list, show, create, assign, sprint, deps, search, epics, children, count) with JSON output +- Sprint 1 "Run" created with 8 stories targeting moving character on screen via IPC bridge +- Ticket skill rewritten to use ticket CLI instead of raw SQL wrapper scripts +- Godot 4 client boilerplate (epic #277) — scenes, autoloads (SimBridge, GameState, InputMapper), rendering stubs, UI shell (HUD, minimap, monologue display), input system with semantic actions +- gdUnit4 test framework with 7 tests (2 smoke + 5 D-030 Layer 1 fixture tests for snapshot parsing) +- make ci-client pipeline (lint, build, test via gdUnit4 headless runner) +- Camera tracking locked to player position (D-015), monologue display wired to snapshot data (D-016) +- Gitea tea CLI instructions in CLAUDE.md — non-interactive flags, PR workflow patterns +- `/review-pr` skill — dual-agent PR review with Hoshe (code quality) and Tyre (architecture) in parallel, Gitea integration, vendor file exclusion patterns, local merge workflow, heredoc workaround +- Automated Rust install via `make setup` (tooling/install-rust script, rustup + clippy + rustfmt) +- Automated Godot download/install via `make setup` (tooling/install-godot script, installs to ~/bin/godot4) +- InputQueue tick ordering enforcement via debug_assert (determinism guard) +- Serialization roundtrip tests for all PlayerAction and EntityKind variants (D-030 Layer 1) +- Edge case tests: day wraparound at midnight, day() calculation, out-of-order input rejection +- Test runner switched to cargo-nextest (D-030 requirement) +- Rust/bevy_ecs simulation server boilerplate (epic 276) — Cargo project, module structure, core ECS types, plugin scaffolding, deterministic simulation resources, test infrastructure +- SimulationTime resource with D-031 time system (10 ticks/game-minute, 4 day phases) +- SimRng deterministic RNG resource (ChaCha20, seeded for replay) +- InputQueue resource for timestamped semantic player actions +- ObserverSnapshot and PlayerInput IPC types with MessagePack serialization (D-020) +- SimBridge trait abstracting client-server transport +- CauseChain production component for information provenance tracking (D-030) +- SimulationTier types with LRU eviction support (D-026: Active/Background/StateSaved/Ungenerated) +- NPC 10-axis model components (D-024: 7 essential + 3 supporting + CombatCapability) +- Server test infrastructure: 11 inline unit tests + 4 integration tests (smoke + serialization round-trips) +- make ci-server pipeline verified green (clippy, fmt, build, test) +- Round 18 v0.1 gap analysis workshop — 7 agents, 2 rounds, 4 tracks (concept proof, wow factor, missing systems, testability) +- D-030: Testability architecture — 8 sub-decisions for ticket #214 (gdUnit4, hybrid Rust testing, CauseChain component, three-layer IPC testing) +- D-031: Time system — 10 ticks = 1 game-minute, 4 day phases (Morning/Afternoon/Evening/Night), diegetic clock display +- 41 new tickets from gap analysis: 3 epics (Movement & Collision, Observation & Interaction, Game State Management) + 38 stories +- 15 priority promotions including 3 tickets to critical (deterministic replay, divergent knowledge, divergent relationships) +- 21 new dependency records mapping critical path through collision → pathfinding → NPC movement → routine execution +- Workshop directory convention established with per-workshop subdirectories +- Project directory scaffold: client/, server/, tooling/, tests/, .config/, .cache/ +- Top-level Makefile with dev workflow targets (setup, build, run, test, lint, ci, clean) +- Whitelistable sqlite-init and sqlite-seed wrapper scripts completing the db/connectors/sqlite-* set +- Round 17 content architecture workshop — Full team (8 agents, 3 rounds) defining content pipeline +- D-023: Three-tier content model (authored drama modules, templated content, procedural filler) with life-sim substrate +- D-024: NPC generation model — 10 axes (7 essential + 3 supporting) with CombatCapability ECS component +- D-025: Social site / functional cluster as atomic Tier 2 template unit (4-8 NPCs, 15-40 tiles) +- D-026: Simulation tiers with timestamp-based LRU eviction (Active/Background/State-saved/Ungenerated) +- D-027: Vertical slice — smuggler + detective two-character proof-of-concept (supersedes D-006) +- D-028: Dialogue architecture — tagged line pools with four relational layers (access tiers, history, trust-gating, unprompted disclosure) +- D-029: Population entanglement ratio — 30% flat / 50% mundane triangles / 20% intrigue-entangled +- Content architecture workshop brief documenting the "life first, drama second" design philosophy +- Mellanie (Copywriter) activated from standby for content authoring phase +- Round 16 faction development — Full faction framework with lore, political analysis, mechanical grounding (session closed, awaiting team input for v0.1 selection) +- 3D reputation system: Trust × Usefulness × Exposure per faction +- Faction mechanical grounding: starting loadouts, information asymmetry, blind spots, resource loops +- Power dynamics analysis: formal vs. real power distribution across factions +- Character drama templates: whistleblower, inspector with conscience, dual-loyalty operative, reluctant conspirator +- Betrayal vectors and conspiracy potential for all six factions +- "First 30 Minutes" test demonstrating six mechanically distinct faction perspectives +- D-021: Official project title "The Settled Reach" confirmed, domain settledreach.com secured +- Round 14 worldbuilding — "The Settled Reach" original SF setting foundation with full team reactions +- Original terminology established: Settled Reach, Founder Gates, Span Gates, Interstitium, neural lattice, Meridian, imprint, re-embodiment, Perpetuals, the Unbound, Forking, Severance +- Four enhancement tiers: Baseline, Augmented, Transcendent, Elevated (plus the Threshold as endgame horizon) +- Two-tier death system: soft death (lattice intact) and hard death (lattice destroyed, imprint restore) +- Six factions: Concord Assembly, Syndics, Separatists, Guardians of Autonomy/Severance, Veil Institute, Lattice Commission +- Infrastructure-as-mystery: Builders inhabiting the Interstitium, Gyre events as leakage +- Miri's IP originality guardian role — flags concepts too close to source franchises +- Whitelistable wrapper scripts for SQLite and Qdrant connectors (sqlite-query, sqlite-exec, qdrant-search, qdrant-index, qdrant-health, qdrant-count) +- Architecture evaluation and risk assessment documents for Godot+Rust bridge approach +- Round 13 engine selection discussion — full team debate on engine paradigms +- D-020: Engine and architecture selection — Godot 4 client + Rust/bevy_ecs simulation server via subprocess/IPC +- Rejected alternatives R-004 through R-010 documented (pure Bevy, pure Godot, GDExtension, C++ GDExtension, Fyrox, custom framework, protobuf) + +### Fixed +- create-skill references to nonexistent init_skill.py and package_skill.py scripts +- SimBridge wire format: inputs now batch-encoded as Vec\ array per server protocol (was sending individual inputs per frame) +- SimBridge server args: positional address format ("127.0.0.1:9876") matching server CLI, port default corrected to 9876 +- Wire format mismatch: LocalBridge now uses rmp_serde::to_vec_named() (named maps) matching client Protocol.gd expectations +- EOF on bridge read now returns BridgeError::Transport for disconnect detection instead of empty Vec +- WalkabilityMap rewritten from flat Vec to chunk-based HashMap per D-012 architecture (Tyre review) +- LocalBridge mutex .unwrap() → .expect() for clearer panic messages (Hoshe review) +- Documented 16MB MAX_MESSAGE_SIZE rationale in framing.rs (Hoshe review) +- Client input_mapper double-check bug (redundant event.pressed + is_action_pressed) +- Bounds validation on snapshot position arrays in game_state.gd and entity_renderer.gd +- Deterministic test snapshots (replaced Time.get_ticks_msec() with incrementing counter) +- Tween overlap in monologue_display.gd (cancel active tween before creating new one) +- SubViewportContainer missing SubViewport child in minimap.tscn +- Cargo.toml edition 2024 → 2021 for broader toolchain compatibility +- Relationship.target_name: String → target_id: u64 for entity scalability (Tyre review) +- DayPhase enum now derives Serialize/Deserialize (consistent with other enums) +- Replaced all absolute paths (macOS and Linux) with project-relative paths across round-16 docs +- Removed duplicate ROUND-16-STATUS.md from project root (content already in round-16-session-notes.md) +- Added relative-path convention to Qatux agent persona for cross-system consistency + +### Removed +- dotfiles/tmux.conf (no longer needed) + +### Changed +- Q-018 (shadowcasting algorithm selection) resolved via D-035 +- All 18 agent briefings updated for decisions/ directory split and DEVOPS.md references +- Implementation agents (Dudley, Hoshe, Justine, Oscar, Si, Stig, Tyre) now include Development Workflow sections with Makefile targets +- Q-009 (time system) resolved via D-031 +- Ticket catalog expanded from 232 to 273 tickets with sprint sequencing (Run → Feel → Content → Validate) +- Ticket skill updated to use wrapper scripts exclusively (no more direct python3 calls) +- Miri's role updated from Canon Guardian to Worldbuilder & Setting Designer (original IP pivot) +- Project description updated to reflect D-020 engine decision +- Connector usage instructions now reference wrapper scripts instead of python3 directly +- Q-001 (engine selection) resolved via D-020 From 1640c880daac8d4512b4ccdf50abb8e2f6ccf3c0 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 5 Apr 2026 09:39:52 +0200 Subject: [PATCH 3/5] fix(simulation): suppress clippy too_many_arguments on cmd_add_body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refactor from &Commands to individual parameters exposed the 13-argument signature to clippy. Allow attribute is appropriate here — the parameters map 1:1 to DB columns. Co-Authored-By: Claude Opus 4.6 --- server/src/bin/atlas/mutate.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/bin/atlas/mutate.rs b/server/src/bin/atlas/mutate.rs index 080dec933..58ae4b4f7 100644 --- a/server/src/bin/atlas/mutate.rs +++ b/server/src/bin/atlas/mutate.rs @@ -3,6 +3,7 @@ use std::process; use rusqlite::{params, Connection}; use serde::Serialize; +#[allow(clippy::too_many_arguments)] pub fn cmd_add_body( conn: &Connection, id: &str, From fe788a0ae3ed3dbf844c4bc6a26cbc93a644dbe2 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 5 Apr 2026 09:40:28 +0200 Subject: [PATCH 4/5] fix(simulation): rustfmt long argument lists in atlas dispatch Co-Authored-By: Claude Opus 4.6 --- server/src/bin/atlas/main.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/server/src/bin/atlas/main.rs b/server/src/bin/atlas/main.rs index fd35bf9fc..a57d9f331 100644 --- a/server/src/bin/atlas/main.rs +++ b/server/src/bin/atlas/main.rs @@ -236,8 +236,19 @@ fn main() { inhabited, population, } => mutate::cmd_add_body( - &conn, id, system, r#type, *orbit, name, parent, mass_class, atmosphere, *gravity, - biome, *inhabited, *population, + &conn, + id, + system, + r#type, + *orbit, + name, + parent, + mass_class, + atmosphere, + *gravity, + biome, + *inhabited, + *population, ), Commands::AddStation { id, @@ -249,7 +260,15 @@ fn main() { docking, gate, } => mutate::cmd_add_station( - &conn, id, system, orbits, r#type, name, *population, docking, *gate, + &conn, + id, + system, + orbits, + r#type, + name, + *population, + docking, + *gate, ), Commands::Stats => stats::cmd_stats(&conn), Commands::Author { From 65bcbd0aa7f679fca4232034ba1745bd342c34a3 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 5 Apr 2026 09:56:53 +0200 Subject: [PATCH 5/5] fix(meta): move changelog entry to root CHANGELOG.md Remove erroneous server/CHANGELOG.md and add the atlas split entry to the project root CHANGELOG.md where it belongs. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 3 + server/CHANGELOG.md | 1217 ------------------------------------------- 2 files changed, 3 insertions(+), 1217 deletions(-) delete mode 100644 server/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 08113b76b..572e1611a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] +### Changed +- Split monolithic `atlas.rs` (2119 lines) into 8 focused modules under `src/bin/atlas/` — no behavior change (#776) + ## [v0.1.30] — 2026-04-05 ### Added diff --git a/server/CHANGELOG.md b/server/CHANGELOG.md deleted file mode 100644 index 08113b76b..000000000 --- a/server/CHANGELOG.md +++ /dev/null @@ -1,1217 +0,0 @@ -# Changelog - -All notable changes to The Settled Reach project will be documented in this file. - -Format based on [Keep a Changelog](https://keepachangelog.com/). - -## [Unreleased] - -## [v0.1.30] — 2026-04-05 - -### Added -- `corridor-status` subcommand for atlas CLI — shows remaining unfinished systems grouped by geographic sector and hop distance (#744) -- Star map insert module — concentric hop-ring view of 301 systems, sector-colored, click-to-select with info panel, pan/zoom (#674) -- BoneAttachment3D overhead anchor above Head bone for future floating UI elements (#712) -- CharacterVisualDescriptor wired into startup IPC and snapshot restore for save/load persistence (#718) -- Display-only hair highlight swatch (auto-derived from primary tint) in character creation (#719) -- Asset manifest fully populated (11 body types, 14 hair, 4 heads, 4 eyebrows, 8 clothing) with regeneration script (#720) -- Sprint 30 acceptance test suite (27 tests across all 5 tickets) - -### Fixed -- `habitable_planet_count` filter now accepts both "breathable" and "standard" atmosphere values — previously all committed systems reported 0 habitable planets (#762). Systems committed before this fix may have stale `habitable_planet_count = 0`; re-commit to update. -- `corridor-status` uses LEFT JOIN so systems without gate records are included in counts -- `generate_body_matrix` now emits `atmosphere: "standard"` (was "breathable") to match committed-system conventions -- DirAccess asset scanning replaced with manifest JSON — fixes character creation in exported PCK builds (#720) -- Star map set_insert_active() no longer auto-shows the modal panel (#674) -- Star map insert state propagation wired into main.gd (#674) - -## [v0.1.29] — 2026-04-03 - -### Added -- Complete body catalogs for all 301 star systems — every system now has proper_name, bodies, and stations in systems.db -- 24 new system proposals authored across east_reach (6) and deep_frontier (18) corridors -- Bulk-synced 233 proposals into systems.db that were missing from prior sessions -- Named all remaining unsettled systems with GJ designations (301/301 complete) -- Silence topics and narrative hooks for Deep Frontier corridor systems -- Named 5 unnamed stations/bodies: Morwenna, Havelmark, Portela, Grindvik, Sturen Platforms - -### Fixed -- 7 duplicate body IDs in proposals (planet and gas giant sharing same letter suffix) -- star_type/spectral_class mismatches corrected across 36 atlas proposals -- GJ1002 copy-paste planets differentiated with unique orbital parameters -- GJ880 malformed spectral_class and Leirvik station population -- GJ3325 malformed spectral_class -- 40 proposals brought to minimum planet count and given asteroid belts -- atlas-verify now allows named uninhabited bodies (lore names) -- GJ-111 spectral_class corrected to F5/F6V (matching wiki) -- GJ-3943 spectral_class corrected to K5V+M3V (valid binary notation) -- GJ-903 wiki page completed (was empty stub) -- Deleted superseded tooling/atlas-helpers.sh - -## [v0.1.28] — 2026-03-23 - -### Added -- Character creation screen with live 3D preview, 5-tab panel (Body, Hair, Clothing, Accessories, Debug), manifest-driven content -- Face-based body segmentation pipeline — exclusive assignment per face, no overlap, 19 segments (head, neck, torso_upper, torso, hips, arms, hands, legs, feet, eyes, eyebrows) -- 6 body types: average/muscular/teen m/f from Quaternius Source tier .blends -- Solidified hair (20mm) and clothing (25mm) for depth-correct layering over body -- Skin tinting with per-body-type embedded textures and white recolor mask -- Eye color with iris-only mask derived from T_Eye_Split.png green channel -- Eyebrow tinting from hair color via body segment shader -- Quaternius peasant outfit set (tunic, pants, shoes) across all body types -- Asset manifest (manifest.json) controlling all available content — replaces filesystem scanning -- Pre-push lint hook: GDScript parse + gdlint + gdformat (advisory) + Rust clippy/fmt -- Screenshot test automation with JSON config, 4-cardinal captures, quit-after-screenshot -- Debug tab with per-segment visibility toggles, All ON/OFF buttons -- Scroll zoom (cursor-toward on zoom in, head-bone targeting) -- Dynamic UI: tabs hidden when no content, clothing slots hidden when empty, facial hair hidden for female/teen/child -- Gender-aware randomizer -- 26 Q-records filed (Q-063 through Q-088) covering plugins, architecture patterns, and future features -- 15 tickets filed (#722-736) for tooling, UI, distribution, and QA improvements -- Araminta agent updated with Poly Haven + Quaternius asset sourcing rules - -### Fixed -- **ALPHA output in toon shaders caused all depth sorting failures** — removing ALPHA from toon.gdshader and toon_masked.gdshader fixed hair/head clipping, clothing/body clipping, and all z-fighting issues simultaneously -- Character editor: color swatch stale closure, modal OK child traversal, D-165 HSL palette, fallback asset IDs, overhead camera pitch, hair highlight display-only -- Archetype selection screen removed (v0.2 pivot — no Smuggler/Detective) -- GDScript strict typing errors (Variant inference on Dictionary.get(), JSON.parse_string()) -- TabContainer children vanishing on scene instantiation — moved to programmatic creation -- Hair meshes exported with proper skinning (was export_skins=False) -- Hair mask PNGs were solid black (Blender image API bug) — regenerated as white -- Rust formatting issues caught by new pre-push hook -- D-148 overhead camera angle convention clarified (0° → -80°) - -### Changed -- Body segmentation: torso split into torso_upper (spine_03 + clavicle) and torso (spine_01 + spine_02), hips (pelvis) as independent segment -- Clothing no longer hides body segments — solidify handles visual coverage, segment hiding reserved for amputation/prosthetics -- Randomise → Randomize (US English convention) -- pr-push skill: mandatory runtime smoke test before pushing -- pr-review skill: parse check before spawning reviewers, process gap flagging - -## [v0.1.27] — 2026-03-17 - -### Added -- 29 zone-type behavior templates completing the full library of 31 zone types per D-142. 2,210 culture-neutral behavior primitives across rural, industrial, port, extraction, commercial, administrative, research, medical, military, security, entertainment, residential, detention, archaeological, wilderness, and diplomatic zones -- Generator-compatible overheard conversation RON format replacing the deprecated named-NPC YAML. 16 sample conversations parameterized by role pair and zone type with knowledge_payload for investigative value -- Cross-culture name pool collision checking in validate-ron (`--check-name-collisions` mode) - -- Drifter's Guide to the Reach — 100% coverage (301/301 systems). 262 new GTTR entries across all 5 regions plus updated regional index pages with hop-grouped tables -- GJ 6711 / Abzu workshop — 6 rounds, 24 documents: initial briefs (4 agents), cross-pollination syntheses, sealed envelope (observation is load-bearing), stress test challenges against existing canon, final syntheses with Abzu naming, mechanics design (watch investment counter, six player verbs, bleed tile states, Adams & Ford entry filing), carry-out proposals (unresolved — reward design TBD) -- Star map topology and real-coordinate SVG visualizations -- Compact of Westphalia faction page — mutual recognition treaty, ~30-40 west_reach systems, four core principles, rotating council, internal treaty-vs-government tension -- Batch 18 wiki pages — 18 systems reassigned from deep_frontier into named corridors (Waterkant, Breëvlei, Stilwater, Mossbank, Ribeirão, Nascente, Dernier Quai, Marktfeld, Bestevaer, Lichtung, Knotenpunkt, Posto Avançado, Weitblick, Último Farol, plus 4 unsettled) -- Batch 17 wiki pages — Xa Vời, Eisfeld, Confluent (60+ wine châteaux), Dunkelholz, Echternach (Luxembourgish), Bout du Chemin (French), Grenzstein, plus GJ 4056 (unsettled) -- Corporation doc for Vins de Grand Vide — négociant cooperative, three-tier classification (Grand Vide Classé / Vins de Corridors / Vin de Table du Vide), 6 named estates, 350-year commercial archive -- Batch 16 wiki pages — 13 south_reach systems at hops 7-8 including Espinho, Okahandja, Encrucijada, Quilombo, Velha Guarda, Fragua, Puerto Último, plus GJ 902 (unsettled, reserved for base building DLC per D-145) -- Batch 15 wiki page — Shimanami (east_reach hop 8) -- Batch 14 wiki pages — Wagtoring, Dunmore, Caledonia's End (north_reach hops 7-8) -- Batch 13 wiki pages — Stillvakt, Haltefenn, Vindkast, Kopparhytta, Steinfeld, Brückenau (west_reach hops 7-8) -- Corporation docs for Nordmark Skog (timber, Stillvakt) and Talbräu (lager, Brückenau) -- D-145: base building DLC — GJ 902 habitable moon as potential player settlement site -- South_reach wiki pages (batch 12) — Matamba, Inhambane, Isibaya, Kaapse Baai, Mwangaza, Dzimbahwe, Vuurkloof, Várzea, Nowa Huta, plus GJ 695A (unsettled) -- Corporation docs for Ferreira Monteiro (trade arbitration, Matamba) and Stalownia Kowalski (heavy equipment, Nowa Huta) -- East_reach wiki pages (batch 11) — Kaur's Observatory, Seongho, Tình Yên, Purnima, Marunong, Clearwater Station, Tam Giang, Jeonnam, Dagat, Suối Vàng -- Inner hub wiki pages (hops 3-4) — Crown's Hollow, Cairnside, Schuilhoek, Travessia, Nová Tržnice -- Corporation doc for Mercado Travessia (grocery chain, Travessia) -- Corporation docs for Adams & Ford Publishing, Calloway Distillery, thrds (updated index) -- 300-system CSV framework (systems-framework.md) — 66 columns covering colonization waves, cultural archetypes, tone framework, economic distribution, and Van Maanen's Star validation (D-095) -- Star map gate topology — 300-node network with 334 edges across 6 sectors, generated and hand-tuned for natural corridor structure in frontier space -- Star map generation pipeline — seed-based generator, sculpting script, core sector patcher, topology tuner with bridge-safe connectivity guarantees -- d2 sector diagrams — 6 sector maps + overview visualization of gate network -- Multi-table star systems DB schema with GJ catalog IDs as primary keys — 8 tables covering identity, gates, history, economy, factions, culture -- Full wiki prose for 10 core systems — Sirius, Ran, Tau Ceti, Sol, Arbour, Groombridge, Struve, Cygni B, ACB, Bastion -- Corporation wiki pages — Gate Corporation, Mastroianni Vehicle Group, Prometheus Labs -- Wiki category index pages for star-systems, corporations, factions, technology, contraband, concepts -- 10 next-tier systems named and assigned roles — Renaissance, Nova Roma, Prometheus, Proxima, Rigil Kentaurus, Barnard's Star, Lacaille, Cairn, Meridian, Aurelius -- `hop_distance_from_gateway` column in system_gates table - -### Changed -- Renamed GJ-7547 from Wag-'n-Bietjie to Skemeraand ("twilight evening") — better tonal fit for hop 22 position -- Rebuilt catalog.md and star-systems/index.md — now covers all 301 systems (227 named) organized by sector and hop distance -- 14 systems reassigned from deep_frontier into named corridors at hops 5-7 (5 north, 3 south, 3 east, 3 west) -- Renamed Carrefour to Confluent; expanded to 60+ named châteaux with 16 individually described estates -- D-095 aperture range amended from 4-8 to 1-8 — single-aperture dead-end systems valid for isolated frontier outposts -- D-095 amended: inter-system gates are alien-built (aperture count alien-determined), intra-system span gates are human-built (Institute reverse-engineering, Gate Corporation license) -- All system identifiers migrated from arbitrary S-numbers to GJ astronomical catalog IDs -- Q-039 (gate topology generation) resolved by 301-system star map - -### Removed -- Redundant `astronomical_id` column from star systems schema (system_id IS the GJ number) - -## [v0.1.26] — 2026-03-13 - -### Added -- ContentType::Factual — lines with numbers, denials, causal chains bypass LLM and serve base text directly (#650, D-138) -- Voice pipeline observer integration — enrichment systems rewrite dialogue/conversation text with voiced versions before snapshot assembly (#652) -- SQLite settings storage — per-player persistent settings via rusqlite (bundled), IPC protocol v20 with ChangeSettings/RequestAllSettings/DeleteSetting commands (#627) -- Composable behavior engine — three-layer action+modifier+context primitives replace flat culture×zone×role behavior strings (#633, D-139, Q-057 resolved) -- Stronger few-shot examples for Friendly and RoutineDeviation tells (#651) -- AI-Enhanced Dialogue toggle — settings panel toggle with layered hardware detection (RAM/TPT/degradation), battery auto-suspend with player override, warning label (#646, D-138) -- PlatformInfo autoload — client-side OS abstraction centralizing all platform queries: power state, memory, CPU, GPU, display, locale, file paths, diagnostics helper (#659, D-141) -- Vael and Osse culture profiles with voice personas, behavior modifiers, and explicit NEVER blocks (#653) -- Behavior modifiers for all three cultures — 7-category contract: work_pace, physical_manner, social_signal, task_completion, environmental_scan, offduty_posture, authority_response (#634) -- Zone-type template architecture — behavior primitives moved from per-location files to reusable zone-type templates (content/global/zone-types/). 31 zone types planned for v1.0 (#661, D-142) -- POI three-tier system — large POIs as zone types, abandoned flag for decay variants, poi_overlay for small landmarks (D-142) -- Authoring guides for base text elevation, culture creation, and content directory structure -- D-140: dialogue re-voicing quality constraints — Paula's six rules -- D-142: zone-type template architecture for scalable NPC behavior across 300+ systems - -### Removed -- v0.1 content loading system — server/src/content/ module (8200 lines), tooling/content-converter/, tooling/validate-content, content-ron/, content/_meta/ (#655, D-122) -- AiDialogueDetector — duplicate of HardwareDetector, replaced by PlatformInfo abstraction (#659) -- v0.1 hand-authored Van Maanen's Star dialogue, monologue, and NPC profiles — 64 files superseded by generated NPCs (#656, D-122) -- Detective mission system — investigation knowledge, lattice-commission faction, design docs, workshop archives (#657, D-117) - -### Fixed -- Name pool cross-contamination — zero overlaps across Van Maanen's Star, Vael, and Osse cultures -- D-141 → D-142 reference correction in zone-type templates -- Modifier coverage expanded to 2+ per category for all cultures; authority_response differentiated Van Maanen's Star/Osse -- Gendered pronouns removed from culture-neutral zone-type templates -- Ungrounded lore terms (Syndic, Meridian registration) replaced with generic descriptors in Osse culture -- Stale notes in content-structure-canonical.md and base-text-authoring-guide.md corrected -- Legacy annotation added to overheard.yaml (#664 tracks replacement) -- Dead dual_lens properties stripped from environmental YAML - -### Changed -- Protocol version bumped to 20 — ObserverSnapshot includes settings_response field (#627) -- All 18 agent briefings updated for v0.2 pivot — removed detective/smuggler/hand-authored references, aligned with generator-first approach (#658) -- 5 agent profiles (miri, ozzie, paula, inigo, hoshe) updated to remove stale v0.1 framing (#658) -- Q-015 closed as obsolete (D-122 eliminates hand-authored FRIEND content) -- D-018 perception model: franchise-specific example replaced with generic framing - -## [v0.1.25] — 2026-03-07 - -### Fixed -- Name pool first-pick bias — generator spike produced "Dav" as NPC 1 across all seeds; now uses derived RNG per zone+culture (#628) -- Behavior dedup — same behavior string no longer assigned to multiple NPCs in one zone run (#629) - -### Added -- Zone identity specs renamed to location-specific: van-maanens-star-rural-zone.ron and van-maanens-star-industrial-zone.ron — acknowledges these are culture×zone content, not reusable templates (#630, Q-057) -- ~108 new NPC behavior pool entries across all roles in both zone files — trader stage directions, foreman humanity behaviors, dock_worker/technician off-shift/break room behaviors (#630) -- Q-057 open question: composable behavior generation — decompose hand-authored pools into role actions + culture modifiers + context tags (#633, #634) -- Relationship-to-behavior pipeline — NPC behavior lines now reflect social connections (rivals ignore each other, friends gravitate, subordinates defer) (#631) -- Want/State layer — NPCs have internal motives (Bored, Alert, Suspicious, AvoidingSomeone, LookingForInfo) that leak through observable micro-tells (#632) -- LLM voice pipeline — Spike 1 (sr-voice CLI) and Spike 2 (full pipeline integration) complete. Gemma 2B Q4_K_M via stdin/stdout JSONL pipes, composition engine with double-prompt technique, 39 quality test cases (#638-644, D-138) - -## [v0.1.24] — 2026-03-06 - -### Added -- Character archetype select screen — two-card UI (Smuggler/Detective) between New Game and session start, keyboard+mouse selection, ESC cancels (#588, D-027) -- Triangle activation consumer — urgent monologue chime fires once per triangle per session when triangle_crisis_events received (#590, D-039) -- News ticker HUD — scrolling marquee visible in The Last Shift zone, hidden elsewhere, reads current_ticker from snapshot (#592, D-039) -- Triangle activation proximity monologue lines — 5 smuggler lines (Kael Davan) and 5 detective lines (Sera Venn/Torek Lintar) that fire when observing triangle anchor NPCs post-activation (#597, D-035, D-039) - -### Changed -- Protocol version bumped to 19 — StartupMessage includes character_archetype, snapshot includes triangle_crisis_events and current_ticker (#588, #590, #592) - -## [v0.1.23] — 2026-03-04 - -### Added -- TileKind enum (Floor/Wall/Void/Restricted) on WalkabilityMap with per-tile type data alongside walkability (#576, D-094) -- Location YAML tile format — hand-author tiles as string arrays (F/W/V/R characters), loaded into WalkabilityMap on production startup (#577) -- Chunk streaming system — ChunkLoadRadius and cadence-gated load/unload around player position, v0.1 covers full district (#578, D-012) -- EngagementRecord component — per-NPC observation time, conversation count, and monologue trigger count tracked by perception/dialogue/monologue systems (#570) -- MovementHistoryBuffer resource — 3000-tick ring buffer of player positions with co-presence proximity query (#571) -- Storyteller lifecycle rules — single activation per session, no concurrency, terminal resolution constants (#572) -- Storyteller activation_pass() — gate/proximity/engagement scoring/routing/module selection/TriangleActivatedEvent on 10-tick cadence (#579) -- Debug console server — 10 DebugCommandKind variants (AdvanceTicks, SkipToContamination, TeleportToPosition, InspectNpc, ListTriangles, etc.) with DebugResponsePayload on ObserverSnapshot (#580) -- Debug console client — tilde-toggle UI panel with command input, output log, settings toggle, and full DebugCommandKind dispatch via protocol v18 (#581) -- Entity-bound dialogue speaker colors — NPC colors assigned by entity ID (not screen position) with per-conversation lifecycle and round-robin palette (#573) -- Sova Transit District tile maps — 5 locations authored: The Terminal (44×28), The Last Shift (34×22), Maintenance Corridors (58×6), Gate Ground (40×34), Gate Gallery (32×10) (#582, #583) - -### Fixed -- LOS boundary walls — 1-tile wall margin beyond vision cone included in visible_tiles as BoundaryWall sector, walls at fog edge now render instead of bleeding into fog (#584) -- LOS boundary walls client — BoundaryWall tiles render through fog without marking explored, 4 new fog tests verify lifecycle (#585) -- Entity renderer test failures — updated 7 stale ColorRect/position assertions for Sprite2D migration, fixed SoundIndicatorRenderer class cache (#574) -- Dialogue speaker color contrast — re-enforce contrast floor after desaturation for passive (overheard) lines -- PROTOCOL_VERSION 17 → 18 mismatch — client rejected every server snapshot -- Debug console D-088 pause — sim now pauses while console is open, matching dialogue/settings overlay behavior -- Debug console settings toggle reads live state instead of ConfigFile, preventing checkbox divergence - -### Changed -- PROTOCOL_VERSION bumped 17 → 18 (debug_response field on ObserverSnapshot, DebugCommand PlayerAction variant) - -## [v0.1.22] — 2026-03-03 - -### Added -- Visual test harness — `make screenshot`, `make test-visual`, `make visual-update` for automated visual regression testing with golden PNGs across 11 scenarios (fog, HUD, dialogue, minimap) -- Visual movie mode — `make visual-movie` captures interaction flows as frame sequences with contact sheet generation -- World seed protocol — StartupMessage carries world_seed from client to server after handshake, enabling deterministic NPC population seeding (D-010, D-029) -- EntanglementConfig — per-seed NPC population ratios (flat/mundane/intrigue) sampled from seeded RNG with D-029 bounds, ensuring same seed = same world (#175, #178) -- Fog debug mode — toggle FogState.debug_exploration to render raw exploration texture as colored overlay for diagnostic use -- D-110 through D-112: z-level addressing, subterranean architecture, no instancing decisions -- Q-051: speech bubble indicator over speaking NPCs -- Sprint 22 "Wire" briefings (server, client, visual, CI, planning, joint) - -### Added (server) -- Production NPC pool generation — 23 authored Sova NPCs spawn with EntanglementTag (Flat/Intrigue) based on triangle membership (#176, D-029) -- Authored triangle instantiation — 5 Sova triangles (3 active forks, 2 passive tensions) loaded from content YAML with deterministic IDs (#188, D-087) -- Contamination activation mechanic — timer-based storyteller fires after 30 game-minutes, pressures active triangles, emits ContaminationEvent (#254) -- Modifications data model stub — Vec on chunk entities, round-trips through save/load for future construction DLC (#567, D-112) -- Zone Gate gauntlet room — two-zone test room with door boundary, zone crossing detection system (#512) -- Fuzzy map tests — 50-seed randomized testing of procedural maps against 4 structural invariants (#509) - -### Fixed -- Fog shader: silent compilation failure in OpenGL3 compat mode — removed `return` statements from fragment() which are not supported, causing fog overlay to render as no-op (root cause of Sprint 22 fog regression) -- Fog system: blocky stair-stepped edges at vision cone boundary — doubled Gaussian blur step size for D-066 compliant 6-8 tile smooth gradient (#569) -- Fog system: zero visibility in explored areas — switched bounds calculation from visible_tiles (empty in live server mode) to visible_positions, and removed shader guard that cut off gradient bleed into unexplored tiles (#569) -- Fog shader alpha tuned to D-059 spec: light fog 0.25-0.35 (was 0.25-0.55), deep fog 0.55-0.70 (was 0.78-0.90) — world content now visible through fog instead of hidden behind it (#563) - -### Changed -- Fog shader now distinguishes light fog (near cone, neutral dark) from deep fog (far from cone, zone temperature tint) with separate Perlin noise breathing cycles (8-10s / 15-20s) -- Zone temperature tint populated per-tile from server zone_id: bar=warm amber-dark, hub=cool blue-dark, corridor=neutral dark (D-059/D-046/D-077) -- Simplified vision cone from 3-sector (forward/peripheral/blind) to forward-only 120° arc — server sends only forward-cone tiles, client renders explored tiles behind the player with light fog overlay -- Simplified fog shader from 5-layer to 3-layer model (clear, explored, unexplored) -- Fog texture resize now preserves exploration data — tiles behind the player stay as light fog instead of reverting to unexplored black -- Updated D-015/D-017 perception decisions to reflect simplified cone model -- Moved connector scripts from db/connectors/ to tooling/db/ (#274) — backwards-compat symlink removed in #568 - -### Removed -- db/connectors symlink — all references now use tooling/db/ directly (#568) - -## [v0.1.20] — 2026-02-25 - -### Added -- Social site template schema — RoleSchema (#163), SpaceSpec (#164), TriangleDef (#106) with YAML deserialization, sample templates at server/data/templates/ -- Single-ownership model — TemplateOwnership component, TemplateReferenceMap resource, cross-template reference links preserved across save/load and tier eviction (#165, D-025) -- Triangle generation — intra-template constraint satisfaction assigns NPCs to triangle roles, minimum 2 triangles per template with fallback on imperfect seeds (#107) -- Triangle escalation system — tick_triangle_escalation runs per game-minute, tension increments toward ToleranceThreshold, TriangleCrisisEvent emitted on Active phase entry, ResolveTriangle stub command (#250, D-087) -- Protocol v16 — TriangleCrisisEventWire on ObserverSnapshot for future client rendering of triangle crises -- D-093: Sova Transit District spatial layout — 4 social sites (Terminal, Bar, Gate Cluster, Sector 3), 2 encounter nodes, zone palette, gate cluster 7-zone spec, z-level scheme (z=0 maintenance, z=1 main, z=2 observation gallery), 3 investigation paths, corridor widths -- D-094: Spatial hierarchy — chunk (64×64 sim) → block (128×128 sim) → district (4×4 blocks, 256×256 visual), supersedes D-014 estimate -- D-095: Horizon stations and transport lore — span gates (human-built, dual-use), horizon stations (alien-built, 4-8 apertures), "The Ring" per-system naming, sequential hop travel, The Loop internal tram -- Generator architecture workshop brief (ticket #562) — top-down pipeline for district generation, targeting Q-036 resolution -- SnapshotEventRouter — callable-based snapshot dispatch replaces inline if-has blocks in main.gd (#559) -- YamlParser shared utility — unified YAML parsing for UI strings and checklist conditions (#560) - -### Fixed -- Wire triangle crisis event queue into observer snapshot — clients now receive TriangleCrisisEventWire via protocol v16 (was always empty) -- Persist TriangleState in SaveStateV1 — triangle phase and tension survive save/load cycles -- Validate dangling with_role references in TriangleDef constraint validation -- Replace O(n²) fallback NPC assignment with BTreeSet; prevent same NPC assigned to two roles in one triangle -- Replace O(N*M) scan in apply_resolve_triangle with BTreeMap index for O(1) per-command lookup -- Add From impls for RoleId, TriangleId, StableId, TriangleCrisisEventWire — eliminate fragile .0 newtype access -- Consolidate near-identical unit tests with integration counterparts - -### Changed -- Sova station profile updated — horizon gates located at The Van Maanen Ring (800 AU), not on Station Sova; Admin Hub houses transit processing facility only -- game_state.gd: stationary_ticks and zone_id now read from server snapshot with deprecated client-side fallbacks (#557, D-020) -- dialogue_box.gd: decoupled from GameState and AudioManager via signals — zero direct autoload references (#558, D-020) -- main.gd: snapshot dispatch via SnapshotEventRouter, dialogue signal coordinator handlers (#559, #558) -- ui_strings.gd and checklist_evaluator.gd: delegate to YamlParser, ~140 lines of duplication removed (#560) - -## [v0.1.19] — 2026-02-25 - -### Added -- Sprint 20: Shape planned — 11 tickets (server 6, client 4, planning 1) covering template/triangle schemas, client refactors, and district layout design discussion -- Planning team ticket type in sprint-plan skill — supports design discussions with purpose-assembled agent panels, Qatux and SI for bookkeeping -- Client PR #70 merged — save/load client UI, F5/F6 quicksave/quickload (#554) -- Server PR #68 merged — Sprint 19 save/load, tier eviction, test infra (7 tickets, 2714 lines) -- Client PR #67 merged — Sprint 19 test infra, session management, debug overlay (5 tickets, 2547 lines) -- CI PR #69 merged — Sprint 19 test runners, IPC fixtures, protocol handshake, benchmark (4 tickets, 1297 lines) -- Test runner scripts — 7 bash scripts (run-rust, run-godot, run-ipc-fixtures, run-ipc-protocol, run-ipc-integration, run-ipc-benchmark, run-all) with structured JSON output (#270, D-030) -- IPC serialization fixtures — 5 msgpack fixtures with Rust generator, cross-language GDScript validation (22 assertions) (#271, D-030) -- Protocol handshake client — HANDSHAKING state in SimBridge, HandshakeMessage decode with 5s timeout (#556, D-020) -- IPC round-trip benchmark — p50/p95/p99 latency reporting, 5ms threshold (#342, D-020) -- Protocol version handshake — `HandshakeMessage` as first IPC frame before tick loop, forward-compatible input handling (#555, D-020) -- Protocol v15 — `save_result` field on ObserverSnapshot for client save/load confirmation -- State serialization primitives — `serialize_npc_to_frozen`/`deserialize_npc_from_frozen` with full D-024 10-axis coverage for tier eviction freeze/thaw (#96, D-026) -- Scope tag system — `ScopeTagKind` (Neighborhood, ActiveQuest, Colleague, KnownContact), `ScopePinned` marker, automatic assignment from KnowledgeGraph and RelationshipGraph (#98, D-026) -- Timestamp-based eviction — `LastInteractionTick` LRU tracking, `SimSpacePressure` resource, BinaryHeap eviction respecting scope-pinned entities, Active cap 80 (#97, D-026) -- Save/load ECS extraction — `save_to_file`/`load_from_file` via MessagePack, `SaveGame`/`LoadGame` IPC commands, `SaveLoadResultWire` on ObserverSnapshot (#553, D-085) -- ScopePinned eviction regression test — adversarial at-scale test proving pinned NPCs survive eviction even with oldest ticks -- Test infrastructure — Layer 3 integration test entry point, three-layer architecture per D-030 (#200) -- Information boundary negative tests — 4 tests proving no passive KG leakage, LOS fog holds, tier boundary holds, per-NPC save isolation (#272, D-010) -- gdUnit4 CI runner script — headless test execution via `run_gdunit4.gd` with exit code for CI (#205) -- Scene testing utilities — SceneHelper class with node existence, signal, and path helpers for gdUnit4 (#206) -- GameState apply_snapshot tests — 14 tests covering v2+ fields: game_time, facing, interactions, monologue, stance, inventory (#206) -- Game session management — per-game save directories under `user://saves/-/` per D-085, SessionManager autoload, main menu scene (#258) -- Debug visualization overlay — F3-toggled dev overlay with LOS rays, vision cone arcs, NPC path trails, knowledge confidence tags, tick timing sparkline (#348) -- SimBridge→TestHarness extraction — test simulation logic separated into dedicated RefCounted class with backward-compat proxy API -- Workshop outcomes files — formal closure for content-gap-analysis, KG-information-boundaries, v01-content-scoping, v01-gap-analysis, wiki-review -- D-087 through D-092 — recovered decisions from v01-content-scoping and wiki-review workshops (triangle config, pause system, content scope, voice registers, anchor lines, complicity theme) -- Q-030 through Q-039 — open questions from workshop backlog (seed schema, style guide, cultural ingredients, NPC architecture, PC archetypes, sacred/profane framework, district skeleton, generator pipeline, authored content estimate, gate topology) -- Decision ID claim system — `db/connectors/decision` CLI with `next`, `claim`, `check-dupes` commands to prevent cross-worktree D/Q/R ID collisions, pre-commit duplicate check -- D-085: Per-game save directory structure — every new game creates `user://saves//`, F5 quicksave, F6 quickload -- Q-029: Save file format design — long-term considerations for versioning, compression, integrity, metadata headers -- D-086: Renumbered insert icon system (was D-084 on visual branch) to resolve cross-worktree ID collision -- Save/load wireframe updated for D-085 — LOAD tab shows games grouped by directory with expand/collapse, QUICKSAVE slot, F5/F6 hints -- Sprint 19: Persist planned — 16 tickets (server 7, client 5, CI 4) covering save/load, tier eviction/scope, test infrastructure -- Character creation & game setup workshop brief — covers creation model, seed boundary, gate activation, quest seeding, game toggles (resolves Q-011) -- Protocol v14 — `poi_list`, `examine_result`, `player_knowledge` ObserverSnapshot wire types with live KG serialization (#151, #174, #264) -- Minimap rendering — circular 160px diegetic insert overlay with POI dots (colored by category), border arrows for distant POIs, player-centered fixed-north (#151) -- Dialogue UI hardening — confrontation italic voice (D-063), examine result overlay with 5s auto-dismiss and confidence coloring (#174) -- Knowledge/journal panel — right-side insert panel (J key), facts grouped by entity, contradicted entries in amber with strikethrough, stale entries dimmed, mutual exclusion with dialogue (#264) -- Sprint 18 client test suite — 50 gdUnit4 tests for dialogue (D-062, D-063, D-064) and journal (KG parsing, scene structure, UIStrings), plus test plan document -- D-084: dual-namespace line ID scheme for auto-generated NPCs — role pool (shared, unchanged) + instance override (opt-in, seeded counter). Resolves Q-028 (#544) -- Tier 1 drama module schema (`content/schemas/drama_module.schema.yaml`) — entry conditions, NPC requirements, event sequences, outcomes, pool format (#158) -- Smuggling ring v0.1 stub module (`content/modules/tier1/smuggling_ring_v0_1.yaml`) — vertical slice Tier 1 module with 6 NPC roles, dual event sequences, 5 outcomes (#158) -- Line ID authoring guide (`docs/design/line-id-authoring-guide.md`) — dual-namespace conventions for hand-authored and auto-generated NPC content -- Tier 1 module authoring guide (`docs/design/tier1-module-authoring.md`) — field reference, NPC pattern/motivation tables, design principles, pre-submission checklist -- Background tier state machines — schedule, mood, relationships, job tick once per game-minute for Background NPCs (#95, D-026) -- NPC vision system — symmetric shadowcasting for Active-tier NPCs, NpcMemory with last-known-position and zone inference (#115, D-011) -- NPC player-awareness behavior — PlayerAwareness component tracks LOS duration, suspicion accumulation, routine deviation triggers (#244) -- Skill system & combat flag — SkillSet component (BTreeMap), CombatCapability marker from combat_trained skill (#91, D-024) -- Player-action social propagation — three-order trust ripple (100%/40%/20%) through RelationshipGraph with cycle prevention (#249, D-029) -- Examine mechanic — process_examine_interaction with character-filtered observation text, KG DirectObservation write, examine_result in ObserverSnapshot (#242) -- Character goal/pressure framework — CharacterPressure component (exposure/institutional/relationship), wired to snapshot HUD data (#248) -- Save state data model — SaveStateV1 struct with MessagePack serialization, roundtrip tests for entity/KG/relationship/clock state (#256) -- Tell state derivation wired into ObserverSnapshot — integration tests for Nervous tell on Major secret + high stress (#337) -- Sprint 18: Touch planned — 14 tickets (server 9, client 3, copy 2) covering examine mechanic, NPC awareness, social propagation, minimap, dialogue UI, save state model -- `.claude/rules/` directory — modular auto-loaded instructions (tea-cli, git-safety, project-structure, team-patterns, local-services) -- KnowledgeGrant untagged enum with Fact and Entity variants, ContentEntityRegistry for NPC spawn-time entity resolution (D-079, #545) -- KnowledgeGranted event processing — grants fire at dialogue line selection, runtime NPC KG guardrail (D-079, #546) -- ContradictionClaim struct with 600-tick window detection in observe_entity, epistemic neutrality for both sources (D-083, #547) -- NPC-to-NPC knowledge transfer system — trust-gated fact exchange, confidence capping at KnowsOf, ToldBy source construction (D-080, #548) -- tell_state KG awareness — NPC relationship reads from KG for other-entity state, MVP information boundary (D-082, #549) -- Contradiction monologue with pre-resolved entity names, PersonOfInterest relationship shift, THE FRIEND arc event chain (D-083, #550) -- Unprompted disclosure system — DisclosureCandidates component, 7 trigger gates, three-layer rate limiting, two-stage trait filter (D-081, #551) -- Trait modifier system — Cautious/Gossipy/Loyal/Talkative filter predicates via content-authorable config (D-081, #173) -- POI data model and proximity-based discovery system via KnowledgeGranted events (#148, #149) -- Protocol versioning tests — version round-trip, mismatch detection, serde_default migration pattern, full variant coverage (#232) -- Team monitoring rules — heartbeat rule for stuck agent detection, bottleneck detection pattern -- `tooling/tea-comment` — single-command wrapper for posting Gitea PR/issue comments with multi-line bodies -- D-086: Insert icon system — custom SVG icons over icon fonts, authored to insert geometric constraints with lattice_profile weight scaling -- Insert/HUD wireframe and visual spec (#314) — dual character variants (smuggler social network view, detective investigation overlay) with pixel-precise layout, entity markers, time display, border arrows, commission grid, and all interaction states -- Contradiction monologue lines — 16 hand-authored lines (8 detective, 8 smuggler) for Sera/Kael FRIEND arc, Phase 2 blindsiding + Phase 3 pattern recognition, cognitive-dissonance-not-accusation tone per D-083 (#552) -- Diegetic tutorial monologue — 20 lines (10 per character) teaching movement, fog, sound, NPC interaction, and insert/HUD through character voice, fire-once on first-time events (#330) -- Diegetic time display on insert HUD — station local time (HH:MM), day phase with cycle-tinted color, day number on InsertOverlay (#263) -- Relationship color accent on E-Talk overlay — 3px left-edge bar using D-033 palette signals NPC relationship at a glance (#537) -- `Constants.format_game_time()` helper for converting game-minutes to HH:MM station time -- `/sprint-status` cleanup sweep skill — consistent health report with tickets by status, PR cross-reference, bookkeeping issue detection, and open work by team -- `sprint sweep` CLI subcommand — structured JSON output for sprint health checks (grouped tickets, per-team summary, issue detection) -- Knowledge Flow & NPC Boundaries workshop — 5 D-records (D-079–D-083) covering grant architecture, NPC-to-NPC propagation, unprompted disclosure, NPC information boundaries MVP, contradiction detection pipeline -- 7 knowledge graph implementation tickets (#545–#551) with full dependency chain and line estimates -- Contradiction monologue content ticket (#552) for Sera/Kael FRIEND arc -- Sprint 17 completion proofs: contradiction detection fires, NPC-to-NPC knowledge transfers -- Entity renderer migrated from ColorRect placeholders to Sprite2D with D-019 angle sprites — self_modulate for D-033 tinting, 8→4 octant direction mapping, feet-anchored y-sort (#540) - -### Fixed -- Client protocol version bumped to 15 to match server (was still at 14 after server PR #68 added save_result field) -- gen_fixtures.rs version comments changed from hardcoded 14 to PROTOCOL_VERSION constant -- run-ipc-benchmark dead --iterations flag removed (Rust compile-time constant governs rounds) - -### Changed -- Team boundary framing — replaced worktree-centric language with `$WORKTREE_TEAM` env var identity across CLAUDE.md and skills (sprint-start, sprint-plan, pr-review) to prevent agents from following `.git` pointers across boundaries -- CLAUDE.md compacted from 188 to 67 lines — CLI references, endpoints, and patterns moved to `.claude/rules/` -- `/sprint-status` delegates to haiku subagent — keeps sweep JSON, template read, and PR list out of main context window -- `sprint sweep` JSON trimmed — removed unused fields (`ok`, `sprint.status`, `priority`, `ticket_id`), shortened issue detail strings -- Sprint status output template condensed — rendering rules moved to skill definition, bookkeeping table simplified to 2 columns -- Model selection documented in CLAUDE.md — `/model sonnet[1m]` and `/model opus[1m]` for 1M context sessions -- Sprint 17 briefings updated with workshop results — server (14 tickets), copy (2 tickets), client (2), visual (1) -- Q-024 (gossip timing), Q-025 (KG memory), Q-026 (contradiction detection) closed -- Sprint 16 closed (8/8 done) -- 3D sprite render pipeline — Camera3D at D-019 angle (-72.5° from horizontal), three-point studio lighting rig, orthographic projection, resolution chain 1024→256→64 -- Generic NPC capsule model (24×32px footprint per D-044) and structural wall model for pipeline validation -- Test sprites: 8 runtime 64px sprites (NPC + wall × 4 directions) deployed to client/assets/sprites/ -- Pipeline documentation (renderer/README.md) — camera spec, lighting rig, resolution chain, model authoring guide -- DialogueResponse verb handler — players pick dialogue options and receive follow-up lines via full D-028 four-layer pipeline (#539) -- Trust-gated gossip verification — integration tests confirm Secret/Real/Surface tier gating per D-075 (#171) -- Line variety tracker wiring — DialogueCooldownTracker prevents repeat lines within 600-tick window (#338) -- DialogueResponse cross-language fixture for GDScript testing -- Sprint team lifecycle through PR review — teams stay alive for commit → push → review → fix loop → approve → shutdown -- Zone_id extraction in game_state.gd optimized from O(N) tile scan to O(1) dictionary lookup — builds _tile_by_coord from member visible_tiles covering both test and live paths (#543) -- Shared run_dialogue_pipeline() helper eliminates ~60 lines of duplication between Talk and DialogueResponse systems -- Dialogue and monologue line IDs migrated from location-scoped (the-terminal_d_039) to NPC-scoped (kael-davan_d_001) namespace — each NPC has an independent sequence per D-035 (#542) -- DialogueCooldownTracker documented as per-player-global by design (NPC-scoped line IDs per D-035 prevent collision) -- CONFRONTATION_LINES marked TODO for migration to D-028/D-035 content pipeline -- pr-push and pr-review skills updated with team lifecycle awareness - -### Fixed -- PR #59 review: stale mood vocabulary updated in line-pool-format.md, style-guide, and content-directory-structure.md to post-Sprint 14 values -- PR #59 review: orphaned location-scoped IDs in maintenance-tech.yaml comments and smuggler-inventory.yaml cross-references updated to NPC-scoped -- PR #59 review: Lera Sessik tenure corrected from "twelve years" to "eighteen years", NPC header fixed -- PR #59 review: ring-operative.yaml fact_id corrected from `location.surveillance_gaps` to `investigation.surveillance_gaps` -- Dialogue systems moved from BridgePlugin to NpcPlugin — game logic registers where it belongs (#538) -- Schedule ambiguity: emit_observation_events now has explicit .before(advance_tick) constraint -- process_dialogue_response updates ActiveDialogue tick and InteractionMemory on follow-up -- DialogueResponse range check added (CLOSE_RANGE, matching Talk/Confront pattern) -- Weighted selection fallback replaced with unreachable!() — dead code removed -- assert!(false) → panic!() in serialization tests (clippy) -- SetFacing and TeleportToHub added to roundtrip test coverage - -## [v0.1.15] — 2026-02-23 - -### Added -- Sprint 16 "Converse" briefings — 8 tickets across server/client/copy/visual teams -- 19 UI wireframes — HUD, dialogue, monologue, popups, menus in v0.1 and v1.0 variants with D-record cross-references -- d2-diagram skill — text-to-diagram generation with project defaults (theme 200, dagre, PNG) -- frame0-wireframe skill — UI wireframing via Frame0 HTTP API, replaces MCP dependency with bash+curl -- 16 decision diagrams — architecture, data-flow, entity, state, and UI categories covering all project decisions - -### Changed -- frame0-wireframe skill rewritten — JSON-as-truth workflow with frame0-sync.py, batch export, renderer-only guidance -- pr-review skill — all reviewer agents now use worktree paths instead of git show -- Dialogue panel is always visible as permanent insert UI element (D-061) -- Makefile: check-protocol target verifies server/client protocol versions match before build -- D-035 amended: line ID namespace changed from location-scoped to NPC-scoped (Sprint 15) -- Tilemap z-layer filtering — FloorTiles renders z=0 only, z=1/z>1 reserved for future layer nodes (#71, D-049) -- Entity 24x32 footprint per D-044 visual hierarchy — split ENTITY_SIZE into WIDTH/HEIGHT with separate offsets (#72) -- Follow target stub on GameState — `follow_target_id` field ready for server #241 Follow verb -- Manual exponential camera smoothing — CAMERA_SMOOTHING_SPEED constant (8.0), same lerp pattern as entity renderer (#117) -- 31 new Sprint 15 validation tests — camera smoothing, UI framework z-layers, entity footprint, Sprint 14 regressions -- SpatialIndex trait with naive Vec implementation — entities_in_range, entities_at, update methods with Manhattan distance (#340) -- NPC generation pipeline — procedural seeding of all 10 D-024 axes via SimRng with constraint validation (#92) -- Personality and tell system — 5 tell categories (Nervous, Angry, Friendly, Guarded, RoutineDeviation) derived from NPC axis values each tick (#90) -- Tolerance threshold monitoring — ToleranceBreachEvent on stress exceeding per-NPC threshold, mood FSM integration (#105) -- Routine deviation detection — RoutineDeviationEvent on wrong location/activity for day phase, absence detection, pathfinding-aware (#243) -- Follow mechanic — Follow verb, proximity/LOS tracking, double-frequency observation events, NPC suspicion accumulation, configurable thresholds (#241) -- Monologue event triggers — observe_npc, hear_sound, observe_anomaly, witness_interaction, post_conversation with D-035 context tags (#119) -- Protocol v13 — tell_state on VisibleEntity, follow_state on ObserverSnapshot, Follow verb - -## [v0.1.14] — 2026-02-21 - -### Added -- Unified dialogue log — player-NPC and overheard NPC-NPC conversations in one chronological scrolling panel (#535, D-061/D-078) -- F3 debug overlay — real-time game state display with tick, FPS, position, entity counts, dialogue/monologue status (#511) -- Monologue display — multi-line priority queue with character colours, italic BBCode, stagger animation (#122) -- Protocol v9 — conversation_events, conversation_ended, dialogue_response fields with carry-forward logic -- Dialogue theme system — configurable NPC name colour palette, entry timing, passive opacity via dialogue-theme.yaml -- Monologue display system visual spec — typography, positioning, stacking, priority, fade animation, character color differentiation, 80-char line constraint (#315) -- Entity color system spec — D-033 relationship-to-player mapping, transition animations, color blindness assessment (#304) -- Text display hierarchy spec — 4 content pipelines (dialogue, monologue, observation, environmental) with z-layers and positioning (#316) -- Sound indicator visual design — fog-edge pulse for D-018 three-range sound model with direction encoding and range differentiation (#317) -- THE FRIEND visual treatment spec — 3-phase earned visual detail for Kael Davan and Sera Venn (#318) -- Environmental text visual standards — signage, terminal, and news ticker rendering with bilingual Concordat/Van Maanen's Star treatment (#334) -- Tell visual/behavioral expression spec — 5 tell categories mapped to 6 Tier 2 behaviors (#251) -- Monologue line pool maxLength raised from 160 to 256 chars (soft guidance ≤160) -- NPC name masking infrastructure — entity-anchored dialogue log with server-side role labels, retroactive name update on learning, NpcColorIndex for stable color assignment -- Dialogue option keyboard selection (1/2/3 number keys) and numbered option labels -- Interaction list chrome — background panel, mouse hover highlighting, click-to-interact, pointing hand cursor - -### Changed -- D-061 updated to document unified conversation log architecture from Sprint 14 -- Dialogue options switched from RichTextLabel to Label for reliable VBoxContainer sizing - -### Fixed -- Visual grammar dialogue max-width corrected from "~70% screen width" to 640px per D-076 -- BBCode injection in dialogue log formatting — server-sourced strings now escaped with [lb] -- Per-frame dialogue log rebuild replaced with dirty flag (performance) -- dialogue_active lifecycle — now cleared after panel fade completes per D-064 -- PAUSE/UNPAUSE routed through main.gd input recording for bug report replay (#507) -- WASD input freeze after filing bug report — LineEdit focus not released before queue_free() across CanvasLayers -- WASD not reactivating after Talk — dialogue_active held for entry_lifetime instead of cleared immediately -- Recognition chime spam — entity IDs now tracked permanently per room instead of expiring -- Audio path warning — res://audio/ corrected to res://assets/audio/ in AudioManager -- world_radial.tscn anchors_preset warning — changed from 15 to 0 -- bug_report_dialog.gd push_warning changed to print for informational message - -## [v0.1.13] — 2026-02-20 - -### Added -- D-078: Overheard NPC conversation — passive dialogue panel with server-authoritative stochastic word occlusion -- Sprint 14 "Live" briefings — 22 tickets across server (7), client (3), copy (6), visual (6) - -## [v0.1.12] — 2026-02-19 - -### Added -- Tier marker components (#93), active tier simulation (#94), tier transition logic (#99) -- Information tag schema (#138), component-level access control (#139) -- Line previewer CLI (#193) -- Sound event system — server pipeline (#124) -- Close-range stereo audio — client positional 2D (#125) -- Medium-range visual indicators — fog-edge directional arrows (#126) -- HashMap ban in simulation crate via clippy (#343) -- Tracing crate infrastructure — JSON format, tick duration logging (#344) -- System dependency graph debug command — `--dump-schedule` CLI flag (#346) -- rng_seed field on ObserverSnapshot for deterministic replay (#527) -- v0.1 Visual Grammar Document (#303) -- Placeholder art specification (#252) -- Spatial layouts: Logistics Hub (#311), Bar (#312), Smuggling corridors (#313) -- Cultural generation guide — 5-dimension framework for Sova Transit District cultural voice (#189) -- Sova Texture Appendix — 20-term slang glossary, sensory profile, Meridian self-censorship rules (#302) -- Contraband specification — unlicensed lattice components, supply chain, street terminology (#321) -- Sova Station Profile — 6 districts, governance, off-station references (#320) -- Span Gate Transit Schedule — hourly schedule, maintenance windows, ring operational calendar (#336) -- Meridian Coverage Map — 10 named zones from Commission-grade to dead air (#335) -- Character definition schema and both character builds — smuggler + detective (#179, #180, #181) -- Divergent starting knowledge and relationships per character (#182, #183) -- Detective institutional chain of command (#322) -- Contradiction arc design document — reusable FRIEND pattern (#332) -- Mirror moment design document — 7 core dual-perspective observation triggers (#329) -- First 5 minutes experience design — systemic opening per character (#259) -- Opening hook content per character (#260) -- Knowledge vocabulary for v0.1 content — entity/world categories, prerequisite format (#368) -- Knowledge state vocabulary — author-facing quick reference (#309) -- Knowledge fact catalogs — 10 YAML files in content/global/knowledge/, 73 canonical facts -- D-075 endorsement — archetype dimension review recorded in decisions/content.md -- Flat NPC memorable trait pass — Pael, Ren, Tev with noise-floor profiles (#307) -- Environmental text content — 20 items across Terminal, Bar, and Corridors with dual-lens notes (#262) -- Diegetic insert flavor text — per-character labels and notification strings (#331) -- News ticker / Meridian feed — 30 lines including batch 44xx recall dual-lens moment (#306) -- Workplace content pack — The Terminal: 5 NPC dialogue files (#190) -- Bar content pack — The Last Shift: 3 NPC dialogue files (#191) -- Smuggling ring content pack — maintenance corridors: coded vocabulary, dual registers (#192) -- Generation pass expansion — 80 ambient variant lines across all 9 dialogue files (#194) -- Sprint 13 "Sound" briefings — 9 tickets across server, client, audio, visual teams; full audio architecture + gauntlet expansion + monologue display spec - -### Fixed -- Entity renderer field name bug — `id` vs `entity_id` (#345) -- Dialogue max-width pixel value — 640px per D-076 (#447) -- Routine tests missing ActiveSim — 3 of 5 tests passed trivially without the required tier marker -- `_observer_pos` misleading unused prefix renamed to `observer_pos` (used for sound event filtering) -- Stale protocol version doc comment "Current: 9" corrected to 10 -- FactionOnly non-numeric `faction_id` attribute now logs a tracing::warn instead of silently denying -- SOUND_EVENT_ASSETS walk-speed key mismatch — `sfx_footstep_metal` corrected to `sfx_footstep_metal_walk` - -### Changed -- Removed orphaned `SimulationTier`/`LastInteraction`/`ScopeTag`/`ScopeKind` types from tier.rs (unused outside own tests) -- Sound pipeline documented as intentionally empty in v0.1 (no producers yet, full pipeline wired) -- Observer test setup now inserts SoundEventQueue resource for integration coverage -- Added FactionOnly positive test case and Medium-range occlusion TODO -- Sound indicator colors sourced from Constants instead of duplicated hex literals -- play_loop() null guard on stream.duplicate() -- Camera zoom fallback uses Constants.CAMERA_DEFAULT_ZOOM - -## [v0.1.11] — 2026-02-19 - -### Added -- Sprint 12 "Build" briefings — 50 tickets across server, client, copy, visual, ci teams; production-layer foundations + all v0.1 copy authoring -- `.tmp/` gitignored repo directory for agent temp files — avoids Bash permission prompts during PR review comment posting -- `sed -n` blanket permission in shared settings - -### Changed -- All skills renamed to domain-action convention (e.g. `commit`→`git-commit`, `review-pr`→`pr-review`, `gen-audio`→`audio-gen`, `render-sprite`→`sprite-gen`) — 12 renames total -- `pr-review` skill uses Write tool into `.tmp/` instead of Bash heredocs to `/tmp/` - -## [v0.1.10] — 2026-02-19 - -### Added -- `project.yaml` — technical project descriptor with version, architecture, simulation, and content model as the canonical version source of truth -- Scratchpad: asset generation pipeline idea (registry, status tracking, prompt versioning, pre-sprint cohesion) -- Scratchpad: remote terminal proxy idea for mobile monitoring of Claude Code permission prompts and interactive elements -- `make perf-baseline` — full plugin stack tick benchmark (50 measured ticks, 5 warmup) capturing per-tick timing, entity counts, process RSS, and shadowcast benchmarks; outputs structured JSON to `tests/perf/baseline.json` with `--compare` mode for regression detection (>20% threshold, D-026 budget check) -- Michroma font integration (#517) — Michroma-Regular.ttf as game font with +1px tracking FontVariation, global Theme with cyan-white (#E0F7FA) implant text color, IMPLANT_TEXT_COLOR/DIM/PULSE constants -- Mouse-relative facing and movement (#526, D-054) — mouse position determines facing direction (client-side float), WASD remapped to cursor-relative (W=toward, S=away, A/D=strafe), SET_FACING action sends octant to server, smooth facing indicator rotation -- Room reset client UX (#502) — amber reset_plate tile type, 0.15s screen flash on room reset, 'Reset Room' interaction verb -- Auto-checklist progress tracking (#503) — ChecklistEvaluator parses room YAML and evaluates 7 condition types against GameState with latching, ChecklistOverlay renders progress in gauntlet mode only, 48 new tests -- 4 ambient zone loops: station base, workplace, bar, corridor — SAO-generated organic soundscape with crossfade loop points (#327) -- 2 footstep SFX: metal walk and run — SAO hybrid with best-transient extraction (#327) -- `audio-batch` command — batch audio generation from JSON manifests, supports SAO and harmonic synthesis, with `--dry-run`, `--only`, and `--skip-existing` flags -- `--post` and `--output-ogg` flags on `audio-generate` — chain post-processing (trim, normalize, convert) into a single command - -### Changed -- `push-pr` skill now runs `/commit` first when uncommitted changes are detected -- Insert open/close now sends explicit PauseSimulation/ResumeSimulation (#518, D-058) — replaces toggle-style pause with idempotent pair -- Interaction list colors reference Constants.IMPLANT_TEXT_COLOR instead of hardcoded values -- World radial menu uses theme font instead of ThemeDB.fallback_font -- Monologue chimes replaced with production-quality manual synthesis — insert-tech aesthetic per D-074, pure sine harmonics with mathematical envelopes (#327) - -### Fixed -- Bidirectional relationship check (#515) — Check 9 tested `target in npc_rels` which missed NPCs with no relationship entries; changed to `target in self.npcs` - -## [v0.1.9] — 2026-02-18 - -### Fixed -- `make game` now builds client before launching — was missing `build-client` dependency, causing class_name registration failures after `make clean` -- `build-client` uses `--import --quit` instead of just `--quit` — ensures `.godot/` cache and `global_script_class_cache.cfg` are created from scratch -- `make clean` preserves `client/.godot/` directory (clears contents only) to avoid Godot startup issues - -### Added -- `--description TEXT` flag for `ticket create` CLI — previously required raw SQL workaround to set ticket descriptions - -### Changed -- UI audio assets revised — monologue chimes re-generated (0.8s, insert-tech aesthetic), fog_recognition re-generated (was silent), all 8 assets normalized to 44.1kHz stereo LUFS -16 (#453) -- review-pr skill: explicit verdict rules — critical/warning → REQUEST_CHANGES, suggestion-only → APPROVE - -### Added -- `make golden-diff` + `make golden-update` targets (#486) — developer workflow for golden file comparison and regeneration; safe restore on cargo failure -- Gauntlet checklist YAML schema (#497) — 7 condition types evaluable from ObserverSnapshot, per-room checklists for 3 rooms, `make checklist-validate` and `make checklist-generate` targets, wired into `pre-pr-content` gate -- Gauntlet room timer + personal bests (#496) — GauntletHUD shows TIMER: MM:SS (PB: MM:SS), starts on room entry, resets on room change, persists stats to user://dev/gauntlet-stats.json, session summary on disconnect, hidden in non-gauntlet mode -- WRONG button F12 MVP (#495) — bug report capture: pause sim, show modal prompt, save snapshot.json + render.txt + description.txt to user://bug-reports/, Esc to cancel -- GameState.room_id and gauntlet_mode fields — parsed from ObserverSnapshot, enabling gauntlet UI -- BUG_REPORT action in InputMapper (F12 binding) with SimBridge wire guard (client-only) -- 24 new anti-tedium tests — GauntletHUD lifecycle (16: timer, PB, visibility, room change, session tracking) + BugReportDialog (8: pause/unpause, wire guard, text render, edge cases) -- whatsinagame starter kit — reusable multi-agent team bootstrap for any project (3-tier profiles, 18 skills, 16 agent archetypes, stakeholder personas, ticketing DB, decision tracking) -- Domain-action naming convention for skills documented in create-skill guide -- `gauntlet` feature flag (default-on) — allows stripping Gauntlet test world from release builds with `--no-default-features` -- `EXPECTED_ENTITY_COUNT` and `RESET_PLATE_STABLE_IDS` constants — entity counts derived from StableId ranges instead of hardcoded values -- Debounce exact-boundary test for room reset (tick 9 rejected, tick 10 accepted) -- Reset plate StableId verification in `stable_id_ranges_match_spec` -- Runtime content test (`content_runtime.rs`) separated from structural loading tests -- Client P2 tests (#492) — 16 gdUnit4 tests for camera (smoothing, zoom, viewport, follow, no-pan), entity alpha/color (peripheral, forward, NPC color, player constant), UI (monologue, interaction, inventory, dialogue, pause, fog blob, fog z_index) -- Client P3 tests (#493) — 12 gdUnit4 tests for z-layer ordering (floor/ysort/fog/UI), entity lerp (snap, converge, LERP_SPEED=12.0), Tyre additions (recognition, facing, delta scaling, blob removal) -- Anti-tedium regression tests (#494) — 5 tests: F12 no-crash guard, no queued input, gauntlet UI hidden in default/normal snapshot/multi-tick modes - -### Changed -- Room reset API consolidated to `plan_reset` only — `execute_reset` removed (was a maintenance trap; production uses Commands via `plan_reset`) -- `room_at()` documented with z-range and corridor overlap assumptions -- TCP runtime test now has 10s read timeout and registers player in EntityRegistry - -### Removed -- Dead `RoomMember` component from reset.rs (defined but never used) -- Protocol v8: dialogue_response field decoding (DialogueResponseEvent with line_id, text, speaker_entity_id) from server #305/D-028 -- Test client binary scaffolding (#480) — standalone crate at `tooling/test-client/` with CLI (--connect, --replay, --text, --json, --quiet, --golden, --ticks), exit codes (0/1/2), golden file JSON diff, JSONL replay loader -- Snapshot text renderer (#481) — `format_snapshot_text(&ObserverSnapshot)` pub-exported from server crate, entity labels as kind:entity_id sorted by distance, room name stub, 10 unit tests -- Sprint 9 (Gauntlet) briefing files — server, client, CI, audio, joint — 23 tickets across 4 teams -- Weapon aim lock audio (`sfx_weapon_aim_lock.ogg`) — clinical targeting confirmation tone for weapon aim state (#440) -- Stance change audio (`sfx_stance_change.ogg`) — subtle mechanical click for stance toggle feedback (#440) - -### Fixed -- MessagePack int_64 encoder dead code branch (#516) — `-(1 << 63)` overflowed making int_64 branch unreachable; negative values beyond int_32 now correctly encode as 0xd3 instead of 0xcf -- Cross-encoder fixture pipeline hardened — encode failures now exit non-zero instead of writing empty .msgpack files (#475 review) -- GDScript fixture test no longer silently skips on missing/empty fixture dir — asserts instead (#475 review) -- GDScript fixture generator covers all PlayerAction variants (added MoveSouth, MoveEast, MoveWest, Unpause, ToggleStanceDown, WalkAway) -- `make pre-pr` now checks GDScript fixture staleness alongside Rust fixtures -- Protocol version bumped from 7 to 8 to match server — fixes 5 test failures from version mismatch -- Interact action encoding changed from unit variant to struct variant to match server's PlayerAction::Interact { target_entity_id, verb } -- Monologue duplication test (test_monologue_not_duplicated_after_consumption) fixed — was using poll_snapshot() which doesn't consume _last_snapshot in test mode -- `make pre-pr` target — full pre-PR verification chain: lint → build → test → content validation → fixture staleness (#460, #465) -- Branch-specific pre-PR variants: `make pre-pr-server`, `make pre-pr-client`, `make pre-pr-content` -- Content cross-reference validation (9 checks) — canonical_id uniqueness, relationship targets, location slugs, dialogue locations, fact_ids, triangle membership, npc_count, dialogue line_ids, bidirectional relationships (#464) - -### Fixed -- Dialogue schema missing `focused` (mood) and `greeting` (situation) values added in Sprint 7-8 content -- Speaker wire ID silent fallback — dialogue now warns and skips when target entity missing from registry (was silently using 0) -- Cross-plugin system ordering — trigger_recognition_monologue now runs after detect_anomalies (latent determinism bug) -- Walk-away ordering — process_walk_away now runs after process_talk_interaction (prevents same-tick race) -- ActiveDialogue overwrite — new Talk while in existing dialogue now emits IncompleteInteraction before replacing -- Server-side Talk range check — handle_talk now enforces CLOSE_RANGE before setting TalkRequest (was client-only) -- ExamineNpc label collision — VerbKind::ExamineNpc now uses "Examine NPC" label (was "Observe", same as generic Observe) -- Dead conditional in main.rs collapsed (both branches were identical) -- WalkAway variant added to all_player_action_variants_roundtrip serialization test - -### Changed -- DialogueCooldownTracker.used changed from Vec to BTreeMap for O(log n) lookup (D-041 compliance) -- MonologueState.shown_ids changed from Vec to HashSet for O(1) contains check (was O(n) per tick) -- Secret trust tier documented as unreachable with TODO for Phase 2 KG-gated unlock - -### Added -- Determinism test: different_seed_produces_different_replay — exercises SimRng via dialogue weighted selection -- Dialogue selection pipeline (#305, D-028) — 4-layer filtering engine: access tier from KG relationship, situation derivation from game state, trust tier, weighted topic+mood scoring via SimRng. Full Talk verb → selected line → ObserverSnapshot pipeline with cooldown tracking -- ContentSlug component (#452) — stable content identity from YAML (e.g. "kael-davan") independent of runtime Entity handles, for knowledge graph interaction memory across save/load -- Walk-away KG recording (#427, D-064) — IncompleteInteraction knowledge events with Talk/Confront type, ActiveDialogue tracking, walk-away detection clears dialogue and records in KG -- Anomaly detection for urgent recognition (#450, D-060) — AnomalyMarker flags PersonOfInterest/Contradicted entities for 0.3s cognitive delay instead of 0.6s normal -- Recognition monologue during cognitive delay (#451, D-060) — monologue fires at delay START (grey blob phase), not completion. v0.1 fallback lines, anomaly prioritization, cooldown tracking -- Server --test-mode, --port, --seed CLI flags (#459) — LISTENING:{port} stdout signal, OS-assigned ports, deterministic seed override, stderr-only tracing -- Determinism gauntlet test (#466) — 20-tick replay determinism regression test with movement, stance, pause/unpause exercise -- Pause guard test suite (#461-463, #468) — 7 tests covering movement, unpause, roundtrip, stance, interact, batch, tick_rate during pause -- EntityRegistry lifecycle tests (#469) — stale mapping, re-register, unknown unregister edge cases -- Boundary value encode/roundtrip tests (#471) — 41 values across all MessagePack integer format boundaries -- Encoding asymmetry tests (#473) — Rust decoder accepts GDScript-style signed encodings for unsigned fields -- Boundary fixture generation (#472) — 14 raw + 5 snapshot fixtures at integer format boundaries -- Malformed batch rejection test (#479) — truncated, garbage, and mixed payloads rejected atomically -- Per-fix determinism unit tests (#467) — equidistant NPC ordering, visible tile sorting, same-tile mover resolution - -### Fixed -- Determinism: visible_ids HashSet → BTreeSet for stable iteration order (#456) -- Determinism: visible entities in snapshot sorted by entity_id (#457) -- Determinism: movers sorted by Entity bits in validate_movement (#458) -- Pause guard blocks all actions except Pause/Unpause while paused (previously only blocked movement) - -### Changed -- Protocol version bumped from v7 to v8 (dialogue_response field in ObserverSnapshot) - -### Added -- AudioManager autoload (#255, D-068/D-069/D-073) — 5-bus architecture (Music, Ambient, WorldSFX, PlayerActions, UISounds), directory-scan asset registry, spatial/non-spatial playback, audio dip profiles (dialogue, confrontation, listening_focus) with low-pass filter sweep, zone crossfade stub -- Dialogue response selection (#435, D-061/D-062) — structured options with response_id, priority sorting, max 3 visible, invisible locked options, RichTextLabel for BBCode support -- Walk-away mechanic (#437, D-064) — WASD triggers WalkAway input during dialogue, 300ms fade, dialogue_active flag gates movement, re-show on server interrupt -- Confrontation text styling (#436, D-063) — italic first-person options, 1.5s monologue beat with dialogue dim to 70%, audio dip via AudioManager, walk-away cancels in-flight beat -- MessagePack boundary value tests (#470) — 41 values (25 positive, 16 negative), encode-only header verification, roundtrip, Rust-style unsigned decode overlap tests -- Client P0 regression tests (#477) — monologue carry-forward (Bug #5), camera stability during pause (Bug #2) -- Client P1 tests (#478) — fog shader state (4), entity lifecycle (2), pending recognition blob (1) - -### Changed -- Fog byte magic numbers replaced with named constants (#476) — VIS_HIDDEN/PERIPHERAL/FORWARD, EXP_UNEXPLORED/EXPLORED/VISIBLE in FogState -- Dialogue options now carry structured {text, response_id, priority, confrontation} instead of plain strings -- Protocol v7 dialogue decode validates and skips malformed options - -### Added -- QA test architecture workshop complete — 3-round, 7-agent workshop producing 60 tickets (epic #455): Gauntlet test world (7 rooms, 48 entities), test client binary (tooling/test-client/), determinism fixes, content validation, make pre-pr pipeline, 38 client tests, anti-tedium features, human tester workflow -- Workshop skill updated — agents now write output files to disk instead of sending messages, fixing documenter access -- Camera anchor test suite — 10 gdUnit4 tests verifying camera init, smoothing cycle, and player tracking -- Entity position lerping — framerate-independent exponential smoothing so entities slide between tiles instead of snapping - -### Fixed -- Server never sends snapshots — blocking TCP read in receive_bridge_inputs stalled the entire bevy Update schedule; switched to non-blocking I/O with WouldBlock handling -- Camera doesn't center on player at startup — Camera2D smoothed_camera_pos starts at (0,0); now disable smoothing during init, snap to player, re-enable after first anchored frame -- Player moves while game is paused — movement commands now discarded when TickRate is Paused (pause/unpause still process) -- Spacebar only pauses, doesn't toggle — added UNPAUSE action with toggle logic based on tick_rate state -- MessagePack encodes tick 128 as -128 — off-by-one in signed int boundary checks (<=128 instead of <128) across int8/16/32/64 branches -- Monologue/dialogue lost on snapshot overwrite — one-shot events now carried forward when a newer snapshot replaces an unconsumed one -- Fog shader white screen on load failure — ColorRect defaults to transparent, shader load failure logged instead of crashing -- Fog desync during camera smooth pan — fog rect now tracks camera position instead of player position - -### Changed -- Server game loop throttled to ~20 ticks/sec (50ms frames) — non-blocking TCP loop no longer spins; remaining frame budget available for NPC AI -- Hold-to-move input model — movement polled each frame with stance-based throttle (Sprint=200ms, Walk=400ms, Careful=600ms, Crouch=800ms) and composite diagonals (W+D → northeast) -- D-053 updated with client throttle rates and input model documentation - -### Added -- Dialogue box UI skeleton (#434, D-061) — bottom screen, max 20% height, ~65% width, NPC speech + max 3 response options, insert-styled colors, WASD walk-away with 300ms fade, no close button, diegetic on InsertOverlay z-layer 6 -- Fog entity visualization (#431, D-059/D-060) — cognitive delay rendering: sonar-style sound pings (3 concentric rings, 1.5s fade), unrecognized grey blobs with 0.8s breathing pulse, D-033 color transition at 50% recognition progress, ±0.5 tile position drift, FogEntities node at z:950 - -### Changed -- Client protocol version bumped from 6 to 7 (pending_recognitions decode for cognitive delay) -- GameState: current_dialogue and pending_recognitions fields wired from ObserverSnapshot v7 -- Scene tree: DialogueBox added to InsertOverlay, FogEntities at z:950 between fog shader and InsertOverlay -- Test mode: mock dialogue (Kael NPC, 3 options) and mock cognitive delay entity (6-tick recognition cycle) - -### Added -- Archetype evidence presentation spec (#443, D-065/D-034/D-033) — detective case file vs smuggler notebook design document: item definitions, knowledge graph presentation, contradiction markers, THE FRIEND arc walkthroughs, systems interaction map, authoring guidelines -- Cognitive delay system (#423, D-060) — CognitiveDelay component buffers perception events before emitting KnowledgeEvents (0.6s base / 0.3s urgent at 10 tps), pending_recognitions in ObserverSnapshot v7 for client fog entity visualization, cancellation on entity LOS exit -- ListeningFocus eavesdrop system (#426, D-053) — stationary_ticks tracking for eavesdrop positioning bonus, 30-tick threshold (20 for Careful stance), Sprint blocks accumulation, registered after validate_movement -- YAML content loader with hot-reload (#326, D-028) — LinePool system parsing dialogue/monologue YAML into BTreeMap-indexed pools, 4-layer query filtering (access > situation > trust > topic+mood), timestamp-polling hot-reload (dev-only), graceful failure preserves previous content -- Line pool format specification (#308) — formal spec at docs/architecture/line-pool-format.md defining YAML structure, tag enums, 4-layer filtering pipeline, prerequisite-to-KG mapping, ID format, and Rust loader interface -- InteractionMemory KG schema design (#442, D-064) — design doc at docs/architecture/interaction-memory-schema.md extending FactKnowledge with interaction tracking, 5-state InteractionState enum, monologue prerequisite extension, NpcTolerance reconciliation -- Audio discussion decisions D-067 through D-074: recognition chime timing, 5-bus architecture, audio dip profiles, confrontation as cognitive vulnerability, monologue chime placeholder strategy, universal conversation murmur, zone crossfade, hybrid audio generation -- Asset pipeline documentation system (docs/assets/) with category-based index, sonic palette, and generation templates for audio, visual, and video pipelines -- Stable Audio Open connector and post-processing wrappers (audio-generate, audio-health, audio-post) with timeout handling for 11GB VRAM constraint -- gen-audio skill with prompt assembly system (sonic palette prefixes + category templates + asset descriptions) -- 6 interaction UI audio assets (#440): cursor_hover, weapon_aim, implant_open, fog_recognition, sfx_monologue_chime, sfx_monologue_chime_urgent — generated via SAO, needs duration trimming (#453) -- Dialogue/confrontation ambient dip implementation spec with full Godot AudioBus tween code -- Synthesis tooling (tooling/synth_ui_sounds.py) for programmatic insert-tech sound generation - -### Changed -- Protocol version bumped from 6 to 7 (pending_recognitions field in ObserverSnapshot) -- MessagePack fixtures regenerated for protocol v7 -- Monologue trigger system uses .values() iterator (clippy fix) -- Monologue schema: relationship prerequisite now requires target and state fields -- 389 tests total (70 new) — cognitive delay pipeline, line pool loader, ListeningFocus, content watching, serialization -- Renamed asset-gen skill to gen-image for consistent gen-* naming pattern -- Client protocol v6 bridge — decode player_stance (4 variants) and player_inventory from ObserverSnapshot, TOGGLE_STANCE_UP/DOWN input actions, 25 gdUnit4 tests -- Three-scope z-layer rendering pipeline (D-049) — world z:0-900 inside CanvasGroup, insert overlay CanvasLayer 10, UI CanvasLayer 20, modal CanvasLayer 30. Y-sort contract enforced, reserved VFX/airborne/lower-floor ranges documented -- Cursor state machine (#429, D-056) — 4 states (Default/EntityHover/ObjectHover/WeaponAim), 150ms transitions, insert-styled geometric shapes -- Fog shader rebuild (#430, D-059) — 5-layer fragment shader with animated Perlin noise, CanvasGroup compositing, FogState autoload for visibility/exploration textures -- Entity interaction list (#432, D-057) — vertical multi-verb menu, insert-styled, sprint suppression, diegetic toggle -- World radial menu (#433, D-058) — 2 spokes (Observe + Insert), drag-release and click-click input, 60-degree acceptance zones -- Inventory UI (#438, D-065) — 3x3 grid, 40x40px slots, 1-9 hotkey selection -- Stance indicator (#439, D-053) — color-coded HUD text, C/X keybinds -- Architecture docs: z-layer gap analysis, fog shader spec, flying taxi feasibility analysis - -### Changed -- Scene tree restructured: Entities z_index 3->0 (critical y-sort fix), FloorObjects->10, YSortGroup->100, Overhead->300, FogOverlay->900, ModalLayer added -- constants.gd rewritten with three-scope z numbering and full reserved range documentation -- Fog renderer replaced: TileMapLayer-based fog_renderer.gd deleted, replaced by shader-based fog_shader.gd + fog.gdshader - -### Added -- ObserverSnapshot v6 wire protocol (#449) — player_stance (MovementStance) and player_inventory (Vec\) fields with serde defaults for backward compatibility -- Stance system (#417) — Sprint/Walk/Careful/Crouch movement stance with tick-based speed (1/2/3/4 ticks per move), monologue rate multipliers (40%/100%/150%/100%), PlayerMoveCooldown component, ToggleStanceUp/Down player actions -- TilePresence posture layers (#420) — Standing/Prone/Seated/Fixture occupancy layers enabling same-tile coexistence (e.g. seated NPC + standing player), layer-based collision in validate_movement -- ObjectType component (#421) — Readable/Container/Terminal/Door/Pickup/Furniture types with Phase 1 verb sets computed from type + proximity range -- Phase 2 verb filter (#422) — KG-gated observer-side verb processing: POI priority flips (D-060), Confront injection at KnowsDetails+ confidence, contradiction marking, archetype-specific label relabeling (Smuggler sees Move/Stash, Detective sees Scan/Flag on containers) -- CharacterArchetype component — Smuggler/Detective archetype for Phase 2 verb label differentiation (D-057) -- VerbKind::Confront — Phase 2 only verb injected when observer has KnowsDetails+ on an NPC at close range -- Smuggler inventory system (#424) — CarriedBy(StableId) component, Take/Place verbs, 9-slot (3x3 grid) capacity, auto-slot assignment, info boundary enforcement (carried items invisible to other observers) -- MovementProfile component (#418) — per-archetype default stance (smuggler=Walk, detective=Walk), applied on spawn, factory methods for future archetypes -- Sprint interaction buffer suppression (#419, D-055) — sprint stance explicitly clears interaction buffer, no verbs computed or sent during sprint, anomaly monologue pipeline unaffected -- Sprint anomaly double-take monologue (#428, D-055) — SprintAnomalyQueue component detects Contradicted entities during sprint, fires delayed retroactive monologue after ~1.5s ("Wait — something wasn't right back there"), first-in-wins queue semantics, 3 hardcoded v0.1 lines - -### Changed -- Protocol version bumped from 5 to 6 (stance, inventory, ObjectType, verb system fields) -- MessagePack fixtures regenerated for protocol v6 -- Input processing queries expanded for stance and cooldown components with backward-compatible Option wrapping -- Observer pipeline queries expanded for Stance and CharacterArchetype components -- NearbyInteraction carries object_type and contradicted fields for Phase 2 context -- BridgePlugin system ordering: process_sprint_anomaly_monologue runs after trigger_monologue, compute_observer_snapshot runs after anomaly processing -- Player spawn includes MovementProfile, Stance, PlayerMoveCooldown, and SprintAnomalyQueue components -- 331 tests total (131 new) — comprehensive QA coverage across stance, occupancy, Phase 2 verbs, sprint suppression, inventory, anomaly monologue, and wire format - -### Added -- D-066: Dual-scale grid — 0.5m simulation tiles for stealth granularity, 1m visual tiles for proportional art (2x retina factor). All world geometry 2x2 sim tile minimum so cover/LOS maps 1:1 with visuals. Amends OQ-01. -- Sprint CLI (`db/connectors/sprint`) — unified sprint lifecycle management with 5 subcommands: status, start, stop, start-work, prepare. Auto-detects sprint from DB state and team from git branch. Guards prevent activating unplanned sprints. -- Shared permission settings in `.claude/settings.json` — git, ticket/sprint CLI, make, tea, and core skills pre-approved across all worktrees. Deny rules block destructive operations. -- Decisions D-053 through D-065 from Control & Interaction Workshop — formalized interaction verb system, contextual actions, NPC awareness model, and related design decisions -- Sprint 6 Touch briefings for server, client, copy, and joint teams -- Control & Interaction Workshop outputs — full workshop notes and outcomes -- Smuggler inventory item specs for transit district (#441) -- Start-workshop skill for multi-agent design workshops - -### Fixed -- Added worktree boundary rules to CLAUDE.md — agents must stay within the git root, no navigating to sibling worktrees or above the repo -- Plan-sprint skill now enforces worktree-relative paths in generated briefings -- Worktree-update skill now discovers branches dynamically via `git worktree list` instead of relying on hardcoded branch names — fixes missed branches like `planning` - -### Changed -- Start-sprint and plan-sprint skills updated to use sprint CLI instead of manual multi-query workflows -- Permission syntax migrated from deprecated `:*` suffix to modern space-wildcard format across all worktrees -- Internal monologue trigger system (#414) — enter_location fires on first tick, time_idle fires after 100 ticks of no movement, 300-tick cooldown, dedup within session, random line selection from content pools via ChaCha20 RNG -- MonologueEvent in ObserverSnapshot v5 — current_monologue field carries id, text, and display duration across the IPC bridge -- Client monologue display wiring — protocol v5 decoding, GameState extraction, HUD display pass-through - -### Fixed -- NPC spawn missing Interactable component (#413) — NPCs spawned from content and proof room now have Interactable, enabling E-prompt detection -- PlayerAction::Interact was a no-op (#415) — changed from unit to struct variant with target_entity_id and verb fields, server logs interaction data - -### Changed -- Protocol version bumped from 4 to 5 (MonologueEvent field, Interact variant change) -- MessagePack fixtures regenerated for protocol v5 - -### Changed -- UIStrings YAML parser now handles arbitrary nesting depth and inline comments — adapts to copy team's restructured ui-strings.yaml with multi-level sections (relationship_states, health_values) -- HUD uses new YAML keys: `hud.perception_mode_prefix`, `hud.time_prefix`, `hud.health` (empty prefixes display values directly) -- Interaction prompt keybind hint ("E") hardcoded instead of loaded from YAML — keybinding is not copywriter text - -### Added -- Pre-commit FactId validation hook (#393) — grep-based check validates fact_id references in content YAML against canonical knowledge catalogs; advisory mode when catalogs are stubs, enforcing mode when populated -- Pre-commit hook infrastructure — `.config/hooks/` with modular dispatcher, `make setup-hooks` target, `core.hooksPath` config for worktree-safe hook installation -- `make check-fact-ids` target for manual fact_id validation -- UIStrings autoload with YAML-based UI string loading (#409) — minimal YAML parser, `get_text()` lookup with fallback-to-key -- HUD and interaction prompt labels now loaded from `client/data/ui-strings.yaml` instead of hardcoded strings -- Character voice speech patterns (#310) — sentence-level execution spec for smuggler and detective covering contractions, punctuation, stress markers, vocabulary, verbal tics, and authoring checklist -- NPC authoring style guide (#379) — 954-line handbook: tier budgets, 9 NPC patterns, dialogue/monologue rules, tag taxonomy, dual-lens coordination, Van Maanen's Star/Sova culture, FRIEND phase mapping, validation checklist -- UI microcopy (#409) — 72 YAML strings for client integration: interaction verbs, relationship states, HUD labels, perception modes, notifications, knowledge panel, tutorial prompts -- Kael Davan FRIEND pack (#297) — 86 hand-authored lines across 3 locations, 5-phase relationship arc with contradiction scene, dual-lens notes -- Sera Venn FRIEND pack (#298) — 75 hand-authored lines at The Last Shift, trust-gated gossip, avoidance contradiction, contaminated trust arc -- PC-as-NPC content (#401) — 70 authored items enabling second-playthrough recognition (D-039 wow moment #4) - -### Changed -- Moved ui-strings.yaml from content/campaigns/_meta/ to client/data/ for direct Godot client loading - -### Fixed -- Fact ID format collision across FRIEND content — normalized 73 flat IDs to dotted category.topic format, fixing broken detective evidence chain from Sera to Kael observations -- Entity ref format inconsistency — normalized underscore format to npc:hyphenated across ~15 monologue references -- Phase tag inconsistency between FRIEND packs — standardized to phase-N format, added missing phase-2 tags to Kael ring ops lines -- Detective monologue gap for Kael — added 7 observation lines (7 → 15 total), added mood tags and bar_evening situation to Kael bar content -- Hoshe QA briefing updated with integration test coverage priorities — test harnesses, dedicated client-server test map, edge case focus -- Sprint 5 "Live" team briefings — copy (6 tickets), client (2), CI (1), joint coordination for content-at-scale sprint targeting FRIEND packs, voice patterns, NPC style guide, PC-as-NPC authoring, UI microcopy, FactId validation -- Live server mode (`make game`) — single command builds server, launches client with TCP connection, auto-kills server on exit; `make stop` helper for manual cleanup -- `SR_LIVE=1` environment variable switches SimBridge from test mode to real TCP server connection -- TileKind in wire protocol (#412) — server sends Floor/Wall/Door/Object per visible tile, client renders walls and floors in live mode -- Input roundtrip integration test (#411) — spawns real server, connects via TCP, validates full movement and interact pipeline -- Debug logging for received player inputs on server (visible with `RUST_LOG=debug`) - -### Fixed -- Interaction prompt target+verb data now attached to Interact action in game loop (#405) — was TODO stub, server receives `{target_entity_id, verb}` payload -- Player entity detection uses `kind.variant == "Player"` instead of hardcoded `entity_id == 1` — fixes "player not found" warnings when connected to real server (which assigns different IDs) -- Movement keys now work in live mode — client was sending millisecond timestamps as input tick, server only processes ticks <= current frame counter; now uses server tick from latest snapshot -- Entity-to-tile alignment in live mode — server sends tile-center render coords (tile 16 → 16.5), entity renderer now floors to tile index before positioning - -### Added -- Content loader Phase 2 (#408) — 2-phase spawn pipeline loads real YAML content into ECS entities: enums, entity attributes, pools, templates, triangles, NPC profiles with Want/Tolerance/Contentment/Personality/Tells/Skills axes plus cross-reference resolution for Secrets, Relationships, Information, and DailyRoutine -- Global enum YAML files (#387) — 9 enum definitions (situations, topics, moods, triggers, access-tiers, trust-tiers, activities, patterns, motivations) from D-035 taxonomy -- Entity attributes YAML (#388) — 16 canonical knowledge graph attribute keys from D-024 with A7 workshop updates -- Seed-time pools (#389) — 5 single-candidate pools for deterministic v0.1 testing -- Social site templates (#390) — 3 templates (logistics-hub, bar, smuggling-ring) with role slot definitions per D-025 -- Triangle YAML files (#391) — 5 v0.1 triangles (3 active fork, 2 passive) per D-024 workshop synthesis -- Seed configuration schema design (#394) — design document defining game-start randomization: FRIEND selections, pool draws, template assignments, entanglement config, ChaCha20 RNG protocol -- YAML to RON converter tool (#403) — build-time converter in tooling/content-converter/, runs via `make content-ron` -- Line previewer CLI (#407) — 4 subcommands (dialogue, monologue, coverage, sequence) for content authors to test line selection without running the full game -- Happiness added to WantKind enum — Harek remapped from Safety to Happiness - -### Fixed -- Dialogue-pool schema corrected to use arrays for situation/topic/mood per D-035 (were incorrectly single strings) -- NPC want.primary changed from narrative strings to WantKind enum keywords — fixes silent Want component drop at spawn time -- Schema enum constraint added to npc-profile.schema.json for want.primary validation - -### Added -- Sprint 4 "Feel" team briefings — copy (8 tickets), server (9), client (1 carry-over), CI (1), joint coordination -- Interaction prompt system (#405) — server-driven "E - Talk" prompt decoding v4 nearby_interactions with nested VerbOption structs, fade animation, extensible get_interaction_target/get_selected_verb interface for future radial verb menu -- Art direction & mood board workshop (3 rounds + closing) — 4-agent team establishes visual identity, 16 art direction principles, 9 mood board images, 10 candidate decisions (D-042–D-051) -- 3D-to-2D sprite render pipeline (`client/tooling/sprite_renderer/`) — Godot @tool scene renders textured 3D models at "the angle" (-72.5deg ortho) from 4 cardinal directions at 1024/256/64 resolutions with outline applied at working resolution -- `/render-sprite` skill — CLI wrapper for the render pipeline with headless import step -- Pipeline POC: Era 1 institutional wall + bar green wall textures generated via Nano Banana and rendered through full pipeline -- PerceptionQuery trait and ActivePerceptionMode resource — abstraction layer for D-017 perception mode swapping (NaturalVision default implementation) -- VisibilityGeometry intermediate resource decoupling FOV computation from entity filtering -- Client-side PROTOCOL_VERSION enforcement — snapshot decoder rejects version mismatches with error log -- POI verb priority test in observer pipeline — asserts both verb kind and priority values end-to-end - -### Changed -- Observer pipeline decomposed into two-stage system: compute_visibility_geometry (geometry) → compute_observer_snapshot (entity filtering + assembly) -- POI verb priority adjustment moved from simulation phase (interaction.rs) to perception phase (observer) — fixes D-010 information boundary violation -- compute_nearby_interactions no longer reads KnowledgeGraph — determines verb availability by proximity only, verb priority adjusted by observer -- compute_nearby_interactions scheduling moved from SimulationPlugin to BridgePlugin for explicit ordering with geometry and observer systems -- Client test snapshot updated to v4 format (Protocol.PROTOCOL_VERSION, tick_rate replaces paused) - -### Added -- Content validation tooling — `make validate-content` validates campaign YAML files against JSON schemas, maps files by directory context -- Entity::to_bits() roundtrip test — guards against bevy version changes silently breaking wire IDs -- TickRate switch mid-accumulation test — verifies Half→Full→Paused→Half transitions preserve accumulator state -- Tick rate scaling system (#406, D-052) — Full/Half/Paused rates with fractional accumulation, SetTickRate player action, replaces binary pause flag -- Proximity detection and interaction verbs (#404, D-060) — compute_nearby_interactions system with Manhattan distance ranges (close ≤2, mid ≤5), context-sensitive verb computation (Talk, Observe, Examine), PersonOfInterest priority flip, NearbyInteraction in ObserverSnapshot v4 -- Content directory skeleton (#385, D-057) — district-as-atomic-pack layout with Sova Transit first district, 17 NPC stubs, 3 locations, 5 triangles, dialogue/monologue pools, factions, knowledge catalogs, enum definitions -- Content schema definitions (#386) — 8 JSON Schema files (draft 2020-12) for district, location, npc-profile, dialogue-pool, monologue-pool, triangle, routine, fact-catalog validation -- PROTOCOL_VERSION constant in bridge types — versioning strategy documented (subprocess IPC, serde defaults for field evolution) -- Campaign, system, station JSON schemas for hierarchical content validation - -### Fixed -- Test suite aligned with server v4 protocol enforcement — all hand-built snapshots include version field, verb priorities 1-indexed, ExamineNpc label corrected to "Observe" -- E2E proof tests resilient to entity ordering — player found by kind instead of array position, wall-hides test checks specific NPC position instead of total count, supports 3-NPC proof room layout -- Wire entity_id now uses StableId consistently across observer, observation, interpretation, and interaction systems (was Entity::to_bits() in some paths) -- Restored system/station/district hierarchical fields in district metadata (incorrectly removed during canonical_id cleanup) -- Unregistered entities in observer/interaction now log tracing::error instead of silently falling back to Entity::to_bits() - -### Changed -- NearbyInteractionBuffer refactored from global Resource to per-entity Component on PlayerCharacter — multiplayer-ready (D-009) -- Observer module split into mod.rs (244 lines) + tests.rs (480 lines) — reduces module complexity -- Content directory restructured from flat districts/ to hierarchical campaigns/main/systems/van-maanens-star/stations/sova/districts/transit/ — path mirrors canonical IDs, glob-based discovery, multi-campaign/DLC ready -- Content manifest (content.yaml) rewritten for glob-based district discovery -- District identity fields (system, station, district) now derived from directory path — removed from district.yaml required fields -- NPC canonical_id schema accepts district-scoped IDs (npc:transit.kael-davan) for cross-district uniqueness -- ObserverSnapshot protocol bumped to v4 — adds nearby_interactions field, tick_rate replaces paused field in GameTime -- NearbyInteractionBuffer.interactions is now private with take() accessor (no per-frame clone) -- NearbyInteraction.distance changed from f32 to u32 (matches manhattan distance) -- Missing PlayerCharacter in input processing now panics instead of silent no-op -- Verb sort uses (priority, kind) tuple for deterministic ordering at equal priority -- Unregistered entity in knowledge events triggers debug_assert + error (was warn) -- District schema: canonical_id is now optional (derived from directory path at load time) -- Skills trimmed for CLAUDE.md deduplication — search-docs (-48%), ticket (-17%), review-pr (-35%) now reference CLAUDE.md for basics instead of repeating them -- Review-pr reviewer profiles extracted to `references/reviewer-profiles.md` for progressive disclosure - -### Removed -- Dead generate_snapshot function in bridge/mod.rs — superseded by compute_observer_snapshot -- content/global/regions/ directory — region data absorbed into system.yaml metadata - -### Added -- Dual Lens Authoring Guide — 7-chapter reference for writing content that works for both smuggler and detective perspectives (D-027, D-028, D-032, D-034, D-035) -- THE MIRROR pattern spec — transparency-as-contrast NPC design with Naia Tamm reference implementation and generator template -- Smuggler voice card — register parameters, 5 voice anchors, 4 anti-patterns, paired comparison examples, display constraints, authoring checklist -- Smuggler moral arc spec — 4-phase trajectory (Comfort, Doubt, Reckoning, Compromise), FactId gates, monologue trigger rules, Kael intersection mapping -- PC-as-NPC unified spec — starting knowledge/relationship graphs, tell inversion, orientation monologue, 9-step conversion checklist, v0.1 smuggler + detective briefs -- Triangle 1 Hub Power Volume Escalation fork — 3-path smuggler decision (escalate/stabilize/mediate), ~41 authored dialogue lines, NPC state change tables -- Content directory structure design doc — runtime content/ layout, canonical ID format, 8 JSON Schema specs, 3-tier validation pipeline, migration path from wiki -- Interaction verb spec — 7 v0.1 verbs (Move, Look, Monologue, Examine Object, Examine NPC, Talk, Overhear), priority resolution, server pipeline architecture -- v0.1 wow moments checklist — maps all 6 D-039 moments to content deliverables, tickets, dependencies, completion status -- Nils Davan off-stage NPC stub — ring coordinator, GHOST + HANDLER pattern, lattice message design, relationship map -- NPC pattern/motivation mapping applied to all 18 NPC wiki pages with composition reads -- Structured NPC data model (#86) — replaced stub string/f32 fields with typed enums and integer types for D-010 determinism (WantKind, SecretSeverity, Skill, PersonalityTrait, CombatStyle, RelationshipKind) -- Global RelationshipGraph resource (#87) — BTreeMap with tuple key for efficient prefix queries and reverse lookups -- A* pathfinding system (#237) — PathRequest/ComputedPath/PathBlocked components with cardinal-neighbor A* and manhattan heuristic -- NPC path following system (#238) — MovementSpeed throttling, per-tick path advancement with MoveIntent creation -- Daily routine system (#88) — NpcPlugin with PreviousDayPhase resource and check_phase_transition system issuing PathRequests at day-phase boundaries -- Multiple NPC spawning (#84) — 3 distinct NPCs (dock worker, field tech, guard) with full component bundles and RelationshipGraph edges -- Observation event generator (#239) — RoutineDeviation, Absence, and NewEntity triggers from comparing visible snapshot against NPC routines and knowledge state -- Review-pr skill routes reviewers by branch type — server/client get Hoshe+Tyre, copy gets Hoshe+Paula+Miri, visual gets Hoshe+Araminta, audio gets Hoshe+Ozzie -- Start-sprint skill spawns team agents from sprint briefings — parses the Agents line, creates a team with tasks from tickets, and launches all listed agents as background teammates - -### Fixed -- IPC error handling (#341) — DeserializationWithDump/MutexPoisoned error variants, hex dump logging on deserialization failure, graceful error classification in bridge I/O systems -- NpcPlugin system ordering — routine phase transitions now run before pathfinding so PathRequests are picked up same frame -- Stale doc comment in observation event generator — system runs before knowledge updates, not after -- Clippy warnings from Rust 1.93 — derive Default, is_multiple_of, collapsible if - -### Changed -- Hael renamed to Naia Tamm across all wiki files (16 files updated) -- Canonical full names applied to 11 single-name NPCs (Voss→Arvo Voss, Devra→Devra Talsen, etc.) -- Location shortcodes standardized in monologue guide (hub_m_ → terminal_m_ per D-036) -- Entity attributes updated to 16 canonical keys — 4 new role-perspective keys (risk_assessment, loyalty_assessment, position_integrity, moral_weight), secret_held→leverage_held rename -- Drin Rosta expanded from Tier 3 to Tier 2 — full 10-axis profile, 6 voice lines, Triangle 2 + Triangle 5 roles -- NPC index roster table expanded with Pattern and Motivation columns -- Sprint 3 "Know" team briefings regenerated from database — all four files (server, client, joint, copy) now match actual sprint 3 ticket assignments -- Ticketing database moved to shared worktree location (`../settledreach.db`) — eliminates binary merge conflicts across branches -- All Python connectors use script-relative path resolution instead of `$REPO_ROOT` env var or git rev-parse -- `$REPO_ROOT` environment variable removed — all scripts, skills, and docs use relative paths -- Merged copy branch — 39 tickets, wiki review + content scoping workshops, game world glossary -- Merged maintenance branch — worktree-update skill - -### Added -- `make db-backup` / `make db-install` — database backup to `docs/backups/` (main-only) and restore for new clones -- Worktree-update skill backs up the shared database after merges on main - -### Removed -- `db/commonwealth.db` from git tracking (replaced by shared `../settledreach.db`) -- `$REPO_ROOT` env var from all 9 worktree `settings.local.json` files - -### Added -- Worktree-update skill — non-destructive branch sync with PR detection and conflict safety -- Game world wiki — 45-file glossary covering Sova Transit District: 17 NPCs, 5 triangles, 3 social sites, 7 factions, knowledge vocabulary -- Wiki Review workshop (4 rounds + lead interview) — 300-world generator model, cultural ingredients menu, three-system NPC architecture, Sacred/Profane/Middle Kingdom framework -- v0.1 Content Scoping workshop (2 rounds + closing) — 16 EntityKnowledge keys, mechanical NPC mapping, YAML content format, 7-verb interaction model, server-authoritative pause, 20 decisions (D-042–D-061) -- 39 implementation tickets (#371–#409) from content scoping workshop — copy 21, server 13, client 2, ci 1 -- Control & Interaction workshop brief (queued) -- Large content push team pattern in CLAUDE.md -- Inigo sound designer agent — soundscape design, ambient layers, diegetic cues, D-018 audio propagation -- Team-agent mapping in sprint planning — each team has defined default agents for briefing assignment -- Sprint skills recognize all team branches (server, client, copy, audio, visual, ci) -- Sprint 3 "Know" team briefings — server (6 tickets), client (2 tickets), joint (2 split tickets), copy (1 carry-over) -- Copy team sprint briefing for Sprint 2 (#368 knowledge vocabulary) -- Sprint 2 proof: fog of perception E2E tests (#357) — 3 tests verifying all 7 acceptance criteria through real server pipeline (movement, tiles, fog, wall hiding, corner reveal) -- Server proof room — wall at (16,14) between player at (16,16) and NPC at (16,13) for LOS testing -- Dynamic test snapshot in SimBridge — tracks player position from queued inputs, Bresenham LOS, Manhattan-distance visibility for standalone demo mode -- 4 Bresenham LOS unit tests — clear path, wall blocked, diagonal, same position (PR #13 review) - -### Fixed -- Type safety in GameState visible_tiles loop — validates Dictionary with x/y keys before access (PR #11 review) -- Consistent reset_test_state() usage across all test files (PR #13 review) -- E2E connection loop now detects server process death early (PR #13 review) -- Corner reveal test verifies NPC position at (16.5, 13.5) (PR #13 review) -- Entity renderer skips redundant modulate.a writes when alpha unchanged (PR #11 review) - -### Added -- Observer snapshot knowledge integration (#366) — VisibleEntity carries relationship state (D-033 color) and observation type (Visible/Remembered), remembered entities appear as fog ghosts at last known position -- Knowledge graph system (#361, #362, #363, #365) — per-entity KnowledgeGraph component (D-041), StableEntityId + EntityRegistry, KnowledgeEventQueue, decay system, 4-level confidence hierarchy -- Direct observation knowledge flow (#364) — perception emits DirectObservation/LeftLOS events to knowledge graph, entities entering/leaving LOS tracked -- Protocol v2 decoder — extracts game_time, player_facing, visible_tiles, and per-entity visibility sectors from ObserverSnapshot v2 -- D-033 entity color palette (#130) — relationship-based colors (teal/green/amber/red), Phase 1 defaults by entity kind -- Peripheral vision dimming — entities in peripheral vision rendered at 50% alpha (D-015) -- Player facing direction indicator — Polygon2D triangle on player entity showing 8-directional facing -- GameState v2 fields — game_time, player_facing, visibility_sectors stored from snapshot data -- Test snapshot updated to v2 format with visibility sectors, game_time, and player_facing -- Observer visibility query (#112) — replaces unfiltered generate_snapshot with LOS-filtered compute_observer_snapshot combining shadowcasting + vision cone -- Vision cone system (#111) — forward/peripheral/blind sectors per D-015, Facing component updated on movement -- Symmetric shadowcasting (#110, #359) — Albert Ford algorithm with rational fraction slopes, benchmarked 1.2-10.5x faster than recursive, symmetry guaranteed (D-035) -- ObserverSnapshot v2 schema (#358, #25) — version field, GameTime, FacingDirection, VisibleTile, VisibilitySector types, visibility tag on entities -- D-035 decision record — symmetric shadowcasting selected over recursive (resolves Q-018) -- Tile rendering engine (#129) — programmatic TileSet with floor/wall/door/object placeholders, renders from snapshot tile data -- Fog overlay rendering (#131) — three visibility states (visible/fog-edge/hidden) via TileMapLayer overlay -- Camera lock to character (#116) — Camera2D smoothing at 2x zoom, locked to player position (D-015) -- Test room environment — 8x8 room with corridor and Manhattan-distance visibility for development without server -- `ticket team` command and `--team` filter — comma-separated team assignment for tickets (server, client, joint, content) - -### Changed -- All instruction files (CLAUDE.md, skills, agent files) now use `$REPO_ROOT` env var instead of `git rev-parse --show-toplevel` — pre-set per worktree via `.claude/settings.local.json` -- `start-sprint` skill now requires plan mode — agent must create and get approval for a concrete sprint plan before starting implementation - -### Fixed -- Entity renderer protocol field mismatches — "id"→"entity_id", "type"→"kind.variant", "position"→x/y fields now match Protocol.decode_entity() output -- Entity centering — entities (24x24) now centered within 32px tiles instead of top-left aligned -- `start-sprint` skill uses `git rev-parse --show-toplevel` for worktree-safe absolute paths — fixes "No such file or directory" errors on team branches - -### Changed -- Background clear color set to near-black for unexplored areas (was default Godot gray) -- Scene render order: Tiles → FogOverlay → Entities (fog covers tiles, entities render on top) -- FogOverlay node type changed from Node2D to TileMapLayer for tile-based fog rendering -- GameState now stores visible_tiles and visible_positions from snapshots -- Test snapshot includes player entity (kind "Player"), second NPC entity, tile data, and visibility data - -### Added -- Sprint 2 "See" briefings (server, client, joint) — fog of perception through the bridge -- `/plan-sprint` skill — automates sprint planning workflow and briefing file generation -- `ticket show` multi-ID support and `--brief` flag for compact human-readable output -- Q-018 through Q-023 — 6 open questions from architecture audit (shadowcasting, entity ID stability, collision resolution, tick overflow, pathfinding cache, debug visualization) -- 5 architecture spike workshop briefs (knowledge graph, observer pipeline, NPC AI state machines, save/load, map authoring) -- 17 tickets from architecture audit (#339-#355) — 7 Sprint 1 tasks, 5 Sprint 2+ tasks, 5 workshop epics -- End-to-end connection test (#81) — GDScript test spawning Rust server, connecting via LocalBridge, sending MoveNorth input, verifying player movement in snapshot response (D-030 Layer 3) -- Batch input encoding (Vec\ wire format) — Protocol.encode_player_inputs() batches all inputs per tick into one framed message matching server expectations -- EntityKind::Player fixture — snapshot_player.msgpack for cross-language testing, multi-entity fixture updated to include all 4 entity kinds -- 7 new tests (4 batch encoding, 1 framed batch roundtrip, 1 Player fixture decode, 1 E2E connection), 43 total client tests passing -- LocalBridge GDScript TCP transport (#79) — 4-byte big-endian length-prefix framing matching Rust server, StreamPeerTCP wrapper with partial read handling -- ServerProcess subprocess manager — spawns/stops Rust server via OS.create_process(), auto-cleanup on destruction -- SimBridge live transport integration — _process() polling loop for TCP receive/send, connection state machine (DISCONNECTED → CONNECTING → CONNECTED → ERROR) -- 8-directional input support — 4 diagonal movement variants (NE, SE, SW, NW) in InputMapper, SimBridge wire mapping, and project.godot input actions -- 12 LocalBridge tests (framing roundtrips, cross-layer Protocol+framing, diagonal wire mapping) -- 4 diagonal movement cross-language fixtures (Rust → GDScript, D-030 Layer 1) -- 33 total client tests passing (up from 20) -- TcpBridge transport for Godot client connection — TCP localhost IPC alongside existing Unix socket LocalBridge -- Input processing system (process_player_input) — drains InputQueue, converts PlayerActions to MoveIntent components, handles pause/unpause -- Snapshot generation system (generate_snapshot) — builds ObserverSnapshot from ECS state with render coordinate conversion -- Bridge I/O systems (receive_bridge_inputs, send_bridge_snapshot) — wire bridge to ECS pipeline with graceful disconnect detection -- Full game loop in main.rs — TCP accept, tick loop with ServerRunning resource, CLI/env addr config -- PlayerCharacter marker component, Player EntityKind variant, SnapshotBuffer resource -- E2E game_loop integration test verifying player movement through full pipeline -- 8 new tests (3 TCP bridge + 4 input processing + 1 E2E game loop), total 53 -- Architecture audit framework (docs/audits/) — adversarial two-round review pattern by Tyre + Troblum -- Sprint 1 architecture review — full decision + code audit, GREEN architecture, AMBER implementation plan -- v0.1 content gap analysis workshop — 6 agents, 2 rounds, 9 content layers, 8 new decisions (D-032 through D-039) -- D-032: Separate monologue pools per playable character (hard partition, not filter) -- D-033: Entity color represents relationship to player character (asymmetric per character) -- D-034: THE FRIEND NPC pattern — production-level emotional centerpiece per character (Kael Davan, Sera Venn) -- D-035: Converged tag taxonomy for dialogue/monologue line pools (6 structural + 3 selection tags) -- D-036: Sova Transit District / Van Maanen's Star as v0.1 setting (first named star system) -- D-037: Contraband specification — unlicensed lattice components (moral ambiguity by design) -- D-038: Audio in v0.1 scope — 8 AI-generated files via Stable Audio Open -- D-039: All 6 wow moments promoted to v0.1 must-have scope -- LocalBridge IPC over Unix domain sockets (#78) — length-prefixed MessagePack framing, SimBridge implementation, BridgeResource ECS wrapper -- Tile collision system (#236) — TilePosition component, chunk-based WalkabilityMap (D-012), MoveIntent + validate_movement system -- TilePosition ↔ f32 render coordinate conversion (to_render_coords, from_render_coords) bridging simulation and wire format -- Chunk load/unload support in WalkabilityMap — HashMap with 32x32 tile chunks -- 8-directional movement — diagonal PlayerAction variants (MoveNortheast/Northwest/Southeast/Southwest) + TilePosition::all_neighbors() -- Entity-entity collision in validate_movement — spatial occupancy check prevents multiple entities on same tile -- 25 new tests (4 framing + 2 IPC integration + 17 movement unit + 2 movement integration), total 45 -- MessagePack serialization for GDScript (ticket #77) — Protocol codec decoding ObserverSnapshot/PlayerInput from Rust wire format, encoding PlayerInput for server -- Godot4MessagePack library (pure GDScript) for MessagePack encode/decode -- Rust fixture generator (gen_fixtures.rs) producing canonical .msgpack test fixtures with rmp_serde -- 8 cross-language protocol tests verifying Rust↔GDScript MessagePack compatibility (D-030 Layer 1) -- SimBridge wired to Protocol codec with receive_bytes()/drain_outbound() for transport layer -- `db/connectors/ticket` CLI — ergonomic ticket management (list, show, create, assign, sprint, deps, search, epics, children, count) with JSON output -- Sprint 1 "Run" created with 8 stories targeting moving character on screen via IPC bridge -- Ticket skill rewritten to use ticket CLI instead of raw SQL wrapper scripts -- Godot 4 client boilerplate (epic #277) — scenes, autoloads (SimBridge, GameState, InputMapper), rendering stubs, UI shell (HUD, minimap, monologue display), input system with semantic actions -- gdUnit4 test framework with 7 tests (2 smoke + 5 D-030 Layer 1 fixture tests for snapshot parsing) -- make ci-client pipeline (lint, build, test via gdUnit4 headless runner) -- Camera tracking locked to player position (D-015), monologue display wired to snapshot data (D-016) -- Gitea tea CLI instructions in CLAUDE.md — non-interactive flags, PR workflow patterns -- `/review-pr` skill — dual-agent PR review with Hoshe (code quality) and Tyre (architecture) in parallel, Gitea integration, vendor file exclusion patterns, local merge workflow, heredoc workaround -- Automated Rust install via `make setup` (tooling/install-rust script, rustup + clippy + rustfmt) -- Automated Godot download/install via `make setup` (tooling/install-godot script, installs to ~/bin/godot4) -- InputQueue tick ordering enforcement via debug_assert (determinism guard) -- Serialization roundtrip tests for all PlayerAction and EntityKind variants (D-030 Layer 1) -- Edge case tests: day wraparound at midnight, day() calculation, out-of-order input rejection -- Test runner switched to cargo-nextest (D-030 requirement) -- Rust/bevy_ecs simulation server boilerplate (epic 276) — Cargo project, module structure, core ECS types, plugin scaffolding, deterministic simulation resources, test infrastructure -- SimulationTime resource with D-031 time system (10 ticks/game-minute, 4 day phases) -- SimRng deterministic RNG resource (ChaCha20, seeded for replay) -- InputQueue resource for timestamped semantic player actions -- ObserverSnapshot and PlayerInput IPC types with MessagePack serialization (D-020) -- SimBridge trait abstracting client-server transport -- CauseChain production component for information provenance tracking (D-030) -- SimulationTier types with LRU eviction support (D-026: Active/Background/StateSaved/Ungenerated) -- NPC 10-axis model components (D-024: 7 essential + 3 supporting + CombatCapability) -- Server test infrastructure: 11 inline unit tests + 4 integration tests (smoke + serialization round-trips) -- make ci-server pipeline verified green (clippy, fmt, build, test) -- Round 18 v0.1 gap analysis workshop — 7 agents, 2 rounds, 4 tracks (concept proof, wow factor, missing systems, testability) -- D-030: Testability architecture — 8 sub-decisions for ticket #214 (gdUnit4, hybrid Rust testing, CauseChain component, three-layer IPC testing) -- D-031: Time system — 10 ticks = 1 game-minute, 4 day phases (Morning/Afternoon/Evening/Night), diegetic clock display -- 41 new tickets from gap analysis: 3 epics (Movement & Collision, Observation & Interaction, Game State Management) + 38 stories -- 15 priority promotions including 3 tickets to critical (deterministic replay, divergent knowledge, divergent relationships) -- 21 new dependency records mapping critical path through collision → pathfinding → NPC movement → routine execution -- Workshop directory convention established with per-workshop subdirectories -- Project directory scaffold: client/, server/, tooling/, tests/, .config/, .cache/ -- Top-level Makefile with dev workflow targets (setup, build, run, test, lint, ci, clean) -- Whitelistable sqlite-init and sqlite-seed wrapper scripts completing the db/connectors/sqlite-* set -- Round 17 content architecture workshop — Full team (8 agents, 3 rounds) defining content pipeline -- D-023: Three-tier content model (authored drama modules, templated content, procedural filler) with life-sim substrate -- D-024: NPC generation model — 10 axes (7 essential + 3 supporting) with CombatCapability ECS component -- D-025: Social site / functional cluster as atomic Tier 2 template unit (4-8 NPCs, 15-40 tiles) -- D-026: Simulation tiers with timestamp-based LRU eviction (Active/Background/State-saved/Ungenerated) -- D-027: Vertical slice — smuggler + detective two-character proof-of-concept (supersedes D-006) -- D-028: Dialogue architecture — tagged line pools with four relational layers (access tiers, history, trust-gating, unprompted disclosure) -- D-029: Population entanglement ratio — 30% flat / 50% mundane triangles / 20% intrigue-entangled -- Content architecture workshop brief documenting the "life first, drama second" design philosophy -- Mellanie (Copywriter) activated from standby for content authoring phase -- Round 16 faction development — Full faction framework with lore, political analysis, mechanical grounding (session closed, awaiting team input for v0.1 selection) -- 3D reputation system: Trust × Usefulness × Exposure per faction -- Faction mechanical grounding: starting loadouts, information asymmetry, blind spots, resource loops -- Power dynamics analysis: formal vs. real power distribution across factions -- Character drama templates: whistleblower, inspector with conscience, dual-loyalty operative, reluctant conspirator -- Betrayal vectors and conspiracy potential for all six factions -- "First 30 Minutes" test demonstrating six mechanically distinct faction perspectives -- D-021: Official project title "The Settled Reach" confirmed, domain settledreach.com secured -- Round 14 worldbuilding — "The Settled Reach" original SF setting foundation with full team reactions -- Original terminology established: Settled Reach, Founder Gates, Span Gates, Interstitium, neural lattice, Meridian, imprint, re-embodiment, Perpetuals, the Unbound, Forking, Severance -- Four enhancement tiers: Baseline, Augmented, Transcendent, Elevated (plus the Threshold as endgame horizon) -- Two-tier death system: soft death (lattice intact) and hard death (lattice destroyed, imprint restore) -- Six factions: Concord Assembly, Syndics, Separatists, Guardians of Autonomy/Severance, Veil Institute, Lattice Commission -- Infrastructure-as-mystery: Builders inhabiting the Interstitium, Gyre events as leakage -- Miri's IP originality guardian role — flags concepts too close to source franchises -- Whitelistable wrapper scripts for SQLite and Qdrant connectors (sqlite-query, sqlite-exec, qdrant-search, qdrant-index, qdrant-health, qdrant-count) -- Architecture evaluation and risk assessment documents for Godot+Rust bridge approach -- Round 13 engine selection discussion — full team debate on engine paradigms -- D-020: Engine and architecture selection — Godot 4 client + Rust/bevy_ecs simulation server via subprocess/IPC -- Rejected alternatives R-004 through R-010 documented (pure Bevy, pure Godot, GDExtension, C++ GDExtension, Fyrox, custom framework, protobuf) - -### Fixed -- create-skill references to nonexistent init_skill.py and package_skill.py scripts -- SimBridge wire format: inputs now batch-encoded as Vec\ array per server protocol (was sending individual inputs per frame) -- SimBridge server args: positional address format ("127.0.0.1:9876") matching server CLI, port default corrected to 9876 -- Wire format mismatch: LocalBridge now uses rmp_serde::to_vec_named() (named maps) matching client Protocol.gd expectations -- EOF on bridge read now returns BridgeError::Transport for disconnect detection instead of empty Vec -- WalkabilityMap rewritten from flat Vec to chunk-based HashMap per D-012 architecture (Tyre review) -- LocalBridge mutex .unwrap() → .expect() for clearer panic messages (Hoshe review) -- Documented 16MB MAX_MESSAGE_SIZE rationale in framing.rs (Hoshe review) -- Client input_mapper double-check bug (redundant event.pressed + is_action_pressed) -- Bounds validation on snapshot position arrays in game_state.gd and entity_renderer.gd -- Deterministic test snapshots (replaced Time.get_ticks_msec() with incrementing counter) -- Tween overlap in monologue_display.gd (cancel active tween before creating new one) -- SubViewportContainer missing SubViewport child in minimap.tscn -- Cargo.toml edition 2024 → 2021 for broader toolchain compatibility -- Relationship.target_name: String → target_id: u64 for entity scalability (Tyre review) -- DayPhase enum now derives Serialize/Deserialize (consistent with other enums) -- Replaced all absolute paths (macOS and Linux) with project-relative paths across round-16 docs -- Removed duplicate ROUND-16-STATUS.md from project root (content already in round-16-session-notes.md) -- Added relative-path convention to Qatux agent persona for cross-system consistency - -### Removed -- dotfiles/tmux.conf (no longer needed) - -### Changed -- Q-018 (shadowcasting algorithm selection) resolved via D-035 -- All 18 agent briefings updated for decisions/ directory split and DEVOPS.md references -- Implementation agents (Dudley, Hoshe, Justine, Oscar, Si, Stig, Tyre) now include Development Workflow sections with Makefile targets -- Q-009 (time system) resolved via D-031 -- Ticket catalog expanded from 232 to 273 tickets with sprint sequencing (Run → Feel → Content → Validate) -- Ticket skill updated to use wrapper scripts exclusively (no more direct python3 calls) -- Miri's role updated from Canon Guardian to Worldbuilder & Setting Designer (original IP pivot) -- Project description updated to reflect D-020 engine decision -- Connector usage instructions now reference wrapper scripts instead of python3 directly -- Q-001 (engine selection) resolved via D-020