Files
settled-reach/decisions/architecture.md
T
jpmschweitzerandClaude Opus 4.6 8abb8e4ec1 docs(decisions): formalize D-194 through D-218 generation cascade records
25 D-records defining the full generation pipeline from heightmap to
walkable tile: WorldTier taxonomy (D-218), settlement classification
(D-196), city generation context (D-200), drainage routing (D-208),
attractor matching (D-211), district mix (D-194), and supporting
enums/types. Produced by workshop #897, formalized from ticket specs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 18:40:02 +02:00

164 KiB
Raw Blame History

Architecture Decisions

Technical foundation decisions that constrain implementation: engine, client-server, ECS, simulation, testability, performance budgets, audio architecture.


D-008: Action pillar design principles

  • Date: 2026-02-08
  • Decision: The action/combat system follows these principles:
    • Rimworld/XCOM hybrid - simple mechanics, stats-driven. Simple gets complicated fast on its own.
    • Z-levels - floors, verticality. Not 3D rendering, but 3D space. Fights across floors of a building.
    • Perception-bounded - you see/hear what your character can. Rest is abstracted/occluded. Same principle as information asymmetry applied to physical space.
    • LOD/occlusion - simulation reduces outside player view. Both a design principle and performance optimization.
    • Large, varied maps - locations should feel big. Achieved through procedural generation (templates + procedural flesh), trickery, or both. Procedural layouts also feed replayability.
    • Multiple maps - different locations are separate maps. Step through a wormhole, load a new place.
    • Wildly asymmetric encounters - balanced fights are the exception. Power mismatches in both directions are the norm and the source of tension.
    • Hubris wall as design principle - let the player feel powerful, then recontextualize their power level. Not cheap difficulty spikes - genuine "you were playing a smaller game than you thought" moments. The books are not always balanced; the fun comes when a character hits their hubris wall.
    • Death = information loss, not game over - memory cell backup system means death costs you everything since your last backup. Lost knowledge, lost relationships, lost deals. The storyteller knows what you've lost and can exploit it.
    • Scales with ascension - baseline human fights with weapons, Higher fights with biononics, ANA-connected fights with something else entirely. The pillar transforms as the character evolves.
  • Design references: Rimworld (real-time, simple systems, personal stakes), XCOM (tactical asymmetry, pod activation moments), Dwarf Fortress (z-levels, simulation depth)
  • Raised by: Team Leader (Jeroen), with input from full team.

D-009: Multiplayer - design for it, build single-player first

  • Date: 2026-02-08
  • Decision: Single-player is the build target. Multiplayer is designed into the architecture from day one so it can be added without rewriting the game.
  • Rationale: Two players experiencing the same conspiracy from different keyholes (Senate insider + Guardian operative) is a killer feature. But building multiplayer too early kills projects. The compromise: architectural decisions now that make multiplayer a "add networking" problem later, not a "rewrite everything" problem. Team Leader flagged that bolting multiplayer on after the fact is one of the hardest things to do - so the architecture must be honest about this from the start.
  • Cost: ~15-20% slower single-player development due to required abstractions. Accepted as cheap insurance.
  • Raised by: Full team discussion. Tyre led technical framing, Team Leader insisted on architectural honesty.

D-010: Multiplayer-ready architectural baseline

  • Date: 2026-02-08
  • Decision: Four non-negotiable architectural principles that must be present from the first line of code:
    1. Client-server separation - even in single-player. The game simulation runs as a "server," the player view is a "client." Single-player = local client + local server. This is the single decision that makes or breaks retrofitting multiplayer.
    2. Information boundaries as a first-class system - every piece of game state is tagged with who knows it. Not fog-of-war bolted on - the engine fundamentally thinks in terms of "what does this observer have access to." Required for single-player asymmetric information anyway. Multiplayer just means multiple observers.
    3. No baking player identity into the game loop - the simulation doesn't know there's "the player." It knows there are characters, some of which are player-controlled. Adding a second player-controlled character should be a configuration change, not a rewrite.
    4. Deterministic simulation with input events - game state advances based on timestamped actions, not "whatever the local machine calculated." Enables synchronization later without rewriting the simulation.
  • Side benefits (Nigel's observation): Every one of these makes single-player better too. Information boundaries make NPC AI smarter about what they know. Client-server makes save/load cleaner. Deterministic simulation makes debugging easier. No sacrifice.
  • Engine implication: Client-server friendliness is now a hard requirement on the engine shortlist (see Q-001).
  • Raised by: Tyre (Technical Architect), endorsed by Team Leader as "sound architectural baseline."

D-012: Chunk-based map architecture for future borderless generation

  • Date: 2026-02-09
  • Decision: Maps use chunk-based generation and loading from day one. Chunks load/unload around the player. A bounded map is "only generate chunks within this boundary." Removing the boundary later to enable Minecraft-style borderless generation is a configuration change, not a rewrite.
  • v0.1: Bounded ~150x150 per world, 2-3 z-levels, chunk-based internally.
  • Future: Borderless generation. The world generates as you explore. The map can never be "solved" by walking to every corner. New areas develop, existing areas change.
  • Rationale: Same principle as D-010 (multiplayer architecture) - design for the future, build the simpler version now.
  • Raised by: Tyre (Technical Architect), endorsed by Team Leader.
  • Amendment (2026-04-05): The ~150×150 visual tile estimate is superseded by D-094 (256×256 visual tiles per district, 4×4 blocks). D-094 explicitly amends this decision.

D-020: Engine and architecture selection — Godot client + Rust simulation via subprocess/IPC

  • Date: 2026-02-09
  • Decision: The game uses a split architecture: Godot 4 (GDScript) as the rendering client, Rust with bevy_ecs standalone as the simulation server. The two communicate via subprocess/IPC (local socket for single-player, TCP for multiplayer). NOT via GDExtension.
  • Architecture:
    • The Rust simulation is a standalone binary with zero Godot dependencies. It runs the ECS world, perception queries, AI, storyteller, combat — all game logic.
    • The Godot client is a pure renderer: receives ObserverSnapshot data, draws tiles/sprites/fog, plays audio, shows UI, captures input. No game logic in GDScript.
    • Single-player: Godot launches the Rust binary as a child process. Local Unix socket or localhost TCP.
    • Multiplayer: Godot connects to a remote Rust server. Same protocol. The simulation binary doesn't know the difference.
    • This IS the D-010 client-server architecture — literally, not simulated.
  • Serialization:
    • MessagePack for all client-facing communication (Rust↔Godot). Dynamic structure supports variable HUD composition driven by perception modes (D-017). Cross-language, debuggable.
    • bincode reserved for future Rust↔Rust server-to-server sync (same binary, hot path, zero overhead).
    • protobuf rejected — solves deployment/versioning problems we don't have, poor GDScript support.
  • Why subprocess over GDExtension:
    • Eliminates entire risk categories: gdext pre-1.0 API churn, Godot version ABI breakage, FFI thread safety (Gd<T> is !Send), cross-boundary memory management.
    • Decouples learning: build and test Rust simulation standalone, build Godot renderer standalone, connect when both work.
    • Maps directly to D-010 client-server with no simulation — it IS client-server from day one.
    • Either side can be upgraded, replaced, or scaled independently.
    • Cost: ~1-5ms serialization latency per tick. Acceptable for a detective/strategy game, not a twitch shooter.
  • Key patterns:
    • ObserverSnapshot: the only data structure crossing the boundary. Contains visible entities, fog state, sound events, monologue triggers, HUD widget data. Variable shape per character build.
    • PlayerInput: semantic actions (MoveNorth, Interact, UsePerceptionMode), not raw key events. Timestamped for deterministic processing.
    • SimBridge trait: abstracts transport. LocalBridge (subprocess, channels) and NetworkBridge (TCP, MessagePack) implement the same interface.
  • Protocol versioning policy: PROTOCOL_VERSION gates wire format compatibility — field names, types, message structure. Bump when deserialization would fail (added/removed/renamed fields, new enum variants, changed types). Do NOT bump for gameplay parameter changes that affect what data flows through the same format (vision cone angles, NPC behavior, map layout, balance tuning). Per-character variation (D-015 cone config, D-017 perception modes) means these parameters differ between entities on the same server simultaneously — they are game state, not protocol. The client renders whatever ObserverSnapshot the server sends; it has no awareness of cone angles or perception mode configuration.
  • Kill switch: If no working prototype (character + fog + one NPC) exists by week 8 of development, pivot to pure Godot. If bridge/sync code exceeds game logic for 3 consecutive sprints, the architecture tax is too high.
  • Development sequence:
    1. Build Rust simulation as standalone binary (testable via terminal/logs)
    2. Build Godot renderer as standalone project (hardcoded test data)
    3. Connect via MessagePack protocol
  • Evaluation reports: docs/architecture/eval-godot-rust-bridge.md (Tyre), docs/architecture/risk-godot-rust-bridge.md (Troblum)
  • Raised by: Team Leader (Jeroen) proposed Godot client + Rust backend. Tyre designed architecture. Troblum's risk assessment shifted integration from GDExtension to subprocess/IPC. Full team endorsed.
  • Dissent: None. Troblum's CRITICAL risk flags on GDExtension were accepted; subprocess approach addresses them.

D-026: Simulation tiers with timestamp-based eviction

  • Date: 2026-02-10
  • Decision: Four simulation tiers: Active (30-80 NPCs, full sim at 10-20 ticks/sec), Background (500-2,000 NPCs, state machine ticks 1/game-minute with 4 machines: schedule, mood, relationships, job), State-saved (10,000+, frozen serialized structs ~1-2KB each), Ungenerated (doesn't exist yet). Eviction uses interaction-timestamp LRU against available sim-space. NPCs with active scope tags (neighborhood, active-quest, colleague, known-contact) stay fully simulated. State-saved NPCs reactivate on player return (~2-5ms). Density follows the player — content generated ahead of arrival, home system fully instantiated at game start.
  • Rationale: Timestamp-based eviction replaces categorical persistence rules with one priority queue. State-save makes disposal reversible. bevy_ecs dynamic component add/remove makes tier transitions seamless. Tyre confirmed all proposals fit within performance budgets.
  • Cross-reference: Content density implications in D-029.
  • Raised by: Team Leader (timestamp model), Tyre (technical validation), Gestalt (scope tags)
  • Dissent: None

D-030: Testability architecture — 8 decisions for ticket #214

  • Date: 2026-02-11
  • Decision: The v0.1 testability architecture is confirmed with 8 sub-decisions from the Gap Analysis Workshop (Round 18):
    1. Rust test organization = Hybrid. #[cfg(test)] for unit tests inside modules + tests/ directory for integration tests. Both via cargo nextest run.
    2. Godot test framework = gdUnit4 (changed from GUT). Native JSON output, stable headless via GdUnitCmdTool, GdUnitSceneRunner for scene lifecycle tests, organizational maintenance.
    3. IPC testing = Three-layer architecture. Layer 1: fixture-based serialization roundtrip (fast, every edit). Layer 2: mock subprocess protocol state machine (medium, every PR). Layer 3: real subprocess integration (slow, daily/pre-merge).
    4. Production code constraints + CauseChain. No #[cfg(test)] in production. Public API is the test surface. ECS World setup replaces mock injection. CauseChain is a production component (monologue provenance, journal, debugging) that tests also leverage.
    5. Test runner tooling. cargo-nextest (Rust) + gdUnit4 (Godot) + bash wrapper scripts in test/ directory, whitelistable for agent use.
    6. Test output format = JSON summary. Consistent schema across all runners (suite, total, passed, failed, failures array). JUnit XML as secondary CI format.
    7. #201 (Deterministic replay) promoted to CRITICAL. Simulation must consume time, randomness, and input exclusively through injectable resources (SimulationTime, SimRng, InputQueue). Required by D-010 principle 4.
    8. Test priority aligned with hard blockers. Phase 1 (sprint 1-2): test infra + collision/pathfinding/time. Phase 2 (sprint 3-4): monologue pipeline integration test + information boundary negative tests. Phase 3 (sprint 5+): CauseChain verification + divergent snapshots.
  • Rationale: Two rounds of analysis by Tyre (Technical Architect) and Hoshe (QA Engineer) with cross-validation from all design agents. Key change: gdUnit4 over GUT driven by agent-driven development requirements (JSON output, headless stability, bus factor). CauseChain endorsed unanimously after all design agents independently identified the need for information provenance tracking.
  • Raised by: Tyre (architecture), Hoshe (testability analysis). Full workshop endorsed.
  • Dissent: GUT vs gdUnit4 resolved in Hoshe's favor — Tyre explicitly changed position. No remaining dissent.

D-031: Time system — game clock and day phases

  • Date: 2026-02-11
  • Decision: The v0.1 time system uses the following model:
    • Tick-to-time mapping: 10 simulation ticks = 1 game-minute (at 10 tps, 1 real second = 1 game-minute). A 30-minute real-time play session covers ~12-18 game-hours — enough for a full NPC daily cycle.
    • Day phases: Four phases drive routine transitions: Morning, Afternoon, Evening, Night. NPCs transition between routine activities at phase boundaries (e.g., go to work in Morning, to the bar in Evening).
    • Time display: Diegetic — shown on the player's neural insert HUD. The character checks their insert to see the time, consistent with D-013.
    • Pause: Available in single-player. Simulation freezes, UI stays responsive. Compatible with future multiplayer (D-009) where pause would be disabled or vote-based.
    • Time-skip: Deferred for v0.1. The "wait/stake out" mechanic (if implemented) would advance time while the player observes from a fixed position.
  • Not in scope for v0.1: Deep time (years/decades), day/night lighting, seasonal cycles, time zones between locations.
  • Resolves: Q-009
  • Raised by: Tyre (technical proposal), Gestalt (day-phase design). Confirmed in Round 18 Gap Analysis Workshop with full team consensus.
  • Dissent: None.

D-041: Knowledge Graph Data Model

  • Date: 2026-02-11
  • Decision: The knowledge graph is a per-entity bevy_ecs Component with BTreeMap storage for deterministic iteration. Each entity that has knowledge (player character, Active-tier NPCs, Background-tier NPCs) gets a KnowledgeGraph component containing: (1) entity knowledge map: BTreeMap<StableId, EntityKnowledge>, (2) fact knowledge map: BTreeMap<FactId, FactKnowledge>. Knowledge confidence uses a 4-level hierarchy: Suspects < KnowsOf < KnowsDetails < Direct. Knowledge state tracks temporal/logical status: Active (believed true), Contradicted (conflicting information exists), Stale (aged beyond threshold). Knowledge source provides provenance per entry: DirectObservation, Heard, ToldBy, Inferred, Background. Stable entity IDs (StableId(u64)) replace bevy_ecs Entity handles in knowledge references, mapped via EntityRegistry resource for bidirectional StableId <-> Entity lookup. Knowledge updates flow through event-driven architecture: perception systems emit KnowledgeEvent to KnowledgeEventQueue resource, knowledge update system drains queue and writes to KnowledgeGraph components. Basic decay runs once per game-minute (every 10 ticks per D-031), downgrading confidence levels based on last_observed_tick age against configurable DecayThresholds.
  • Sprint 2 scope: Full data structures + direct observation flow + basic decay + observer snapshot integration (#112). Deferred to Sprint 3+: NPC-to-NPC gossip, ToldBy/Inferred source generation, Contradicted state detection, Stale state logic, knowledge-driven dialogue filtering, monologue triggering, misinformation.
  • Canonical reference: Full Rust struct definitions at docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md Part 3 (lines 320-752). All implementation must conform to those types.
  • Key design choices:
    • BTreeMap over HashMap: D-010 principle 4 (deterministic simulation) requires stable iteration order. BTreeMap provides O(log N) lookups (~6-8 comparisons at N=50-200), deterministic serialization, and predictable replay behavior. HashMap iteration is non-deterministic and incompatible with deterministic replay (D-030 sub-decision #7).
    • Per-entity Component, not centralized Resource: Enables Changed<KnowledgeGraph> dirty tracking, per-entity serialization for D-026 tier transitions, no shared mutable state, natural ECS query patterns.
    • 4-level confidence hierarchy: Resolves Q-016. Suspects = "something's off", gates initial investigation. KnowsOf = "X is involved in Y", gates topic-specific dialogue and peer-tier access (D-028). KnowsDetails = actionable detail, gates confrontation and secret-tier dialogue. Direct = currently in LOS, provides live position data and maximum rendering fidelity. Maps to D-028 access tiers: surface available at any level, real at KnowsOf+, secret at KnowsDetails+.
    • KnowledgeState for contradiction detection: THE FRIEND arc (D-034, D-039 wow moment #3) requires detecting when a ToldBy entry conflicts with a DirectObservation entry. Both entries receive Contradicted state, triggering monologue event and relationship state shift (PersonOfInterest). Sprint 2 only uses Active state; contradiction detection ships Sprint 3.
    • StableId for knowledge references: Partially resolves Q-019 for server-side and knowledge graph purposes. Knowledge graphs reference StableId(u64) that persists across save/load cycles, not bevy_ecs Entity (generational index). EntityRegistry maintains bidirectional mapping. Assigned once at entity creation, never changes. Client-side mapping (Godot StableId -> scene node) remains open.
    • Event-driven updates: Phase 2 (perception) emits events. Phase 3 (knowledge) consumes events and writes graphs. Phase 4 (snapshot) reads graphs. Prevents mutable borrow conflicts in bevy_ecs.
  • Performance budget: ~14 KB per NPC knowledge graph (50 entities + 20 facts). Active tier (80 NPCs) = ~1.1 MB. Background tier (2,000 NPCs, 10 entries each) = ~5 MB. Total live memory: ~6 MB. Knowledge lookups are O(log N) at N=50 (~100ns per query). Not on critical path (shadowcasting/spatial queries consume 10-20ms per tick, knowledge operations <3ms).
  • Resolves: Q-016 (knowledge hierarchy). Partially resolves Q-019 (entity ID stability, server-side).
  • Blocks: #352 (Observer Snapshot Pipeline Workshop)
  • Raised by: Tyre (architecture synthesis), Dudley (implementation analysis), Gestalt (mechanics validation), Paula (narrative requirements). Workshop participants: Tyre, Dudley, Gestalt, Paula, SI. Source: Knowledge Graph & Information Boundaries Workshop (Epic #351), 2026-02-11.
  • Dissent: None. Gestalt initially proposed dual-axis model (confidence + understanding) but validated that single-axis confidence with future content-driven understanding progression is architecturally sufficient. Dudley proposed HashMap; synthesis chose BTreeMap per D-010 principle 4 with Dudley's explicit acknowledgment ("iteration order is a determinism time bomb").

D-042: UI microcopy format — YAML via GDScript autoload

  • Date: 2026-02-13
  • Decision: UI strings (interaction prompt labels, knowledge panel labels, relationship state descriptors, HUD labels, tutorial text) are stored in YAML format at client/data/ui-strings.yaml and loaded via a dedicated GDScript autoload singleton (UIStrings). UI strings are NOT hardcoded as GDScript constants in client/scripts/constants/ui_strings.gd.
  • Rationale: YAML format enables editing UI text without rebuilding the client and supports future localization infrastructure (all player-facing text in one format). UI microcopy is client-side rendering data per D-020 (Godot is the renderer) — distinct from server-side game content (dialogue/monologue lines). UI labels are presentation metadata that never cross the protocol boundary, so they live in the client repository and load via a client-side autoload rather than the content loader system. Hardcoded constants would require client recompilation for copy edits.
  • Related ticket: #409 (UI microcopy)
  • Raised by: Team decision in Sprint 5 planning
  • Dissent: None

D-054: Tile-based movement with same-tile occupancy

  • Date: 2026-02-13
  • Decision: All movement is tile-based (server-authoritative, discrete positions). Client-side Tween interpolation (100-150ms) hides the grid visually. Same-tile occupancy via TilePresence component (Standing/Prone/Seated/Fixture layers) allows multiple entities on one tile in different postures. Mouse facing is a client-side float; the server receives the facing octant only. Tile occupancy provides trivial collision detection.
  • Rationale: Determinism (D-010 principle 4). Tile-based enables shadowcasting (D-035), pathfinding, chunk-based maps (D-012), and trivial collision. Occupancy system adds positioning depth (doorway blocking, eavesdrop positioning, sitting at furniture) within tile-based constraints. ~150 lines server-side.
  • Implementation: TilePresence enum: Standing, Prone, Seated, Fixture. Multiple entities can share a tile if they occupy different posture layers.
  • Cross-reference: Stance system (D-053), shadowcasting (D-035)
  • Source: Control & Interaction Workshop (2026-02-13)
  • Raised by: Tyre (tile-based, non-negotiable), Dudley (tiles-per-tick model), Nigel (converted in Round 2: "tiles are BETTER for replayability — discrete positions = finite meaningful choices")
  • Dissent: Nigel initially proposed free movement with tile-based collision (Round 1). Converted in Round 2 after demonstrating that tile-based spatial puzzles (doorway decisions, corner peeks, eavesdrop corridors) create replayability.

D-055: Sprint explicitly suppresses interaction buffer

  • Date: 2026-02-13
  • Decision: When in Sprint stance (D-053), the server explicitly clears the interaction buffer. No interaction verbs are computed or sent to the client during sprint. Anomaly monologue survives sprint — the "sprint double-take" (if the character passes something anomalous while sprinting, a delayed monologue fires retroactively: "Wait — was that Kael? At this hour?").
  • Rationale: Mouse gymnastics to click during sprint = bad UX. Explicit suppression is cleaner and deterministic. The sprint double-take preserves the feel that the character is still aware even when the player can't interact — sprint suppresses interpretation (monologue at 40%), not sensory data (overlays still render).
  • Cross-reference: Stance system (D-053), monologue (D-016)
  • Source: Control & Interaction Workshop (2026-02-13)
  • Raised by: Dudley (explicit suppression), Ozzie (anomaly survival / double-take), Gestalt (interpretation vs data framing)
  • Dissent: Gestalt argued physics handles it naturally (player passes through interaction radius too fast to click). Lead ruled explicit suppression for clarity and determinism.

D-066: Dual-scale grid — 0.5m simulation, 1m visual (2x retina factor)

  • Date: 2026-02-14
  • Decision: The game uses two coordinate scales with a fixed 2x retina factor:
    • Simulation grid: 0.5m tiles. All movement, LOS/shadowcasting (D-035), pathfinding, occupancy (D-054), and interaction range operate at 0.5m per sim tile. The server knows only sim tiles.
    • Visual grid: 1m tiles. The Godot client renders floor art, wall art, and structural tiles as 2x2 blocks of sim tiles. Art is authored at 1m conceptual scale.
    • World geometry: 2x2 sim tile minimum. All walls, furniture, crates, doors, and environmental objects occupy a minimum of 2x2 sim tiles (= 1 visual tile). This ensures visual truth and sim truth agree on where solid things are — cover, LOS occlusion, and collision map 1:1 with what the player sees.
    • Entities: 1x1 sim tiles. Characters and small items occupy individual 0.5m sim tiles, giving sub-visual-tile positioning precision. Entities naturally take corners/edges within a visual tile's 1m space.
    • Sprites: 2x2 sim tile footprint. Entity sprites render across 2x2 sim tiles so they feel proportional to the 1m visual grid. Tween interpolation (D-054) hides half-visual-tile movement increments.
  • Mental model: "Objects are where they look. I can position myself precisely within open space." The player reads cover and walls at visual scale (always correct). Fine movement granularity is felt, not counted.
  • What the simulation does NOT know: Visual tiles. The retina factor is purely a client rendering convention. The server operates exclusively on 0.5m sim tiles.
  • Fog shader (D-059): Unaffected — fog is screen-space, driven by PointLight2D vision cone and LOS mask from sim-resolution shadowcasting. Gradient edge "3-4 tiles" is retuned to 6-8 sim tiles (= 3-4 visual tiles) to preserve the intended softness.
  • Cursor/interaction: No change — cursor already resolves to sim tile from pixel position. Interaction range of ~2 sim tiles = 1m (arm's length).
  • Map authoring: Author at 1m visual scale. Subdivision tool expands each visual tile to 4 sim tiles (2x2). Validation enforces 2x2 minimum on all world geometry layers.
  • Amends: OQ-01 resolution (ticket #444). Sim tile size remains 0.5m; visual presentation changes from 1:1 to 2:1 retina factor.
  • Cross-reference: Tile-based movement (D-054), shadowcasting (D-035), fog (D-059), art direction (D-043), z-stack (D-049), stance system (D-053)
  • Rationale: 0.5m sim tiles give stealth-grade granularity for movement stances, cover peeking, and interaction range. 1m visual tiles make spaces feel proportional, sprites look right, and world geometry readable. The 2x2 minimum on geometry eliminates visual/sim mismatch for cover and LOS — the only sub-visual-tile positioning is entity movement, which is communicated through fog feedback, not tile counting. Analogous to macOS Retina: logical resolution (visual) differs from physical resolution (sim), but the system is coherent because both agree on where solid objects are.
  • Raised by: Team Leader (Jeroen) — proposed retina scaling analogy and 2x2 geometry constraint. Tyre (feasibility: trivial, half-day integration). Gestalt (approved with 2x2 constraint resolving LOS readability concern). Ozzie (approved: solves sprite scale without uncanny mismatch).
  • Dissent: Gestalt initially objected to dual-scale (mental model mismatch for cover/LOS). Resolved by the 2x2 geometry minimum constraint — all cover maps 1:1 at visual scale.

D-068: 5-bus audio architecture

  • Date: 2026-02-16
  • Decision: Audio uses a 5-bus architecture with player-facing volume sliders:
    1. Music — future/empty in v0.1. Reserved for diegetic Meridian music in social spaces.
    2. Ambient — station hum (D-038 asset 1) + zone overlays (assets 2-4). Continuous soundscape.
    3. World SFX — NPC footsteps, doors, environmental events. Diegetic world sounds not caused by player.
    4. Player Actions — player footsteps (assets 5-6), future: combat sounds, item interactions. Sounds player directly causes.
    5. UI Sounds — cursor hover, implant open, monologue chimes (assets 7-8), fog recognition. Interface feedback.
  • Client implementation:
    • AudioManager GDScript autoload singleton on client branch (lives with rendering, per D-020).
    • Audio assets committed to audio branch (content, not code).
    • Directory-scan registry pattern: AudioManager scans res://audio/ on startup, maps filenames to AudioStream resources. No hardcoded asset list.
    • If directory empty or asset missing, all play methods no-op with visual fallback (per D-038 architecture).
    • 5 player-facing volume sliders, one per bus. Accessible via settings.
  • Bus routing dropped:
    • Dialogue bus removed — no voice acting in v0.1. NPC conversation murmur goes on World SFX (event-driven, per D-072).
  • Rationale: 5 buses provide player control granularity (disable UI sounds, boost World SFX for eavesdropping, mute Ambient for focus) without over-segmentation. Directory-scan registry eliminates hardcoded asset paths — audio branch can add files without touching client code. No-op fallback means client works identically with or without audio.
  • Cross-reference: Audio assets (D-038), audio dip (D-069), client-server architecture (D-020)
  • Raised by: Team Leader (channel split directive), Tyre (architecture), Inigo and Gestalt (bus refinement)
  • Dissent: None

D-073: Zone crossfade approach — hard boundary, soft audio transition

  • Date: 2026-02-16
  • Decision: Zone audio transitions use hard tile boundary triggers with 1.5-2s audio crossfade tweens. Server sends zone_id per tile in ObserverSnapshot (server-authoritative zone assignment). AudioManager receives zone changes and tweens between ambient layers. No blended overlap zones — the transition smoothness comes from audio fade duration, not spatial blending.
  • Implementation: AudioManager stub in Sprint 7 (5-bus setup, directory registry). Full zone crossfade implementation deferred to Sprint 8+.
  • Rationale: Hard boundaries with soft audio = predictable for simulation, pleasant for player. Avoids complex overlap zone geometry. Crossfade duration (1.5-2s) is long enough to feel smooth, short enough that walking back-and-forth across boundary doesn't create audio chaos.
  • Cross-reference: Audio architecture (D-068), client-server (D-020), ambient assets (D-038)
  • Raised by: Tyre
  • Dissent: None

D-085: Per-game save directory structure

  • Date: 2026-02-25
  • Decision: Every new game creates a dedicated directory under the user save path. All saves for that game (manual, quicksave, autosave) live inside the game's directory. Directory name includes a human-readable game identifier and creation timestamp.
  • Rationale: Natively groups saves by game without requiring a database or index file. Players can browse, back up, or delete game saves at the filesystem level. Avoids a flat save folder where 50+ files from different games are interleaved.
  • Structure: user://saves/<game-id>/ where <game-id> is <timestamp>-<seed> (e.g., 20260225-143022-a7b3f1/). Inside: quicksave.sav, autosave.sav, manual_001.sav, etc.
  • Constraints:
    • Game directory created on "New Game" — even before the first save, so the path exists for quicksave/autosave.
    • F5 = quicksave (overwrites quicksave.sav in the active game dir).
    • F6 = quickload (loads quicksave.sav from the active game dir).
    • Loading screen lists game directories sorted by last-modified, shows most recent save per game.
  • Raised by: Team Leader (Jeroen)
  • Dissent: None

D-088: 3-state pause system — Normal/Overlay/Paused, server-authoritative

  • Date: 2026-02-12
  • Decision: Simulation runs at three speed states: Normal (100% tick rate), Overlay (50% — active during knowledge panel, dialogue, map view), Paused (0% — full pause via Esc). Server is authoritative: client sends pause requests, server sets sim_speed field in ObserverSnapshot. Client reads sim_speed and adjusts presentation. No client-side tick manipulation.
  • Rationale: Server-authoritative speed states preserve D-010 principle 4 (deterministic simulation). Client cannot modify simulation state directly. Overlay mode at 50% ensures UI interactions do not require a hard pause while still giving the player time to read and decide.
  • Raised by: Tyre, Dudley
  • Dissent: None
  • Source: v0.1 Content Scoping Workshop, closing round resolution
  • Cross-reference: D-031 (time system), D-020 (client-server architecture)

D-094: District Spatial Hierarchy — Chunk, Block, District Naming and Sizes

  • Date: 2026-02-25
  • Decision: The spatial hierarchy for map generation and streaming is defined as follows. Chunk = 64×64 sim tiles (32×32 visual tiles, 32m) — the streaming and serialization unit. Block = 128×128 sim tiles (64×64 visual tiles, 64m) — the generator planning unit, composed of 4 chunks arranged in a 2×2 grid. Each block contains 4 chunks; chunks within a block can merge into one large edifice, remain separate (small buildings, gardens, cafes), or form L-shaped buildings across chunk boundaries. District = 4×4 blocks = 512×512 sim tiles (256×256 visual tiles, 256m) per z-level, containing 16 blocks and 64 chunks. Large civic structures (gate terminals, horizon station installations, stadiums, parks) span multiple blocks. Three z-levels for the Transit District = ~1.35MB (trivial). This decision amends D-012 and overrides the ~150×150 visual estimate in D-014.
  • Rationale: Chunk size of 32×32 visual (64×64 sim) gives a 32m streaming cell — large enough to hold a meaningful space, small enough for efficient streaming. The 2×2-chunk block provides a generator planning unit with enough granularity for per-chunk variation. The 4×4 block district (256×256 visual) gives a full district footprint generalisable as a template for the Q-036 generator. The chunk-based fill system within blocks allows the generator to place buildings of varying scale without hard-coding building dimensions.
  • Raised by: Tyre (chunk/block spec and memory confirmation), confirmed by team. Lead ratified district = 4×4 blocks.
  • Dissent: Araminta preferred 32×32 visual chunk size (effectively halving the chunk to a 16m cell). Overruled by lead and team majority — 32m chunk is the minimum viable streaming cell for the simulation architecture.
  • Source: Station District Layout Workshop, Ticket #153, Sprint 20. Round document: docs/discussions/round-20-station-district-layout.md
  • Cross-reference: D-012 (tile spec — amended), D-014 (v0.1 map spec — district bounding box superseded), D-066 (dual-scale grid), D-093 (Sova Transit District layout using this hierarchy), Q-036 (district generator)

D-096: DistrictLayoutMode — Grid and Organic Support

  • Date: 2026-02-27
  • Decision: Two layout modes coexist for district generation. Grid: Commission-planned districts with rectilinear block placement. Organic: pioneer/growth districts with block offsets (±16 sim tiles per axis), rotation (03 steps, 15° increments), variable street width (0.752.0×). Hard technical ceiling: maximum rotation ±45°. Beyond 45°, tile-based pathfinding produces unacceptable movement artifacts. Organic districts produce curved-street impressions through angular jogs and irregular setbacks. Grid vs. Organic proportions must vary per seed to prevent predictable meta-level patterns.
  • Rationale: Grid = power imposed (Commission-planned). Organic = power negotiated (pioneer settlements, organic growth). Both modes encode political and settlement history in spatial form.
  • Source: Generator Architecture Workshop (#562), 2026-02-27. Full spec: docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-1.
  • Raised by: Tyre (technical architecture), Miri (cultural grammar). Full team sign-off.
  • Dissent: None.
  • Cross-reference: D-094 (spatial hierarchy), Q-036 (district generator)

D-097: Guarantee Tier System — Universal / Full-Only / Conditional

  • Date: 2026-02-27
  • Decision: The district generator runs a GuaranteeAuditResult with three tiers of spatial guarantees. Tier 1 — Universal (all inhabited): Social Hub, Informal Zone, Encounter Corridor. Tier 2 — Full-complexity: Traffic Chokepoint, Institutional Space, Insider Space, Economic Node, Horizon View Corridor (coastal), BreachOnly Zone (≥1), Rooftop Discovery Zone (tall structures). Tier 3 — Conditional: A-1 Elevated Vantage, A-2 Egress Multiplicity, A-3 Temporal Opacity Window, A-4 Non-Institutional Route, Economic Asymmetry Signal, Power Gradient Visibility. A Full-complexity coastal urban hub gets up to 13 checks. Archetype placement must vary in angular position (not just distance) across seeds — audit fails if archetypes cluster predictably across a test batch of N seeds.
  • Rationale: The generator makes contracts it keeps. Guaranteed affordances ensure every playstyle has spatial affordances in any district, without hand-crafting each location.
  • Source: Generator Architecture Workshop (#562), 2026-02-27. Full spec: docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-2.
  • Raised by: Gestalt (tier structure + assassin lens integration), Tyre (GuaranteeAuditResult struct). Full team sign-off.
  • Dissent: None.
  • Cross-reference: D-103 (assassin lens guarantees A-1 through A-4), D-102 (horizon view corridor — Tier 2 coastal)

D-099: WallBackside / TileBehindState — Dual Classification

  • Date: 2026-02-27
  • Decision: Two complementary enums classify tiles behind wall surfaces. WallBackside (structural): what is physically there — AdjacentSpace | StructuralFill | ServiceVoid | ChunkBoundary | Exterior. TileBehindState (gameplay): what kind of space this represents — StructuralFill | HiddenRoom | Interstitial. Mapping: ServiceVoid → Interstitial; AdjacentSpace → HiddenRoom or StructuralFill depending on access tier. Era-tagged infrastructure cavity contents with standardized color codes: Era 1 power conduit only (#c8b840), Era 2 power + water/coolant (#4888c8) + comm lines (#b8b8b8), Era 3 full bundle. Backside assignments within a template must have seed-driven variation — not fixed template values.
  • Rationale: "Every wall is a secret keeper." No tile is ever void. Dual classification separates structural truth (what's there physically) from gameplay meaning (what does this imply for the player's investigation).
  • Source: Generator Architecture Workshop (#562), 2026-02-27. docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-4.
  • Raised by: Tyre (WallBackside), Gestalt (TileBehindState). Full team sign-off.
  • Dissent: None.

D-100: Dynamic Modification via Overlay — DamageOverlay and RegenerationStrategy

  • Date: 2026-02-27
  • Decision: Generator output is immutable after Phase 1. All post-generation modifications are applied via overlay, not re-generation. DamageOverlay struct: overlay_type (GasExplosion | Fire | Structural { collapse_direction } | Flooding), epicenter: ChunkLocalPos, radius: f32, intensity: f32, scatter_seed: u64 (variation within zone only). RegenerationStrategy enum: LocalOverlay(DamageParameters) for in-playthrough events (MANDATORY), SoftReseed { seed_modifier: u64 } at scenario boundaries only, FullReseed at era-level discontinuities only. Trauma event → visual stage mapping: PhysicalDestruction/ViolenceEvent → Stage 2 (Fresh Aftermath), decays to Stage 3; EconomicDisruption/PoliticalShock/MigrationShock → quarter fill modifier. Full stage sequence: Stage 1 Active → Stage 2 Fresh Aftermath → Stage 3 Stabilized → Stage 4 Reconstruction → Stage 5 Healed Scar. Destruction palette is corruption-only: no new colors introduced by destruction. Single exception: #c8d8f0 open-sky tile appears when a roofed structure has its roof removed. See D-109 for the XOR prohibition as architectural mandate.
  • Rationale: Modification history diverges per playthrough on the same seed. Same world, different event histories, different delta layers — this is the replayability engine. Causal legibility requires the player to be able to read what happened from the world state.
  • Source: Generator Architecture Workshop (#562), 2026-02-27. docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-5.
  • Raised by: Tyre (structs), Gestalt (LocalOverlay mandate). Destruction stages and palette constraint: Araminta (Round 5).
  • Dissent: None.
  • Cross-reference: D-109 (XOR prohibition as architectural mandate), D-107 (trauma events — cultural track)

D-101: ZonePalette Modifier System

  • Date: 2026-02-27
  • Decision: Zone palettes use ZonePalette { base: BasePalette, modifiers: Vec<PaletteModifier> }. Eight canonical base terrain types: T1 temperate farmland (warm organic, natural lighting) / T2 industrial farmland (cool grey-green, artificial lighting) / T3 wilderness / T4 grassland / T5 coastal water (deep near-black blue, animated specular; referenced by D-102 horizon corridor guarantee) / T6 beach/coastal margin (warm dark tan) / T7 mountain/high terrain (dark blue-grey stone, snow at elevation) / T8 desert/arid. T1 and T2 are explicitly distinct farmland types. Additional terrain types must be specified with new numbers — not silent replacements for existing types. Modifier axes: A (heritage root → material character), B (economic tier → condition/density), C (era → material generation), plus faction overlay, climate, condition, season. Palette modifiers influence NPC appearance as well as environment (people dress like they're from here).
  • Rationale: A zone's visual identity must be legible at a glance. Palette modifiers create cultural visual identity without rewriting base terrain.
  • Source: Generator Architecture Workshop (#562), 2026-02-27. docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-6.
  • Raised by: Araminta (terrain types and color specs, canonical T5/T7 numbering corrected Round 5), Tyre (palette struct). Full team sign-off.
  • Dissent: None.
  • Cross-reference: D-102 (horizon view corridor — T5 coastal water is the referenced terrain type), D-104 (heritage grammar overlay — modifier axis A)
  • Superseded (partial): Modifier axis A (HeritageRoot) superseded by D-167 (2026-03-24). Cultural palette modifiers are now authored per-system via the corridor framework; abstract heritage root IDs no longer drive axis A.

D-102: Horizon View Corridor as Coastal Guarantee

  • Date: 2026-02-27
  • Decision: A negative-space reservation for coastal districts: ≥8 visual tiles unobstructed view corridor from nearest public street to water's edge. No building, tree, or z=4 element may occupy this corridor. A low z=2 element (railing, bench, bollard) marks the waterfront point as a designed viewing location. Tier 2 Conditional guarantee — applies to Full-complexity coastal districts. Position within the district must vary per seed; the Wow Moment of seeing the horizon must be discovered, not expected.
  • Rationale: "Negative-space reservation" framing — the generator reserves space by prohibiting placement, not by placing something. The view of the horizon is a spatially guaranteed player experience.
  • Source: Generator Architecture Workshop (#562), 2026-02-27. docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-7.
  • Raised by: Araminta (visual grammar and negative-space framing), Tyre (implementation constraint). Full team sign-off.
  • Dissent: None.
  • Cross-reference: D-097 (guarantee tier system — Tier 2), D-101 (ZonePalette — T5 coastal water is the terrain type this guarantee references)

D-103: Assassin Lens Spatial Guarantees — A-1 through A-4

  • Date: 2026-02-27
  • Decision: Four derived spatial properties validated by the guarantee audit for Full-complexity districts. These are derived properties of existing spatial configuration, not assassin-tagged features — they add no generation cost; the audit validates existing output. A-1 Elevated Vantage (Tier 3): ≥1 position with clear LOS cone to Traffic Chokepoint. A-2 Egress Multiplicity (Tier 3): ≥2 exit routes to adjacent districts. A-3 Temporal Opacity Window (Tier 3): ≥1 time window where Social Hub has reduced ambient NPC coverage. A-4 Non-Institutional Route (mandatory Full-complexity): ≥1 route to any Insider zone not passing through high-security institutional spaces. A-1/A-2/A-3 are Tier 3 Conditional (trigger on complexity_tier == Full). A-4 is mandatory for all Full-complexity districts regardless of playstyle.
  • Rationale: The investigator/assassin playstyle needs guaranteed affordances without the generator explicitly building for assassination. Derived properties keep generation cost zero while ensuring spatial conditions exist.
  • Source: Generator Architecture Workshop (#562), 2026-02-27. docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-8.
  • Raised by: Gestalt (assassin lens framing and derived-properties insight). Full team sign-off.
  • Dissent: None.
  • Cross-reference: D-097 (guarantee tier system — Tier 3)

D-106: Vertical Scale Architecture and Rooftop Bar Clause

  • Date: 2026-02-27
  • Decision: Four height tiers: S1 (12 z-levels, surface + roof/mezzanine), S2 (310), S3 (1130), S4 (30+). Shadow length is the primary height signal in top-down view (240 visual tiles). Lazy z-level loading: ZLevelLoadState: Loaded | Skeleton | Ungenerated — only current + adjacent z-levels filled by Phase 2. Rooftop Bar Clause: Every tall structure (z_band_count ≥ 3) must assign RooftopConfig: Restricted | PublicWithHiddenLayer. Discovery layer mandatory in both configurations. Heritage root weights the probability between the two configs — it does not determine the outcome. Final config is seeded per-building; a minority of buildings of any heritage root may be the non-dominant type (a Frost building with a rooftop bar must be possible). Z-band floor boundaries must have seed-variation within cultural ordering constraints. Vertical access routes are playthrough-history dependent.
  • Rationale: Height has meaning — floor 30 has information floor 1 cannot have because it is harder to reach. Full determination of rooftop config by heritage root kills the discovery moment.
  • Source: Generator Architecture Workshop (#562), 2026-02-27. docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-11.
  • Raised by: Tyre (z-level architecture), Ozzie (Rooftop Bar Clause — discovery guarantee). Ozzie + Araminta corrected "determines" → "weights probability" in Round 5.
  • Dissent: None.
  • Cross-reference: D-094 (spatial hierarchy), D-097 (guarantee tier system — Rooftop Discovery Zone is Tier 2)

D-108: MobileChunk Specification

  • Date: 2026-02-27
  • Decision: Entity-carried interior space attached to a mobile world entity. Not a district — uses the same chunk fill primitives in a simpler flat structure (no Phase 1/Phase 2 split, no block grid, no zone negotiation). Key structs: MobileChunk, MobileInterior, VesselClass, MobileMovementState (Docked / InTransit / InterSystem / Idle), TransitSocialModifier, MobileNpcSlot, NpcPersistence (Crew | Passenger). Idle = vessel parked at a location but not docked to infrastructure (anchored ship, grounded shuttle). Vessels are persistent world entities — interior cache keyed by entity_id persists across voyages for crew state. Docked state requires dock_position, connected_chunk: Option<ChunkCoord>, docked_since: SimTick, scheduled_departure: Option<SimTick>. scheduled_departure must be populated by the generator; vessels without departure schedules are an error state. Cultural grammar: TransitSocialModifier with TransitVariant (BoundedLinear | BoundedMobile | InterSystem). Vessel visual grammar (5 rules): (1) hull uses vessel-identity material, not zone palette; (2) windows reveal exterior context (docked vs. transit); (3) compression modifier tightens proportions; (4) section transitions use vessel-identity threshold elements; (5) class stratification via proportion, not palette. Replayability requirements R-V-1 through R-V-6 in docs/workshops/generator-architecture/round-4-notes.md §5. Memory: ~0.54KB metadata + up to 64KB ChunkData per vessel; paged by streaming model.
  • Rationale: "The journey is content — mobile environments are social pressure cookers, not loading screens with chairs." Vessel persistence and crew state continuity make the world feel real.
  • Source: Generator Architecture Workshop (#562), 2026-02-27. docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-13.
  • Raised by: Tyre (struct design), Miri (cultural grammar — miri-round4.md), Nigel (replayability requirements), Ozzie (player experience). Visual grammar: Araminta (araminta-round4.md §2).
  • Dissent: Nigel initially proposed instanced districts for vessels; lead ruled entity-carried MobileChunk for persistence.
  • Note: The Idle movement state is the canonical primitive for player-owned stationary installations (space stations, orbital platforms, parked vessels as permanent bases). A MobileChunk in Idle with no scheduled_departure is architecturally equivalent to a static chunk from the simulation's perspective — it participates in the same tile/zone system. This design prevents future over-engineering of a separate 'player installation' system.
  • Cross-reference: D-100 (DamageOverlay applies to vessel damage), D-109 (LocalOverlay mandate), D-111 (MobileChunk Idle state as stationary installation primitive), Q-046 (departure schedule — resolved by this D-record)

D-109: DamageOverlay / RegenerationStrategy Prohibition — Architectural Mandate

  • Date: 2026-02-27
  • Decision: XOR reseeding for in-playthrough events is architecturally prohibited. LocalOverlay is the mandatory modification strategy for all events that occur while the player is present. SoftReseed and FullReseed are permitted only at scenario-boundary and era-level discontinuities respectively — events the player was not present for, where causal legibility is not required. This prohibition is filed as a separate D-record from D-100 because it establishes the modification principle for the entire game, not just the overlay mechanics.
  • Rationale: Causal legibility: the player must be able to look at a damaged district and understand what happened. XOR reseeding destroys the causal thread. Unanimous consensus across all workshop participants — the strongest architectural agreement of the entire workshop.
  • Source: Generator Architecture Workshop (#562), 2026-02-27. docs/workshops/generator-architecture/workshop-outcomes.md §D-READY-14.
  • Raised by: Gestalt (XOR prohibition framing), Tyre (RegenerationStrategy struct). Unanimous.
  • Dissent: None.
  • Cross-reference: D-100 (DamageOverlay + RegenerationStrategy full specification)

D-110: Signed Z-Level Addressing — base_z u8 → i8

  • Date: 2026-02-27
  • Decision: All z-level base fields use signed integers (i8) instead of unsigned (u8). Specifically: MultiBlockReservation.base_z: i8, FloorZone.z_level: i8, and any struct that references a z-level position (not a count). Z-level counts (z_levels: u8 on DistrictSkeleton) remain unsigned — they represent "how many floors", which is always positive. The distinction: base_z is "where does the bottom floor start" (can be negative for basements/subterranean spaces), z_levels is "how many floors total" (always ≥1).
  • Rationale: The Round 4 workshop designs (D-106, D-108) and the gestalt gas explosion example explicitly assume negative z-levels (basements at z=-1 to z=-3, utility sub-levels). The existing u8 type contradicts the design intent. A deep mine is structurally an inverted skyscraper with base_z: -30, z_levels: 30. Lazy z-level loading (ZLevelLoadState) works identically for negative z — the loading system cares about relative offsets between adjacent levels, not the sign.
  • Source: Tyre architectural analysis, 2026-02-27. Confirmed existing Round 4 design intent (D-106 §height tiers, Gestalt Round 4 §gas explosion, Araminta Round 2 §infrastructure routing).
  • Raised by: Tyre (type-level fix), lead (edge case prompt: basements, deep mines).
  • Dissent: None.
  • Cross-reference: D-106 (vertical scale architecture), D-094 (spatial hierarchy), D-108 (MobileChunk)

D-111: MobileChunk Idle State Covers Stationary Player Installations

  • Date: 2026-02-27
  • Decision: The MobileChunk Idle movement state (D-108) is the canonical primitive for player-owned stationary installations — space stations, orbital platforms, parked vessels used as bases, or any persistent interior space the player controls that is not part of the district chunk grid. A stationary installation is a MobileChunk that does not move: it uses the same streaming, save/load, NPC simulation, and LocalOverlay modification system as vessels. Player construction within a MobileChunk (building rooms, placing equipment) requires the DLC construction system to emit valid LocalOverlay modifications — the architectural pattern is ready, the construction system is DLC scope.
  • Rationale: A separate "location instance" system for player bases would require a new coordinate system, new streaming/loading path, new save/load path, portal/transition logic, and duplicate pathfinding/perception/simulation — Tier 4 difficulty for zero benefit over existing primitives. The Idle state already exists in D-108; documenting its design intent for stationary installations costs nothing and prevents future over-engineering.
  • Source: Tyre architectural analysis, 2026-02-27.
  • Raised by: Tyre (architectural mapping), lead (edge case prompt: player bases, fortresses).
  • Dissent: None.
  • Cross-reference: D-108 (MobileChunk specification — Idle state), D-100/D-109 (LocalOverlay for modifications)

D-112: No Separate Location Instancing System

  • Date: 2026-02-27
  • Decision: The game does not use instanced locations (separate spatial domains outside the world chunk grid). All subterranean spaces, player bases, and special locations are represented using existing primitives:
    • Basements / sub-levels: MultiBlockReservation with negative base_z (D-110)
    • Deep mines (shaft-style): Downward MultiBlockReservation, lazy-loaded via ZLevelLoadState
    • Deep mines (cave network): Organic-mode district (D-096) with mine-specific template
    • Player base in existing building: LocalOverlay modifications (D-100/D-109)
    • Player base as hidden bunker: MultiBlockReservation with negative z, generated at world-gen
    • Player-built space: LocalOverlay + construction system (DLC scope)
    • Stationary installation: MobileChunk in Idle state (D-111)
    • Vessel interior: MobileChunk (D-108)
  • Rationale: Instanced locations would require: (1) new coordinate system for instanced space, (2) new streaming/loading path, (3) new save/load path, (4) portal/transition logic between world space and instanced space, (5) duplicate pathfinding, perception, and simulation tier logic. Every edge case maps cleanly to an existing primitive. Nigel's vessel instancing proposal was already ruled out in favor of entity-carried MobileChunk (D-108 dissent note); the same reasoning extends to all "separate space" cases.
  • DLC/Mod implication: LocalOverlay is the universal post-generation modification layer. DLC quest locations, mod-injected dungeons, and scenario-specific spaces can be delivered as overlay packages applied to existing districts under strict conditions — without touching the generator or reseeding. The generator output remains sacred (D-109); overlays are how the world changes after generation. This makes the overlay system the canonical content injection point for all post-generation content, whether player-driven, storyteller-driven, or DLC/mod-driven.
  • Source: Tyre architectural analysis, 2026-02-27.
  • Raised by: Tyre (comprehensive edge case mapping), lead (edge case prompt: deep mines, player bases, fortresses).
  • Dissent: None.
  • Cross-reference: D-110 (signed z-levels), D-111 (MobileChunk Idle for installations), D-108 (MobileChunk), D-096 (organic layout), D-100/D-109 (LocalOverlay)

D-113: Tile data model — extensible per-tile properties

  • Date: 2026-03-05
  • Decision: Replace the current single-character tile encoding (F/W/V/R strings in location YAML) with a tile palette/registry system (option A from the design space). Tiles are typed by a palette ID; per-type properties are defined once in the palette and inherited by all tiles of that type. Per-tile overrides are supported via a sparse overlay map.
  • Current state: Tiles are single characters in string arrays. Each character maps to a TileKind enum (Floor, Wall, Door, Object) and a walkability bool. TileCell in WalkabilityMap stores { walkable: bool, kind: TileKind }. No per-tile properties (material, visual variant, sound, access lists, container contents, damage state, trigger zones) can be expressed.
  • Design survey — what systems need tile-level data:
    1. Doors — access lists (who can open), open/closed state, locked/unlocked. Currently no tile-level door data; TileKind::Door exists but carries no properties.
    2. Containers — contents, capacity, searched state. Currently handled by entity ObjectType::Container on separate entities, not tiles. Containers should remain entities, not tile properties.
    3. Damage stateDamageOverlay (D-100) modifies tiles post-generation. Damage needs to degrade tile properties (walkability, visual, material) without replacing the base tile type.
    4. Visual variants — same logical tile type (e.g., "industrial floor") with per-tile visual variation for visual richness. Currently impossible — all Floor tiles look identical to the client.
    5. Trigger zones — tile-level triggers for entry/exit events (zone transitions, alarms, dialogue triggers). Currently handled by ZoneMap at zone granularity, not per-tile.
    6. Material properties — footstep sound, movement speed modifier, surface type for particle effects. Currently all tiles produce the same footstep sound.
    7. WallBackside (D-099) — structural classification behind wall surfaces. Already defined as an enum but not yet integrated into tile data.
  • Chosen approach — Tile Palette + Sparse Override:
    • Tile palette (YAML, per-district or global): defines tile types by string ID. Each type specifies: walkable: bool, kind: TileKind, material: String (footstep/SFX), visual_base: String (client sprite), visual_variants: u8 (random variant count), los_blocking: bool, movement_cost: f32 (default 1.0), optional wall_backside: WallBackside (D-099). The palette is the type-level contract — most tiles need no per-instance data beyond their palette ID.
    • Tile map (YAML): retains the string-array format for human readability, but each character is a palette key (single char or short code). Backward-compatible: F, W, V, R are reserved palette keys that map to current behavior. New tile types use additional characters or a separate palette layer.
    • Sparse override map (YAML): overrides key on Location — a list of { x, y, properties } entries for tiles that differ from their palette type. Supports: door access lists, initial locked state, visual variant pinning, damage overlay data. Only tiles with non-default properties need entries. Keeps the string map clean for 90%+ of tiles.
    • Runtime representation:
      • TilePalette resource: BTreeMap<char, TileType> loaded at startup. Immutable after load.
      • TileCell extended: { palette_id: char, walkable: bool, kind: TileKind, material_id: u16 }. Material ID is a compact index into the palette's material table.
      • TileOverrideMap resource: BTreeMap<(i32, i32, i32), TileOverride> for per-tile overrides. Sparse — only tiles with overrides consume memory.
      • ECS queries: WalkabilityMap remains the primary interface for movement/pathfinding (unchanged API). TilePalette provides material/visual data when needed (snapshot construction, sound system). TileOverrideMap provides door state, access lists, damage overlays.
  • YAML authoring format:
    # Palette definition (loaded once, reusable across locations)
    palette:
      F: { walkable: true, kind: Floor, material: metal-grate, visual_base: floor_industrial }
      W: { walkable: false, kind: Wall, material: bulkhead, visual_base: wall_heavy, los_blocking: true }
      D: { walkable: true, kind: Door, material: metal-door, visual_base: door_standard }
      G: { walkable: true, kind: Floor, material: glass-panel, visual_base: floor_glass }
      R: { walkable: false, kind: Floor, material: metal-grate, visual_base: floor_restricted }
    
    # Location tile map (unchanged human-readable format)
    tiles:
      - "WWWWWWWWWWWWWW"
      - "WFFFFDFFFFFFFW"
      - "WFFFFFFFFFFGFW"
      - "WWWWWWWWWWWWWW"
    
    # Per-tile overrides (sparse, only for non-default properties)
    overrides:
      - { x: 5, y: 1, door_access: [faction.commission], locked: true }
      - { x: 12, y: 2, visual_variant: 3 }
    
  • Loader contract: ContentPlugin loads palette YAML first, then location tiles. The apply_location_tiles() function resolves each character via palette lookup instead of the current hardcoded match. Unknown characters fall back to Floor with a warning (same as current behavior). Overrides are loaded after tiles and applied to TileOverrideMap.
  • Migration effort for existing locations (5 files):
    • Zero-migration path: The default palette defines F/W/V/R with identical behavior to current hardcoded mapping. Existing location YAMLs work unchanged. No migration required for v0.1.
    • Incremental enrichment: Locations can opt into the new palette by adding a palette: key. Locations without palette: use the global default. Migration is per-location, at author pace.
    • Estimated effort: Palette definition = 0.5 day. Loader refactor = 1-2 days. Override system = 1 day. Total: 2-4 developer-days. No changes to location YAML files required for v0.1.
  • Alternatives considered:
    • (b) Per-tile property bags (arbitrary key-value per tile): Maximum flexibility but violates D-010 principle 4 (deterministic — dynamic typing makes serialization non-deterministic). Memory cost: ~100 bytes/tile vs ~6 bytes/tile with palette. Rejected.
    • (c) ECS-style tile components (tiles as entities): Each tile becomes a bevy_ecs entity with optional components. Elegant in theory but 150x150x3 = 67,500 entities per location, potentially 4M+ entities for a district. ECS entity overhead (~128 bytes each) makes this prohibitively expensive. Queries scale poorly at this count. Rejected for spatial data; tiles remain grid-based. Entities are reserved for interactive objects placed ON tiles.
    • (d) Hybrid (palette + entity overlay): Palette for base tiles, entities for interactive tile features (doors, containers, triggers). This is almost what we chose — the distinction is that our sparse override map is grid-indexed (O(1) lookup by position) rather than entity-query based. Interactive objects that have their own behavior (NPCs, containers, items) remain entities; tile properties that are spatial/static (material, visual variant, access) are grid data.
  • Key design principles:
    • Palette is the type; override is the instance. 90%+ of tiles need only a palette ID.
    • String-array tile maps remain human-readable and merge-friendly. No JSON, no complex nested structures.
    • WalkabilityMap API is unchanged — callers don't know about palettes.
    • BTreeMap for deterministic iteration per D-010 principle 4.
    • Palette keys are char (single Unicode codepoint) for direct mapping from tile string arrays.
  • Raised by: Tyre (architecture), requested by #586 (Epic: extensible tile data model).
  • Dissent: None anticipated — this is a design-only D-record for post-v0.1 implementation.
  • Cross-reference: D-054 (tile-based movement), D-066 (dual-scale grid), D-094 (spatial hierarchy), D-099 (WallBackside classification), D-100 (DamageOverlay), D-012 (chunk architecture)

D-133: Skills affect outcome — same verbs available, skill determines quality

  • Date: 2026-03-05
  • Decision: The skills-to-verb coupling model is: everyone sees the same verbs (mostly). Skills determine how well you execute — bad at social means you can still talk, just badly. Some advanced verbs may still be gated by skill level, but the default is outcome-based, not access-based. This is the simplest learnable model: try anything, skill determines result.
  • Rationale: Verb access gating (skill gates whether you can even attempt an action) creates invisible walls and punishes players for trying. Outcome-based (skill determines quality of result) lets players learn by doing and creates organic differentiation. A tycoon with low social can still negotiate — they just negotiate poorly, which produces interesting consequences.
  • Source: Where's the Fun? Workshop, Round 4 Interview, Decision 5
  • Raised by: Team Leader (Jeroen) — outcome model (option C)
  • Dissent: None
  • Cross-reference: D-120 (no skill ceiling in v0.2)

D-134: Full character customization — hair, clothing, colors at tile scale

  • Date: 2026-03-05
  • Decision: Full character appearance customization is in scope: hair, clothing, colors. Readability at top-down tile scale is solved through outline and highlight mechanics, not by limiting customization options. The character creation screen is an emotional investment moment — the player should feel this is their character.
  • Rationale: Customization at this scale was assumed to be a readability risk. The workshop decision: solve the readability problem rather than limit the player. Readability via outline/highlight is a solved problem in the tile rendering pipeline. Limiting customization would undermine the identity investment that makes life-sim attachment possible.
  • Source: Where's the Fun? Workshop, Round 4 Interview, Decision 11
  • Raised by: Team Leader (Jeroen)
  • Dissent: None

D-135: Setting delivery via both layers — visual world + insert in parallel

  • Date: 2026-03-05
  • Decision: Setting is delivered through two parallel layers: (1) the physical world — visuals and NPC behavior show context, atmosphere, place; (2) the neural insert — names, contextualizes, provides information the character would know from their background. Araminta (visual layer) and Mellanie (insert copy layer) work in parallel. Both layers are required from day one of the tycoon bookmark experience.
  • Rationale: Either layer alone is insufficient. Visuals without naming leave the player in a beautiful void with no cultural foothold. Naming without visuals produces an exposition dump. Both together produce the "this is a place" sensation the workshop identified as the missing ingredient of v0.1.
  • Source: Where's the Fun? Workshop, Round 4 Interview, Decision 12
  • Raised by: Team Leader (Jeroen) — both layered (option C)
  • Dissent: None
  • Cross-reference: D-128 (culture as context for insert copy)

D-136: First Settled Reach moment — auto-generated apartment + insert activation

  • Date: 2026-03-05
  • Decision: The first moment of The Settled Reach is two layered beats: (1) Waking up in YOUR auto-generated apartment (reflects your economic position from the tycoon bookmark; wealthy, modest, or constrained start matters). (2) Insert activation — the neural implant powering on is intimate, personal, tech-specific. The alarm clock is the Groundhog Day homage (D-126). The apartment reflects the character's economic position — auto-generated, not hand-built.
  • Rationale: The apartment establishes place, economic status, and self without exposition. Insert activation establishes the neural lattice as intimate and personal — this is your character's relationship with their technology. Both beats together create the "this is MY character in MY world" moment that v0.1 lacked.
  • Source: Where's the Fun? Workshop, Round 4 Interview, Decision 13
  • Raised by: Team Leader (Jeroen)
  • Dissent: None
  • Cross-reference: D-126 (alarm clock tone), D-135 (both layers active from first moment)

D-137: Generator produces both structural and cosmetic variety at different scales

  • Date: 2026-03-05
  • Decision: The generator must produce two types of variety simultaneously at different scales: (1) Structural variety — operates at seed level: different playthroughs have genuinely different world structures (economic landscape, faction power balance, crisis composition, NPC role distribution). (2) Cosmetic variety — operates within a structure: NPC names, faces, apartment layouts vary per instance. Structural variety is the higher-priority proof for the Sprint 25 spike (D-119).
  • Rationale: Cosmetic variety without structural variety produces "same game with different wallpaper." Structural variety without cosmetic variety produces identical-looking characters with different internal states. Both are load-bearing for the life-sim experience — structural variety drives replay value, cosmetic variety drives in-session believability.
  • Source: Where's the Fun? Workshop, Round 5 Interview, Decision 23
  • Raised by: Team Leader (Jeroen)
  • Dissent: None
  • Cross-reference: D-114 (generator proof-of-life), D-119 (Sprint 25 generator spike)

D-141: PlatformInfo — client-side OS abstraction autoload

  • Date: 2026-03-13
  • Decision: All OS-dependent queries on the client are centralized in a single PlatformInfo autoload (client/scripts/autoloads/platform_info.gd), registered first in the autoload order. Individual systems (HardwareDetector, voice pipeline, settings UI) consume PlatformInfo properties and signals — they never call OS.* directly. PlatformInfo owns: power state (with PowerProfile enum: FULL, BATTERY, POWER_SAVER), memory queries, platform identity, and platform-dependent file paths. Power state is polled on a 30-second timer with a power_profile_changed signal; memory is refreshed on demand. The PowerProfile enum is the abstraction seam for future power-saver detection (GDExtension) without consumer code changes. PlatformInfo is client-side only — the client never relies on the server for hardware info, because the server may not be on the same hardware in multiplayer/remote hosting scenarios. Each side detects independently.
  • Rationale: OS calls were scattered across HardwareDetector, AiDialogueDetector (duplicate), and SimBridge. A central abstraction prevents duplication, provides a single seam for platform-specific behavior, and keeps the client self-sufficient per D-010 (information boundaries) and future multiplayer readiness.
  • Raised by: Team Leader (Jeroen)
  • Dissent: Centralized server detection was considered and rejected — server may not share hardware with client in future multiplayer scenarios.
  • Cross-reference: D-138 (hardware detection for voice pipeline), D-010 (information boundaries), Q-059 (full interface scope — open)

D-148: 30° low-angle camera with 45° map rotation — supersedes D-019

  • Date: 2026-03-17
  • Decision: The default gameplay camera is an orthographic Camera3D at 30° tilt (60° from horizontal) with the tile map rotated 45° into a diamond grid. Not faked in art — an actual Camera3D tilt. Art direction reference: Hades, Divinity: Original Sin. Three camera angle presets: frontal (-5°) (default for character editor/mugshot, see D-158), dramatic/low-angle (-30°) (gameplay default), overhead (-80°) (near-top-down view; 0° in code convention is horizontal, not overhead). Camera tilt cycle (T key in prototype): top-down ↔ dramatic. 30° is the confirmed gameplay default. The 45° map rotation gives natural depth cues and the classic diamond-grid isometric layout. 45° isometric preset superseded — not part of the confirmed set.
  • Rationale: The 30° angle (versus 45° or top-down) gives significantly more character front visibility and wall depth. Players can see faces, clothing, and character detail rather than primarily hat and shoulder. The diamond grid provides natural spatial depth cues without requiring Z-ordering hacks. The T-key prototype confirmed 30° as the most readable angle at gameplay scale.
  • Architecture note: Unlike D-019's amendment (which faked tilt in sprite art), this is a real Camera3D setting. The 45° map rotation is a Transform3D applied to the tile grid root — it does not affect simulation coordinates, which remain axis-aligned. Vision cone math and all server-side systems remain in unrotated space; the client applies the visual rotation.
  • Raised by: Team Leader (Jeroen) — confirmed during Sprint 28 character visuals spike review
  • Dissent: None
  • Supersedes: D-019
  • Cross-reference: D-149 (3D rendering), D-151 (direction count)

D-149: 3D characters rendered live in scene — not pre-rendered sprites

  • Date: 2026-03-17
  • Decision: Characters are rendered as live 3D models in the Godot scene using a CharacterCompositor (Node3D). The camera is a real Camera3D at 30° tilt (D-148). Characters are not pre-rendered 2D sprite sheets. The 3D model is rotated to match the server-tracked 8-direction facing; the camera and lighting remain fixed. Clothing, hair, and accessories are separate mesh layers composited at runtime.
  • Rationale: The Sprint 28 spike used CSG placeholder characters (cylinders, spheres) and confirmed that even crude 3D shapes read as recognizable people at isometric scale — silhouette, proportion, and facing direction are all legible. Pre-rendered sprites would require 8× (or 4×) separate renders per outfit combination; live 3D compositing gives unlimited clothing/color combinations at negligible extra render cost. Direction changes are a model rotation, not a sprite swap. Future animation is natural.
  • Architecture note: The existing EntityRenderer (client/scripts/rendering/entity_renderer.gd) currently uses a single Sprite2D per entity. Under this decision, EntityRenderer is extended to instantiate a CharacterCompositor scene (Node3D subtree) instead. The compositor API is specified in docs/design/compositor-api-spec.md.
  • Raised by: Team Leader (Jeroen) — spike prototype confirmed; Sprint 28 workshop decision
  • Dissent: None
  • Cross-reference: D-148, D-150, D-151, D-152, ticket #693 (compositor implementation)

D-150: Character outline — inverted hull method

  • Date: 2026-03-17
  • Decision: Character outlines are rendered via the inverted hull method (GPU vertex extrusion on a back-face-only render pass). Color: #1e1e24 (very dark blue-grey) for all characters, always — not pure black. Specified by Araminta (art direction, Sprint 28 Round 2). No screen-space outline system. At LOD tier 2 (billboard impostor), the outline is baked into the impostor sprite — no separate draw call needed at that tier.
  • Rationale: Inverted hull is GPU-cheap, works correctly in 3D space, and produces clean consistent outlines. Screen-space methods (e.g., Sobel filter) are more expensive and produce artifacts at isometric angles. The billboard LOD tier naturally subsumes the outline into the baked sprite, so the system degrades gracefully under performance pressure without special outline handling.
  • Raised by: Sprint 28 workshop consensus (Tyre technical, Jeroen confirmed)
  • Dissent: None
  • Cross-reference: D-149, D-152, D-154

D-151: Direction count — 8 server-side facings, 4 visual groups client Sprint 28

  • Date: 2026-03-17
  • Decision: The server tracks 8 facing directions for all characters (N, NE, E, SE, S, SW, W, NW) — full resolution, future-proof. The client renders 4 visual groups for Sprint 28: North (covers N, NW), East (covers NE, E), South (covers SE, S), West (covers SW, W). The 3D model is rotated to the true 8-direction angle; only the visual mesh/asset groups are 4-way. E and W groups share mirrored assets. Post-Sprint 28: additional direction-specific mesh variants can be authored for diagonal facings without protocol changes.
  • Critical distinction — perception vs. rendering: The 8 server facings are perception system input (fog-of-war, vision cone direction, all simulation logic). The 4 visual groups are rendering output (what the player sees). These are not the same thing. Diagonal facings (NE, NW, SE, SW) do not exist as visible character states — a character facing NE renders as East. EntityRenderer maps the server's 8-direction value to a 4-group CharacterFacing before calling the compositor. ModelRoot body rotation uses the true 8-direction angle for subtle lean.
  • Rationale: Hybrid approach (Tyre): 8 server facings ensures the data model never needs migration. 4 client visual groups keeps Sprint 28 asset authoring cost manageable — each new clothing item needs 2 unique meshes (N, E) plus mirroring, not 4 or 8. The 3D rotation to true angle (before the visual group snap) gives subtle body lean and positioning cues even within a visual group. Characters snap to nearest cardinal with smooth rotation interpolation.
  • Raised by: Tyre (hybrid proposal), confirmed by Team Leader (Jeroen) — Sprint 28 Round 2
  • Dissent: None
  • Cross-reference: D-148, D-149

D-152: Character LOD — performance-driven budget, not distance threshold

  • Date: 2026-03-17
  • Decision: Character LOD degrades based on GPU frame budget, not distance or fixed character count. Three tiers:
    • Tier 0 (full): All characters at full 3D detail with all compositor layers active.
    • Tier 1 (simplified mesh): Reduced-poly model; clothing layers merged into combined mesh; inverted hull still active.
    • Tier 2 (billboard impostor): Flat sprite impostor. Outline baked in. Must visually preserve: (1) body size tier (slim/average/stocky silhouette), (2) dominant clothing color (cloth_primary). Applied to characters furthest from player first. LOD trigger is proactive on projected character count, not reactive on frame drop. Reactive triggering produces visible hitches; proactive demotion is invisible. LOD demotes characters outward from player: nearest characters always stay at Tier 0 longest. When paused, render budget is fully freed and all characters restore to Tier 0 — player can inspect the scene at leisure. In a chaotic moment (400+ characters on screen), peripheral detail naturally degrades, which matches the cognitive experience of chaos.
  • Rationale: A fixed distance or count threshold would produce visually jarring sudden LOD pops when character density changes (a crowd gathering). Performance-driven budget is adaptive and invisible to the player. The "paused = full detail" design is a deliberate player affordance — it makes the pause button feel powerful.
  • Raised by: Sprint 28 workshop (Jeroen — Q2 resolution)
  • Dissent: None
  • Cross-reference: D-149, D-150

D-160: Body meshes must be segmented into 18 bone-group regions

  • Date: 2026-03-19 (updated 2026-03-19: 17→18, added torso_upper)
  • Decision: Character body meshes are segmented into 18 bone-group regions (head, neck, torso, torso_upper, arm_upper_l/r, arm_lower_l/r, hand_l/r, leg_upper_l/r, leg_lower_l/r, foot_l/r, eyes, eyebrows). Each segment is a separate skinned GLB on the shared 65-bone skeleton. Segments have a 1-ring vertex overlap at boundaries to eliminate visible seams during animation. Segments can be individually hidden when clothing covers them. The torso_upper variant covers chest-only (for jackets/vests that leave the waist exposed).
  • Rationale: Validated in the Quaternius aesthetic spike. Monolithic body meshes block the compositor: hiding the body to show clothing also removes the head and limbs. Segmentation enables per-slot visibility, head separation for Trellis-generated faces, and limb loss mechanics. The 1-ring vertex overlap was validated as seamless at gameplay zoom.
  • Raised by: Jeroen + Tyre, during spike validation.
  • Cross-reference: D-159 (body type enum), D-149

D-161: Head is always a separate mesh on the Head bone

  • Date: 2026-03-19
  • Decision: Character heads are never baked into body or clothing meshes. The head is always a separate mesh attached to the Head bone via BoneAttachment3D. Trellis generates unique head shapes per character. Hair attaches to the same bone as a separate swappable mesh (enables hairdresser mechanic). The neck stays with the body mesh as a separate segment.
  • Rationale: Validated in the Quaternius spike. The Quaternius pack bakes the head into the body mesh — this blocks clothing display (hiding body = losing head) and prevents per-character face variation. Separating the head unlocks: Trellis-generated faces, idle look-around animation, helmet/hood equipment, hair as a swappable accessory.
  • Raised by: Jeroen, during spike validation.
  • Cross-reference: D-160, D-163

D-162: Clothing is pre-baked per body type via Blender Surface Deform

  • Date: 2026-03-19
  • Decision: Clothing meshes are authored on a reference body type, then batch-fitted to each of the 9 body types via Blender's Surface Deform modifier in an offline pipeline. The fitted variants are exported as separate GLBs. At runtime, the compositor loads the variant matching the character's body type. Runtime clothing scaling does not exist — it was tested (bone pose scaling in Godot) and failed catastrophically.
  • Rationale: Three approaches were tested during the spike: (1) runtime bone scaling — failed with mesh collapse at joints, (2) Blender bone scaling — same failure, (3) Blender Surface Deform — produced output (visual quality at extremes is Q-060). The Surface Deform pipeline is the only viable path. Pre-baking is required; runtime fitting is impossible.
  • Raised by: Tyre + Araminta, during spike research and validation.
  • Cross-reference: D-159, Q-060

D-163: Trellis generates unique heads per character via BoneAttachment3D

  • Date: 2026-03-19
  • Decision: Each character in the game receives a unique Trellis-generated head mesh. The head is a static (unskinned) mesh attached to the Head bone via BoneAttachment3D. Hair is a separate mesh on the same bone. Head generation happens offline during character creation/world generation. The concept image prompt controls face shape, ethnicity, age, and features.
  • Rationale: Validated in the spike: Trellis head + BoneAttachment3D + toon shader produces a coherent character at gameplay zoom. This is the only Trellis output that works on the Quaternius rig — clothing and body meshes both failed. Head generation gives per-character visual identity without hand-modeling.
  • Raised by: Jeroen (insight that Trellis output works as heads), validated by Tyre.
  • Cross-reference: D-161

D-164: Fork Quaternius skeleton, replace all body meshes

  • Date: 2026-03-19
  • Decision: The Quaternius 65-bone skeleton and Universal Animation Library are adopted as the character rig foundation. All body meshes are replaced with hand-authored meshes on the same skeleton. The CC0 license permits unrestricted forking. Quaternius Source tier ($5/mo Patreon, one month) is recommended for .blend source files to support the Blender batch scripting pipeline.
  • Rationale: The spike validated the skeleton and animation library as clean, compatible, and well-structured. The Quaternius Superhero body mesh is retained as the muscular body type (D-159); the Regular mesh becomes average; the Teen mesh becomes teen. The remaining body types (thin, heavy, child) are new meshes authored on the same skeleton. Procedural body type generation via bone scaling failed — separate meshes are required. The skeleton is the expensive part to create from scratch; keeping it and replacing/extending the meshes is the correct split.
  • Raised by: Tyre + Araminta, confirmed by Jeroen.
  • Cross-reference: D-159, D-160

D-166: Development Cascade — 6-Phase First-Things-First Build Order

  • Date: 2026-03-24
  • Decision: Development follows a strict 6-phase cascade. Each phase completes before the next begins. No v0.2 target. No scoping negotiations.
Phase Focus Deliverable
1 Wiki content complete — all planets, moons, stations, heightmaps, artwork Implant-ready Godot map with click-throughs + wiki/GTTR popups
2 Economics layer — supply/demand, transport, corporations, supply chains Economics spreadsheets/graphs with runtime-tweakable simulation
3 Planetary/moon maps & station layouts — cities, rivers, mountains, roads, biomes Atlas of the Reach (implant app)
4 Player control scheme — 2-floor test map, character rendering, walls/stairs/doors, lighting Player viewport with final-version assets
5 World generation (tile/chunk/block) — walkable world, parallel asset pipeline Walkable generated world + asset catalog
6 Detail coloring — room-level content, cultural architecture Only when the world is walkable
  • Rationale: The pattern of negotiating pragmatic v0.2 cuts while discussing room-level detail repeatedly produced superseded decisions, confused agents, and distracted from building the actual product. The cascade enforces a first-things-first discipline: each layer of the game is grounded in the layer below it before detail is added.
  • Raised by: Jeroen, established 2026-03-24 during world generation workshop.
  • Dissent: None.
  • Cross-reference: Initiative #745, Epics #746751, CLAUDE.md cascade table, docs/workshops/world-generation/workshop-outcomes.md

D-169: Implant UI component library — Control node tree with custom Theme

  • Date: 2026-04-05
  • Decision: The implant UI (all diegetic neural overlay panels — star map info, travel planner, station profiles, GTTR reader, cargo manifest, etc.) uses Godot Control node components composed in scenes, styled via a shared Theme resource. Not _draw() draw-objects, not hand-rolled rendering.
  • Rationale: The implant system is tagged as dynamic — different hardware, upgrades, damage states, and faction overlays will change the UI's look and behavior at runtime. This requires layout automation (reflow, resize, expand/collapse), accessibility support, and theme swapping — all things Godot's Control tree provides and a _draw() approach would need to reimplement. The team (Tyre, Stig, Araminta) initially favored _draw() for pixel control, but the dynamic implant requirement makes Control nodes the better long-term investment.
  • Architecture:
    • ImplantPanelPanelContainer with theme override, serves as root for any implant overlay
    • ImplantHeaderHBoxContainer (title label + subtitle label)
    • ImplantSeparatorHSeparator with theme override
    • ImplantDataRowHBoxContainer (key label + value label, two-column)
    • ImplantTextBlockRichTextLabel for wrapping narrative text
    • ImplantStatusBadge — colored dot + label
    • ImplantProgressBar — two-tone, no gradients
    • ImplantTabRow — uppercase labels, underline-active pattern
    • ImplantExpandable — chevron + collapsible section
    • Theme resource (.tres) defines: colors (primary, dim, accent active/positive/negative/warning), spacing (line height, separator padding, panel padding), font sizes (header, body, small, caption)
    • Different implant hardware swaps the entire Theme resource at runtime
  • Rejected alternative: R-NNN _draw() RefCounted components — pixel-precise, zero scene tree overhead, but would require reimplementing layout automation, scroll, and interaction handling as the implant system grows. Correct for a static HUD; wrong for a dynamic system.
  • Raised by: Jeroen + Tyre/Stig/Araminta design discussion, 2026-04-05
  • Dissent: Tyre and Stig initially favored _draw() for aesthetic control. Reversed when the dynamic implant requirement was surfaced.
  • Semantic color roles: PRIMARY_TEXT (#c8d0e0), DIM_TEXT (#667788), ACCENT_ACTIVE (#f0d060), ACCENT_POSITIVE (#44aa66), ACCENT_NEGATIVE (#aa4444), ACCENT_WARNING (#aa8844), SEPARATOR (#c8d0e0 at 12%)
  • Interaction pattern: Invisible until touched. Hover shows 0.3 alpha rect. Active shifts text to ACCENT_ACTIVE. No button chrome — the implant doesn't label its affordances.

D-170: HUD visibility groups — gameplay vs implant layers

  • Date: 2026-04-05
  • Decision: HUD elements register into named visibility groups via a HudGroups autoload. Groups control show/hide of related elements as a unit. Exclusive groups (gameplay, implant) are mutually exclusive — showing one hides the other. Non-exclusive groups (modal, debug) overlay independently.
  • Rationale: Full-screen implant panels (star map, travel planner, GTTR reader) need to hide gameplay HUD elements (stance, minimap, health, interaction prompts). Without groups, each panel manually hides/shows individual nodes — error-prone and unsustainable as the panel count grows.
  • Groups (hierarchical):
    • gameplay — stance indicator, minimap, HUD status, interaction prompts, cursor, inventory, news ticker, examine display. Visible during normal gameplay.
    • implant/map — star map navigator. Mutually exclusive with gameplay and other implant apps.
    • implant/wiki — GTTR reader. Mutually exclusive with gameplay and other implant apps.
    • implant/journal — knowledge journal. Mutually exclusive with gameplay and other implant apps.
    • implant/travel — travel planner. Mutually exclusive with gameplay and other implant apps.
    • implant/station — station profile. Mutually exclusive with gameplay and other implant apps.
    • implant/cargo — cargo manifest. Mutually exclusive with gameplay and other implant apps.
    • modal — settings, bug report, loading screen, debug console. Independent — overlays on top of anything.
    • debug — debug overlay, gauntlet HUD, checklist. Independent.
  • Exclusivity rules: Opening any implant/* app hides gameplay and any other implant app. Closing an app returns to gameplay. Modal and debug are independent layers.
  • API: HudGroups.register(node, "implant/map"), HudGroups.open_app("implant/map"), HudGroups.close_app(), HudGroups.toggle_app("implant/map"), HudGroups.is_app_active("implant/map"), HudGroups.is_implant_active()
  • Implementation: client/scripts/autoloads/hud_groups.gd — lightweight autoload. Uses z-index layer management (not visibility toggling) so all nodes stay active in the tree. Emits gameplay_occluded signal when a fullscreen app covers gameplay — renderers extend GameplayRenderer base class to pause automatically.
  • Raised by: Jeroen, 2026-04-05 — "Walk" badge visible over fullscreen star map.
  • Dissent: None.

D-188: Rename biome_summary to planet_class across codebase

  • Date: 2026-04-06
  • Decision: The biome_summary field in systems.db and all references throughout the codebase are renamed to planet_class. "Biome" describes per-zone vegetation classification (Whittaker table — tropical rainforest, temperate deciduous, etc.). "Planet class" describes the overall planetary character (temperate, arid, frozen, etc.). The two concepts were conflated under one name, causing confusion in the planet generator pipeline where both exist.
  • Scope: DB column (bodies.biome_summarybodies.planet_class), schema SQL, all Rust atlas code, wiki table headers (BiomeClass), atlas proposal JSONs, design docs, decision refs, planet generator scripts.
  • Raised by: Team Leader (Jeroen), during planet generator spike.
  • Dissent: None.

D-191: Atlas of the Reach — Phase 3 Scope and Pipeline

  • Date: 2026-04-10

  • Decision: Phase 3 delivers the Atlas of the Reach as an extension of the implant map (implant/map), adding planetary and regional zoom levels to the existing star map. The Atlas is a read-only spatial intelligence tool — the player looks at it to plan, not to execute actions. It serves dual purpose: gameplay information layer and world-texture content system.

    1. Zoom Hierarchy

    • 4 levels: Reach map (Phase 1, exists) → System view (orbital diagram, new) → Planetary view (hemisphere, new) → Regional view (hundreds-of-km, new — core Phase 3 deliverable)
    • Atlas is the star map extended downward, not a separate app. implant/map at different zoom levels.
    • Station maps and underground/cave city maps are DEFERRED to a later sprint (similar bounded generation pattern).

    2. Existing Foundation

    • 2,394 heightmap PNGs (1024×512 equirectangular, production quality)
    • 2,394 markers.json files with procedural geometry (rivers, oceans, mountains) — all names null, all cities/roads/rail/POIs empty
    • Planet-gen pipeline (tooling/planet-gen/) explicitly designed for Phase 3: render_heightmap.py line 26-27 defers cultural overlay to the atlas app
    • 301 systems in systems.db, 273 inhabited bodies, 466 stations, 668 gate links

    3. Content Pipeline — Sequential Settlement Growth Simulation

    • City placement is terrain-aware and sequential (not scatter)
    • Capital first: favor river mouths (~50% of capitals), scored by habitability (temperature, moisture, slope), coastal access, flat hinterland
    • Subsequent cities: grow along rail corridors from capital (multi-source Dijkstra on cost grid)
    • At cities 34: first foothold on a new continent if one exists (port city)
    • Roads and rail: A* pathfinding on terrain cost grid (water=impassable, mountains=expensive, rivers=cheap corridors), minimum spanning tree — NO straight lines
    • Quadrant distribution: after 2 cities in the same map quadrant, subsequent cities must prefer unoccupied quadrants unless those quadrants have no habitable land
    • Variation: ±25% noise on scoring per seed — same terrain, different seed → different city network
    • Settlement pattern modifies spacing (urban_concentrated=tight, dispersed=wide)
    • All systems same depth, scaled by population. No manual tier classification.

    4. Naming Pipeline — Gemma 2 Voice Pipeline

    • Geographic names generated by the existing Gemma 2 voice pipeline (server/src/voice/, sr-voice binary)
    • Input: body wiki page (planet_class, cultural_corridor, settlement_pattern, population, economic_role, atmospheric_tone) + corridor naming palette
    • Output: culturally-appropriate names for rivers, oceans, mountains, regions, cities
    • Corridor palettes: north_reach (Anglo-Saxon), south_reach (Iberian/Portuguese), east_reach (East Asian), west_reach (Germanic/Nordic), inner_orbit (institutional Latin/Anglo)
    • Dual purpose: Phase 3 content generation AND quality/consistency test of the in-game LLM pipeline
    • Batch throughput: ~80 min for all 2,394 bodies (placement) + ~40 min for 273 inhabited (naming). Parallelizable.
    • Earth-name blocklist as post-processing safety net. Dedup check against full name corpus.

    5. Core Systems as Templates

    • Core systems (Gateway/Sirius, Groombridge/Lendel, etc.) hand-authored as templates
    • Templates establish rulesets and quality bar for the generator
    • Generator produces all remaining bodies → hand-author refinements over the batch
    • Lendel (GJ 380c) is the first proving ground: arid, urban_concentrated, financial hub, 900M pop

    6. Atlas Panel Architecture

    • FULLSCREEN implant app (z=20), same component library as economics panel
    • 3 navigation levels: system picker → orbital diagram → body atlas (heightmap + overlays)
    • Heightmap displayed as Texture2D with pan/zoom
    • Marker overlay renders cities, roads, rail, POIs, named features on top of heightmap
    • Click city → City Data Panel (name, population, currency zone, Commission presence, shadow economy zone, gate distance, economics panel link)
    • Economics panel link: click-through to Phase 2 economics panel pre-filtered to that node — primary Phase 2/3 integration point

    7. Overlay System — 9 MVP Overlays

    • 5 always-on: terrain, infrastructure, named features, gate/spaceport POIs, political zones
    • 4 toggleable: population density, production zones, shadow economy zones (broad bands), corporate presence (Tier 1 only)
    • Deferred overlays visible in toggle bar but locked with unlock requirements shown on hover (creates pull toward Phase 4+ systems)
    • Maps to D-181 signal visibility ladder

    8. Settlement Data Model

    • Amendment (2026-04-15): The original §8 (below) described marker positions as lat/lon objects and city records keyed by population_tier/primary_function/gate_terminal/continent_id. That shape was aspirational — neither the generator nor the hand-authored templates ever emitted it. Both ended up writing pixel-space row/col arrays against a 512 × 256 storage grid, and PR #129 canonizes that shape so the code and the decision stop drifting. The original prose is preserved immediately below; the current shape follows.
    • Original (2026-04-10, superseded): markers.json schema per body: cities (name, lat/lon, population_tier, primary_function, gate_terminal, continent_id), roads (path polylines, connects), railroads (path polylines, connects), pois (name, kind, position), plus existing rivers/oceans/mountains with names filled. Population tier → city count: floor(log10(pop/1M)), modified by settlement_pattern. Gate terminal POI at largest population center, sometimes scattered to a smaller one. Moons: same depth as planets, scale with population.
    • Current canonical format: markers.json is stored in heightmap pixel space. Every marker file declares a grid: { w, h } header — the generator, the 6 hand-authored templates (Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade), and all 2394 procedural seed files ship {"w": 512, "h": 256}. Every position is a two-element array [row, col] of integer pixels into that grid, where row ∈ [0, h) and col ∈ [0, w) (row is the first axis to match NumPy convention and the flood-fill / A* / cost-grid code that tooling/planet-gen/ already runs in). Polyline geometry (roads[*].path, railroads[*].path, rivers[*].path) is [[row, col], [row, col], ...].
    • markers.json top-level schema:
      • grid: {"w": 512, "h": 256}
      • cities[]: {id, name, kind, center: [row, col], population}kind is capital or city; name is empty when awaiting gemma_naming.py (#833).
      • roads[]: {id, name, kind, path: [[row, col], …]}kind is commercial by default for generated roads; hand-authored roads use highway, rural, etc.
      • railroads[]: same shape as roads[]; generated default kind is passenger_freight.
      • pois[]: {id, name, kind, center: [row, col]} — generated POIs are kind: "transit"; hand-authored POIs use institutional, cultural, corporate, etc.
      • rivers[]: {id, name, path: [[row, col], …]}
      • oceans[]: {id, name, kind, center: [row, col], area_fraction} where kind is lake | sea | ocean.
      • mountain_ranges[]: {id, name, center: [row, col], peak: [row, col], area_cells}
    • Pixel space is what the heightmap analysis (flood-fill, A* cost grid, city placement) natively operates on; it is deterministic and avoids double-conversion through a projection.
    • Lat/lon strings are a display-time derivation, not a storage format. The atlas UI converts [row, col] + grid: {w, h} into an equirectangular lat°N/S, lon°E/W string for the city data panel and hover tooltips, using the body's radius for any great-circle distances it needs. This keeps the immersive surface without paying conversion cost in the generator, the DB, or the diff churn on hand-authored files.
    • Atlas DB index (atlas_cities, atlas_roads, atlas_railroads, atlas_pois, atlas_rivers, atlas_oceans, atlas_mountain_ranges, atlas_body_grids) mirrors these scalar fields row-by-row for implant-app and development queries; polyline geometry stays in the JSON files next to the heightmaps.
    • Population tier → city count: floor(log10(pop/1M)), modified by settlement_pattern
    • Gate terminal POI: at largest population center, sometimes scattered to a smaller one
    • Moons: same depth as planets, scale with population

    9. Implementation Pipeline

    • Python: tooling/planet-gen/generate_atlas.py — reuses planet_simulation.simulate(), adds city_placement.py, infrastructure_gen.py, gemma_naming.py, markers_writer.py
    • make atlas-generate runs the full batch. Deterministic per seed. Incremental (skips up-to-date bodies).
    • Pipeline order: simulate terrain → analyze (continents, habitability, river mouths, cost grid) → place cities sequentially → generate infrastructure (A* MST) → name via Gemma 2 → write markers.json

    10. MVP Completion Criteria

    1. Navigation chain works end-to-end (Reach → system → planet → regional)
    2. Regional map content complete for all inhabited bodies
    3. Population-scaled depth (core systems rich, frontier sparse)
    4. City data panel works (click any city, see data + economics link)
    5. Economics panel integration works (link opens Phase 2 panel filtered to node)
    6. All 9 MVP overlays present and functional
    7. Atlas is read-only (no verbs execute from map)
    8. Stations show at system view with mini data panel (no drill-down)
  • Rationale: The heightmap pipeline was designed with Phase 3 in mind — 2,394 base maps exist. The critical path is content (filling empty markers.json arrays), not technology (the atlas panel follows the established implant component pattern). Sequential settlement growth simulation produces more realistic city networks than scatter placement — each city's location is informed by previous placements, terrain, and economic logic. Using the Gemma 2 voice pipeline for naming tests the in-game LLM quality while generating content. Population-scaled depth without manual tier classification keeps the pipeline simple and the authoring burden manageable.

  • Raised by: Full planning team workshop, Sprint 34 (#748). Participants: Gestalt (systems design), Tyre (technical architecture), Miri (worldbuilding), with Jeroen as workshop participant.

  • Dissent: None.

  • Cross-reference: D-166 (development cascade — Phase 3), D-036 (Sova as canonical setting), D-093 (Sova spatial layout), D-094 (district hierarchy), D-095 (Horizon stations), D-170 (HUD visibility/implant apps), D-169 (implant component library), D-181 (signal vocabulary/visibility ladder), D-174 (shadow economy intensity), D-175 (corporation taxonomy), D-138 (Gemma 2 voice pipeline)

D-192: Drop PROTOCOL_VERSION lockstep handshake

  • Decision: Deprecate the snapshot envelope version field, the PROTOCOL_VERSION constants on both server (server/src/bridge/types.rs) and client (client/scripts/protocol/protocol.gd), and the version-mismatch guard in Protocol.decode_snapshot(). Removal is tracked in ticket #868 (server + client coordinated, sprint 37 or later). Once removed, genuine schema mismatches will surface as MessagePack decode errors or missing-field errors at the consumer; that signal is sufficient for our deployment model. Until #868 lands, the field and guard stay in place — they are no longer load-bearing, but removing them requires coordinated edits on both sides and fresh fixture regeneration.
  • Rationale: The version constants were designed for a network deployment where client and server can ship out of sync. Our actual deployment is a subprocess: the Godot client launches the Rust server it was built with. They are always in sync at runtime — the version check has never caught a real mismatch in the field, only dev-time forgetfulness. The cost has been measurable: every protocol-shaping sprint requires bumping two constants in lockstep, and we accumulated tautological tests asserting PROTOCOL_VERSION == N (deleted in sprint 36 — see ticket from this D-record). Removing the handshake makes the per-sprint cost zero. Reversibility: When/if networked multiplayer arrives (no firm date — see D-005), the natural fit is a one-time handshake at connection time (a single client-version vs. server-version exchange in the connection protocol), not a per-snapshot version stamp. So even the multiplayer path doesn't argue for keeping the per-snapshot field — that field would be doubly redundant once a connection-time check exists. The design space hasn't been narrowed.
  • What we lose: A single eager, human-readable error at connect time ("client v22 ↔ server v23"). A genuine dev-time schema drift will now surface as a downstream decode/missing-field error, possibly seconds into a session rather than at handshake.
  • What we keep: All field-presence and roundtrip tests in test_protocol_bridge.gd, test_signal_sprint24.gd, etc. — these cover the behavior the version constant was meant to gate. Decode failure in Messagepack.decode() still rejects malformed payloads.
  • Raised by: Jeroen, sprint-36 client triage. Triggered by stale test_protocol_version_is_19 assertions failing across two suites after the v23 bump, requiring mechanical edits in both places to "fix."
  • Dissent: None.
  • Cross-reference: D-005 (subprocess model — always co-shipped).

D-194: Three-Component District Mix Algorithm for City District Type Distribution

  • Date: 2026-05-01
  • Decision: District type distribution for a generated city is computed from three components combined at generation time:
    1. Population tier guarantees — minimum district counts enforced by city size. Population tier is floor(log10(pop / 1_000_000)), capped at 5. Larger populations guarantee minimum counts of Transit, Commercial, and Residential districts.
    2. 10×9 economic multiplier table — rows are 10 economic_role values (manufacturing, financial, agricultural, extraction, service_mixed, institutional, transit_hub, research, military, residential); columns are 9 DistrictType variants. Each cell is a weight multiplier (0.03.0) applied to that district type's base probability for cities of that economic role.
    3. Political archetype modifiersPoliticalArchetype shifts weights for Institutional, Restricted-access, and Civic district types. Corporate archetype boosts Commercial + Restricted. Commission archetype boosts Institutional + Administrative. Pioneer archetype boosts Mixed-use + Organic residential.
    • Founding age character is applied as a post-mix adjustment to BlockIrregularity (see D-216), not to the district type distribution itself.
    • The mix is self-contained per city: two cities with the same economic role, population tier, and political archetype produce the same district type distribution (modulo seed-driven noise). No city-to-city state dependency.
    • Integer weights throughout — no f32 for D-010 determinism.
  • Rationale: Economic role should visibly shape a city's physical form. A financial hub looks different from a mining hub. Population tier prevents cities from being too small to sustain their economic function. Political archetype encodes power structure in spatial form — Corporate settlements are commercially dense, Commission settlements are institutionally heavy. The three-component model is the minimum set to produce legible variety; adding more inputs risks over-constraining the generator.
  • Ticket: #920
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-191 (atlas pipeline), D-213 (DistrictType enum), D-214 (PoliticalArchetype), D-216 (BlockIrregularity)

D-195: Attractor-Matching Compatibility Matrix for Generative City Placement

  • Date: 2026-05-01
  • Decision: City placement on a planetary surface uses an attractor-matching model. A GeographicAttractor is a terrain feature that increases city placement score at nearby positions. Seven AttractorType variants: RiverMouth, CoastalAccess, RiverCrossing, ValleyFloor, PassEntrance, LakeShore, PlainCenter. A CompatibilityMatrix is a 10×7 scoring table (10 economic_role values × 7 attractor types) whose cells are float weights (0.03.0) representing how strongly that economic role favors that terrain feature. Examples: manufacturing → RiverMouth 2.8, ValleyFloor 2.1; financial → CoastalAccess 2.5, PlainCenter 1.8; agricultural → ValleyFloor 3.0, PlainCenter 2.5. The matrix is authored data (not computed at runtime). Attractor extraction from heightmaps is defined in D-209. Matching algorithm is defined in D-211.
  • Rationale: Terrain-naive city placement produces spatially incoherent worlds. The compatibility matrix gives different city types different terrain affinities, so financial hubs appear on coasts and agricultural cities appear in fertile valleys — without hard-coding placement rules per city type. The float weight matrix gives graduated preference, not binary requirement.
  • Ticket: #919, #925
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-191 (atlas pipeline), D-208 (D8 drainage — attractor extraction), D-209 (feature tag extraction), D-211 (attractor-matching pipeline)

D-196: SettlementClass Enum and Latent Settlement Active/Ghost Logic

  • Date: 2026-05-01
  • Decision: Every settlement (city marker in atlas_city_names) has a SettlementClass that determines how it enters and exits active simulation:
    enum SettlementClass {
        NameLocked,        // Named in wiki; always active regardless of population
        PopulationBudget,  // Active if pop > threshold; ghost if below
        EconomicTriggered, // Active only while economic role condition is met
        OrganicGrowth,     // Emergent; generated by simulation, no prior wiki record
    }
    
    • Active threshold (applies to PopulationBudget): population ≥ 50,000 for a city to receive full Phase 1 district skeleton generation. Below threshold: 1-district stub with Minimal ComplexityTier.
    • Ghost threshold (applies to PopulationBudget): population < 5,000. Settlement is present in atlas data but receives no NPC population; structures are generated as abandoned (Worn/Derelict condition baseline).
    • NameLocked settlements bypass both thresholds — they are always simulated regardless of population (handles narrative-significant small towns).
    • EconomicTriggered settlements collapse to ghost state when their triggering economic condition lapses (e.g., a mining outpost depopulates when the mine is exhausted).
    • OrganicGrowth settlements are not in atlas_city_names at generation time; they are written to the table during simulation when a settlement emerges organically.
  • Rationale: Not every named location needs full generation, and not every simulated location is named. The classification separates authorial intent (NameLocked) from economic reality (PopulationBudget, EconomicTriggered) and simulation emergence (OrganicGrowth). Ghost settlements are important for world texture — abandoned mining towns and depopulated frontier outposts are as legible as thriving hubs.
  • Ticket: #913
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-191 (atlas pipeline), D-200 (CityGenerationContext), D-203 (BodyWorldState), D-207 (atlas_city_names)

D-197: prosperity_baseline Derivation Formula with Topographic Gradient

  • Date: 2026-05-01
  • Decision: Each city's prosperity_baseline (f32, 0.01.0, used as economic pressure state seed) is derived at generation time from four components:
    1. Economic role base — lookup per economic_role value: manufacturing=0.55, financial=0.70, agricultural=0.50, extraction=0.45, service_mixed=0.60, institutional=0.65, transit_hub=0.60, research=0.65, military=0.55, residential=0.50.
    2. Population log-scale bonus0.04 × floor(log10(pop / 1_000_000 + 1)), capped at +0.12. Larger cities are generally more prosperous.
    3. Topographic gradient bonus — terrain features that historically correlate with prosperity add to the baseline: river mouth +0.08, coastal access +0.06, valley floor +0.04, pass entrance +0.03. At most one terrain bonus applies (the highest-scoring attractor at the city's position).
    4. Seed noise — ±0.05 uniform noise applied last (integer-seeded per city, D-010 determinism).
    • Formula: base + pop_bonus + terrain_bonus + noise, clamped to [0.1, 0.95].
    • prosperity_baseline is not the current prosperity level — it is the simulation's starting point and decay/growth target. The live pressure simulation (D-026) drifts from this value based on trade flows, events, and faction pressure.
  • Rationale: A flat random baseline produces economically incoherent worlds. Terrain-informed prosperity encodes real-world patterns: port cities are wealthy, river-mouth cities are strategic. The log-scale population bonus prevents megacities from dominating without eliminating small-city character. Clamping to [0.1, 0.95] prevents degenerate all-thriving or all-collapsing starting states.
  • Ticket: #920 (consumer of prosperity_baseline)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-194 (district mix — consumes prosperity_baseline), D-195 (attractor types that produce terrain bonus)

D-198: Economic Simulation Independence from Layer 12 Spatial Data

  • Date: 2026-05-01
  • Decision: The economics simulation (Phase 2, D-026 background tier) runs independently of Layer 1 (galaxy graph) and Layer 2 (location profiles / planetary topography). Economic state is seeded at game start from systems.db data (economic roles, trade flows, corporate presence) and then drifts via the pressure simulation. The generator (Layer 7 district skeleton) reads economic pressure state as an input but does not feed back into the simulation model. The two layers communicate one-way: simulation → generator (pressure state used to set district condition and density), never generator → simulation.
    • Prohibited: Generator code must not modify PressureState. Generator code must not query live simulation state during async background generation tasks (race condition risk). Generator reads a snapshot of pressure state taken at generation dispatch time.
    • Allowed: The generator reads economic_health, prosperity_baseline, industries, and faction_influence from the snapshot. These are read-only inputs to Phase 1 skeleton classification and Phase 2 condition application.
  • Rationale: Bidirectional coupling between generator and simulation creates initialization order dependencies and potential circular references. The one-way data flow (simulation → generator snapshot → generator) keeps both systems independently testable and avoids race conditions in the Rayon thread pool (D-206). The generator is a consumer of economic state, not a participant in economic evolution.
  • Ticket: #915 (CityGenerationContext reads economic snapshot)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-026 (simulation tiers), D-200 (CityGenerationContext), D-206 (background generation queue)

D-199: 6-Field Minimum Economic Read Set for City Generation Context

  • Date: 2026-05-01
  • Decision: When building a CityGenerationContext (D-200), the generator reads exactly 6 fields from the economic pressure snapshot per city. Reading more fields is permitted but these 6 are the minimum required for correct Phase 1 skeleton classification:
    1. economic_role — primary function of the city (determines DistrictType distribution via D-194)
    2. prosperity_baseline — starting economic health (0.01.0, see D-197)
    3. population — city population (determines ComplexityTier ceiling, BlockSkeleton density)
    4. dominant_faction — faction with highest faction_influence at this location (affects Institutional and Restricted district bias)
    5. founding_age_years — years since settlement founding (drives BlockIrregularity via D-216, era distribution)
    6. settlement_classSettlementClass enum value (D-196, determines whether to generate at all)
    • Fields 15 are read from systems.db (bodies table + economics tables). Field 6 is derived at generator dispatch time.
    • All 6 fields must be present before a generation task is dispatched. Missing fields abort the task with a logged error; generation does not proceed with partial context.
  • Rationale: A fixed minimum read set prevents generators from accumulating unbounded dependencies on simulation state. The 6 fields cover the minimum information needed to produce a correctly-classified skeleton. The abort-on-missing-fields rule ensures generator output is always deterministic from a complete context, never silently degraded from a partial one.
  • Ticket: #915 (CityGenerationContext implementation)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-196 (SettlementClass), D-197 (prosperity_baseline), D-198 (economic simulation independence), D-200 (CityGenerationContext struct)

D-200: Three-Tier Execution Model (Build-Time / Runtime-Background / Runtime-On-Demand)

  • Date: 2026-05-01
  • Decision: The generation pipeline operates at three distinct execution tiers with no cross-tier mutation:
    1. Build-time (Python pipeline): Runs make regen-db. Produces systems.db tables including atlas_body_heightmaps, atlas_city_names, atlas_province_boundaries, body_radius_km. Output is a static artifact committed to the repo. Never runs during gameplay.
    2. Runtime-background (Rayon thread pool, D-206): Triggered by content-spidering events (player approaches a system, NPC names a location, news ticker references a place). Runs D8 drainage analysis (D-208), attractor extraction (D-209), settlement placement, and Phase 1 district skeleton generation. Output goes into BodyWorldState cache (D-203). Transparent to main tick thread.
    3. Runtime-on-demand (main tick thread): Triggered when the player crosses a chunk boundary. Runs Phase 2 chunk fill for the approaching chunk. Must complete within 5ms. Reads from BodyWorldState cache (always populated before this tier runs).
    • Tier boundary rules:
      • Build-time outputs are read-only at runtime.
      • Runtime-background tasks read from systems.db and write to BodyWorldState only.
      • Runtime-on-demand reads from BodyWorldState and writes to the active ECS world (chunk tile data, NPC spawns).
      • No tier may write to a higher tier's outputs. No circular dependencies.
    • CityGenerationContext struct (see below) is the data contract between tiers 1→2.
    struct CityGenerationContext {
        city_id: u64,
        political_archetype: PoliticalArchetype,
        prosperity_baseline: f32,
        surrounding_biome: SettingType,
        road_entry_directions: Vec<u8>,     // compass octants (07)
        footprint_radius_km: f32,
        founding_orientation: FoundingOrientation,
        world_tier: WorldTier,
    }
    
  • Rationale: Three tiers with explicit boundaries eliminates the "where does this code run?" question. Build-time is deterministic and committable. Runtime-background is parallelizable. Runtime-on-demand has strict latency budgets. Cross-tier mutation would create race conditions between the Rayon thread pool and the main tick thread.
  • Ticket: #915
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-026 (simulation tiers), D-194 (district mix), D-203 (BodyWorldState), D-206 (background generation queue), tyre-sw1r3.md Layer 5/6 architecture

D-201: Spatial Hierarchy — Eight Tiers with Locked Dimensions

  • Date: 2026-05-01

  • Decision: The generation pipeline has eight spatial tiers from galaxy to tile. Dimensions are locked and cannot be changed without amending this decision:

    Tier Name Dimensions Purpose
    1 Galaxy 300 systems Galaxy graph, gate topology, cultural corridors
    2 System Orbital mechanics, body catalog
    3 Body ~512×256 pixels (equirectangular heightmap) Planetary topography, climate zones
    4 Region ~50500km Province boundaries (watershed-derived, D-205), biome zones
    5 Settlement ~130km radius City footprint, district layout
    6 District 512×512 sim tiles (256m) Phase 1 skeleton, 4×4 block grid (D-094)
    7 Block 128×128 sim tiles (64m) Generator planning unit, 2×2 chunks (D-094)
    8 Chunk 64×64 sim tiles (32m) Streaming/serialization unit (D-094)
    • Tiers 68 are locked by D-094 (district spatial hierarchy). This decision formalizes Tiers 15 with equivalent lock status.
    • Tier 3 heightmap resolution (512×256 equirectangular at 1024×512 PNG) is the canonical format. Deviation requires amending D-191.
    • Tier 4 province boundaries are pre-computed at build-time and stored in atlas_province_boundaries (D-205). They are not re-computed at runtime.
    • The SettingType enum on DistrictSkeleton is the interface between Tier 5 (settlement planning) and Tier 6 (district generation).
  • Rationale: Locking spatial dimensions prevents the generative layers from drifting in incompatible directions. The heightmap pipeline, atlas pipeline, and district generator all assume these dimensions and would need coordinated migration if they changed. Formalization prevents silent per-system variation.

  • Ticket: #912 (WorldTier enum), #913 (SettlementClass)

  • Raised by: Generation cascade workshop (#897)

  • Cross-reference: D-094 (district hierarchy — Tiers 68), D-191 (atlas pipeline — Tier 3), D-205 (province boundaries — Tier 4), D-208 (D8 drainage — Tier 3 analysis)

D-202: Heightmap BLOB Storage Schema (atlas_body_heightmaps)

  • Date: 2026-05-01
  • Decision: Heightmap elevation data is stored in systems.db as a BLOB in the atlas_body_heightmaps table. Schema:
    CREATE TABLE atlas_body_heightmaps (
        body_id     INTEGER PRIMARY KEY REFERENCES bodies(id),
        width       INTEGER NOT NULL,        -- pixel columns (canonical: 512)
        height      INTEGER NOT NULL,        -- pixel rows (canonical: 256)
        data        BLOB NOT NULL,           -- float32 little-endian, row-major, width×height floats
        sea_level   REAL NOT NULL DEFAULT 0.0,
        imported_at TEXT NOT NULL DEFAULT (datetime('now'))
    );
    
    • data is a float32 little-endian BLOB. Size: width × height × 4 bytes. Canonical: 512×256×4 = ~512KB per body.
    • Values are normalized elevation in [0.0, 1.0]. sea_level is the fraction below which terrain is underwater (default 0.0 = no ocean, overridden per body).
    • The Rust loader reads the BLOB via bytemuck::cast_slice::<u8, f32>() after fetching from SQLite. No endian conversion needed on LE-native systems; the pipeline stores LE explicitly.
    • Only inhabited bodies receive heightmap rows at build-time. Uninhabited bodies are generated on-demand (runtime-background tier, D-200).
    • This table is populated by the import_heightmaps build-time step in the asset pipeline (D-191 §9 pipeline order). It is read-only at runtime.
  • Rationale: Storing heightmaps in systems.db keeps the DB as the single source of truth for all generation inputs, avoids a separate file-fetching path in the Rust server, and allows the pre-push hook (asset pipeline rules) to detect stale heightmap data. The float32 LE layout matches what NumPy and PIL produce natively, minimizing conversion overhead in the Python pipeline.
  • Ticket: #901 (schema), #906 (import), #916 (Rust loader)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-191 (atlas pipeline — heightmap source), D-203 (BodyWorldState — consumer), D-208 (D8 drainage — reads this table)

D-203: BodyWorldState Bevy Resource with LRU Cache

  • Date: 2026-05-01
  • Decision: BodyWorldState is a Bevy Resource holding the Layer 12 output for each recently-accessed planetary body. It functions as an LRU (Least Recently Used) cache:
    • Cache capacity: 50 bodies.
    • Memory budget: ~5MB total (50 bodies × ~100KB per entry average). A single body's Layer 12 data includes: processed heightmap (float32 grid, ~512KB pre-downsampled to ~8KB working resolution), river network (RiverNetwork struct: river cells, confluences, mouths), drainage basin polygons, attractor list, province boundary references.
    • Eviction policy: On cache overflow, evict the body with the oldest last_accessed timestamp. Bodies that are the current player location or adjacent-system neighbors are pinned (not evicted).
    • Population: The runtime-background tier (D-200) populates cache entries via Rayon tasks. Main thread reads are always from the cache; main thread code must never perform blocking DB reads for heightmap data.
    • Struct:
      struct BodyWorldState {
          body_id: u64,
          heightmap: Vec<f32>,               // downsampled working grid
          river_network: RiverNetwork,       // D-208 output
          drainage_basins: Vec<DrainageBasin>,
          attractors: Vec<GeographicAttractor>, // D-195 types
          last_accessed: SimTick,
      }
      
    • The resource is initialized empty and populated on demand. Accessing a body not in the cache triggers a background generation task (D-206).
  • Rationale: The D8 drainage analysis (D-208) and attractor extraction (D-209) are expensive (target: ~50ms/body). Running them on the main tick thread would cause frame drops. The LRU cache ensures the main thread only reads pre-computed data. 50-body capacity covers the typical gameplay scenario (player in one system, neighboring system pre-cached) with margin.
  • Ticket: #917
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-200 (three-tier execution model), D-202 (heightmap BLOB — input), D-206 (background generation queue — populates cache), D-208 (D8 drainage — produces river network)

D-204: body_radius_km Column on bodies Table

  • Date: 2026-05-01
  • Decision: A body_radius_km REAL column is added to the bodies table in systems.db. This value is the mean radius of the planetary body in kilometers, used to:
    • Compute area_count (number of districts a settlement can contain, scales with surface area)
    • Convert province boundary pixel coordinates to real-world km distances
    • Derive the footprint_radius_km field on CityGenerationContext (D-200)
    • Schema change: ALTER TABLE bodies ADD COLUMN body_radius_km REAL (nullable, populated by import step)
    • Fallback derivation (applied when body_radius_km IS NULL): planet_class lookup table with canonical radii:
      • super_earth: 8,000 km
      • earth_like: 6,371 km
      • sub_earth: 4,500 km
      • ocean_world: 6,500 km
      • arid: 5,800 km
      • ice_world: 3,000 km
      • gas_giant: 50,000 km (no settlements)
      • moon: 1,737 km
      • other / unknown: 6,371 km (Earth default)
    • Fallback is applied at query time, not stored back. The column remains NULL until authoritative data is available.
  • Rationale: Surface area scales with radius squared; a body twice Earth's radius has four times the potential settlement density. Without this field the generator must use a flat default for all planets, producing physically implausible city counts on super-earths and moons alike. The fallback ensures the generator works before all bodies have explicit radius data.
  • Ticket: #905 (schema), #910 (populate from planet_class fallback)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-191 (atlas pipeline — body catalog), D-200 (CityGenerationContext — footprint_radius_km)

D-205: Province Boundary Pre-Computation (atlas_province_boundaries)

  • Date: 2026-05-01
  • Decision: Province boundaries (drainage basin divides) are pre-computed at build time from the D8 drainage analysis (D-208) and stored in atlas_province_boundaries:
    CREATE TABLE atlas_province_boundaries (
        body_id   INTEGER NOT NULL REFERENCES bodies(id),
        basin_id  INTEGER NOT NULL,
        path      TEXT NOT NULL,    -- JSON array [[row, col], ...] pixel-space polyline
        area_pct  REAL NOT NULL,    -- fraction of body surface area in this basin
        PRIMARY KEY (body_id, basin_id)
    );
    
    • Boundaries are stored as pixel-space polylines in the same [row, col] convention as markers.json (D-191 §8 canonical format).
    • area_pct is the fraction of the body's total surface area contained within this drainage basin.
    • Province boundaries are the basis for district-level political zoning and cultural corridor assignment at Tier 4 (Region) in D-201.
    • Province count target: 412 provinces per inhabited body, derived naturally from watershed analysis. Bodies with less topographic relief (plains worlds, ocean worlds) produce fewer, larger provinces.
    • At runtime: Province boundaries are read from atlas_province_boundaries at generation dispatch time and cached in BodyWorldState as drainage_basins (D-203). They are not re-computed at runtime.
  • Rationale: Province boundaries define the cultural geography of a world — the mountain ranges and river systems that separated civilizations and produced distinct regional identities. Pre-computing them at build time keeps the runtime-background tier focused on city placement and district generation rather than watershed analysis. Storing as polylines (not rasterized masks) keeps the table compact and human-readable.
  • Ticket: #904 (schema), #907 (populate from watershed analysis)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-191 (atlas pipeline), D-201 (spatial hierarchy — Tier 4 Region), D-203 (BodyWorldState — caches province data), D-208 (D8 drainage — source of basin divides)

D-206: Background Generation Priority Queue and Rayon Thread Infrastructure

  • Date: 2026-05-01
  • Decision: All non-urgent generator work runs through a prioritized Rayon thread pool:
    • Thread count: available_parallelism - 2, minimum 1. Reserves 2 cores for the main tick thread and Bevy scheduler.
    • Priority queue: Four levels: Immediate (player will arrive within 1 game-minute), High (player will arrive within 5 minutes), Medium (player is in the same system), Low (player has seen or heard of this location via NPC or news). Work items at higher priority pre-empt lower-priority items.
    • Work item types: AnalyzeBody(body_id) (D8 drainage + attractor extraction), GenerateSkeleton(city_id, context) (Phase 1 DistrictSkeleton), FillChunk(district_id, block_pos) (Phase 2 chunk fill for pre-loading).
    • Event-driven pre-generation: A SystemNameIndex (Aho-Corasick automaton over all body/system names from systems.db) scans NPC dialogue output and news ticker text. When a scan match hits, the referenced body is queued at Low priority if not already cached. This is the mechanism by which "NPC mentions a place → player travels there → world is already generated on arrival."
    • Completion notification: Completed tasks send a GenerationComplete event to the main tick thread via a crossbeam channel. The main thread drains this channel once per tick.
  • Rationale: The Rayon thread pool handles the D-200 runtime-background tier. The priority queue prevents low-priority speculation from blocking urgent work (player approaching). The Aho-Corasick name index enables cheap always-on scanning — NPC dialogue is low-bandwidth enough that scanning every output line has negligible cost. Pre-generation triggered by narrative content (NPC mentions a place) is the mechanism for making the world feel pre-existing rather than loading-on-demand.
  • Ticket: #924 (background queue), #926 (SystemNameIndex)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-200 (three-tier execution model — runtime-background tier), D-203 (BodyWorldState — output of background tasks), D-208 (D8 drainage — enqueued as AnalyzeBody)

D-207: Fully Generative Placement — markers.json Stripped to Topographic Features

  • Date: 2026-05-01
  • Decision: The atlas_city_names table replaces the authored city positions in markers.json. Going forward, markers.json files contain only topographic features (rivers, oceans, mountain ranges — per D-191 §8 canonical format). City positions, road networks, and rail networks are NOT authored in markers.json; they are generated from the terrain data and stored in atlas_city_names and derived tables.
    CREATE TABLE atlas_city_names (
        id            INTEGER PRIMARY KEY,
        body_id       INTEGER NOT NULL REFERENCES bodies(id),
        name          TEXT NOT NULL,
        economic_role TEXT NOT NULL,
        population    INTEGER NOT NULL,
        corp_id       INTEGER REFERENCES corporations(id),  -- nullable, corp HQ if applicable
        reserved      INTEGER NOT NULL DEFAULT 0,           -- 1 = reserved for authored scenario use
        kind          TEXT NOT NULL DEFAULT 'city'          -- 'capital' | 'city'
    );
    
    • name and position in the markers.json come from different sources: position is generated by the city placement algorithm; name is either authored (wiki), LLM-generated (Gemma 2 naming pipeline), or reserved (corp HQ name). The split allows position generation and naming to run independently.
    • corp_id links to the corporations table when a city is a corporation's headquarters or major hub city.
    • reserved = 1 rows are scenario-specific cities that must not be relocated by the generation algorithm; the generator places other cities around them.
    • markers.json authored city data (hand-written city center positions in the 6 hand-authored templates: Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade) is migrated to atlas_city_names and treated as reserved = 1 rows. The markers.json files for these templates then have their city arrays cleared.
  • Rationale: Authored city positions in markers.json created a split between hand-authored content and procedurally generated content that was impossible to query, diff, or validate consistently. Moving city identity to a table allows: SQL joins against economic data, corp HQ cross-references, scenario reservations, and attractor-matching validation. The topographic features (rivers, mountains) remain in JSON because they are polygon/polyline geometry better suited to JSON than relational rows.
  • Ticket: #902 (schema), #908 (populate from wiki), #909 (corp HQ cross-reference)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-191 (atlas pipeline — markers.json canonical format), D-196 (SettlementClass — column in atlas_city_names), D-200 (CityGenerationContext — reads from this table)

D-208: D8 Priority-Flood Drainage Routing — Layer 1 Empty World

  • Date: 2026-05-01
  • Decision: Drainage routing (the computation of flow direction, flow accumulation, and river network extraction) uses the D8 priority-flood algorithm on the body's float32 heightmap. This is the first Layer 1 computation run on a body before any city placement or attractor extraction.
    • Algorithm: D8 assigns each cell's flow direction to one of 8 neighbors based on the steepest descent. Priority-flood fills depression cells before routing to avoid spurious sinks. Flow accumulation is the count of upstream cells draining through each cell.
    • River threshold: A cell is classified as a river cell when flow_accumulation > 200. This threshold produces river networks of realistic density on canonical 512×256 heightmaps.
    • Outputs stored in BodyWorldState.river_network:
      • river_cells: Vec<(u16, u16)> — pixel positions of all river cells
      • confluences: Vec<(u16, u16)> — positions where two or more rivers merge
      • mouths: Vec<(u16, u16)> — positions where rivers reach sea level or the heightmap edge
    • Province/basin output: Cells that divide adjacent drainage basins become province boundary candidates (D-205). Boundaries are traced as polylines after flow accumulation is complete.
    • Performance target: ~50ms per body on a single Rayon thread for canonical 512×256 resolution.
    • Determinism: Integer-only arithmetic throughout. No f32 in the priority-flood comparisons (use integer-scaled elevation). D-010 compliant.
  • Rationale: D8 is the standard GIS drainage routing algorithm and produces the river networks that drive attractor scoring (river mouths, confluences = high-value RiverMouth attractors). The flow accumulation threshold of 200 was chosen empirically against the Lendel heightmap to produce ~815 named rivers per inhabited body — enough for cultural geography without over-fragmenting the landscape.
  • Ticket: #918
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-195 (attractor types — river mouth is highest-scoring), D-203 (BodyWorldState — output stored here), D-205 (province boundaries — derived from drainage divides), D-209 (feature tag extraction — reads river network)

D-209: Geographic Feature Tag Extraction (7 Settlement Attractor Tags)

  • Date: 2026-05-01
  • Decision: After D8 drainage analysis, 7 AttractorType tags are extracted from the heightmap + river network and stored as Vec<GeographicAttractor> in BodyWorldState. Each attractor has a position [row, col] and a strength: f32 (0.01.0) derived from local terrain quality.
    • Extraction rules per type:
      • RiverMouth: cells in river_network.mouths. Strength = flow_accumulation[cell] / max_flow_accumulation (normalized). Always high-value.
      • CoastalAccess: cells within 3 pixels of a sea/ocean polygon (from oceans[] in markers.json), not already RiverMouth. Strength = 0.6 baseline + coast length bonus.
      • RiverCrossing: cells at confluences or where a river crosses a topographic saddle. Strength = flow_accumulation / max_flow_accumulation × 0.7.
      • ValleyFloor: local elevation minima in non-river cells with positive habitability score (slope < 5°, elevation 1060% of range). Strength = habitability score.
      • PassEntrance: local saddle points between adjacent drainage basins. Strength = inverse of elevation percentile (lower passes score higher).
      • LakeShore: cells adjacent to lake polygons in markers.json. Strength = 0.5 baseline.
      • PlainCenter: cells in flat terrain (slope < 2°) away from all other attractors. Strength = habitability score × 0.4.
    • Sub-biome classification (vegetation, aridity, temperature zones) is derived in parallel and stored as SubBiomeVariant on the attractor for use by the ZonePalette modifier system (D-101).
  • Rationale: The 7 attractor types cover the terrain features that historically determine city placement. Their extraction from the heightmap is deterministic and cheap given the D8 analysis is already complete. The strength normalization ensures attractor scores are comparable across bodies with different elevation ranges.
  • Ticket: #925 (types), #919 (matching pipeline that consumes these)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-195 (CompatibilityMatrix — attractor types), D-203 (BodyWorldState — storage), D-208 (D8 drainage — prerequisite), D-211 (attractor-matching pipeline — consumer)

D-210: Sub-Biome Variant Classification and terrain_modification_cost

  • Date: 2026-05-01
  • Decision: Each GeographicAttractor (D-209) carries a sub_biome: SubBiomeVariant tag that classifies the local terrain more finely than the top-level SettingType. This drives two systems: ZonePalette modifier selection (which visual variant to use) and terrain_modification_cost (how expensive it is to build infrastructure at this location).
    • SubBiomeVariant values: TropicalWet, TemperateForest, TemperateGrassland, BorealForest, Tundra, Desert, Savanna, Alpine, Wetland, CoastalLowland, Volcanic.
    • terrain_modification_cost: f32 (1.0 = baseline, higher = more expensive): derived from sub-biome + local slope. Flat grassland = 1.0. Volcanic = 4.5. Wetland = 3.2. Alpine = 3.8. Coastal lowland = 1.4. Used by the attractor-matching pipeline (D-211) to penalize high-cost terrain for economically marginal cities.
    • Sub-biome classification uses: elevation percentile (of body total), local slope, moisture proxy (distance to nearest river mouth or coast), and temperature proxy (latitude of the equirectangular pixel).
    • Sub-biome data is stored in BodyWorldState alongside the attractors; it is not a separate DB table.
  • Rationale: Two cities on coastal terrain feel different when one is a tropical lowland port and the other is a cold Nordic fjord. Sub-biome tags enable the ZonePalette to select the correct visual register (T6 beach/coastal with tropical modifier vs T7 mountain/high with coastal modifier). The terrain_modification_cost gives the generator a principled reason to prefer some attractor positions over others for lower-prosperity cities.
  • Ticket: #919 (attractor matching — uses terrain_modification_cost)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-101 (ZonePalette modifier system — consumes sub_biome), D-195 (attractor types), D-209 (feature tag extraction — assigns sub_biome)

D-211: Attractor-Matching Five-Phase Pipeline for Settlement Placement

  • Date: 2026-05-01
  • Decision: Given a body's Vec<GeographicAttractor> and a set of cities from atlas_city_names, settlement placement runs a five-phase matching pipeline:
    1. Score matrix build: Compute a city_count × attractor_count score matrix. Each cell = CompatibilityMatrix[economic_role][attractor_type] × attractor.strength × (1.0 / terrain_modification_cost).
    2. Tier A greedy assignment: For each city with SettlementClass::NameLocked or population ≥ 1,000,000, assign the highest-scoring unoccupied attractor using greedy selection. These cities must be placed first to anchor the spatial layout.
    3. Hungarian algorithm for Tier B+C: Apply the Hungarian algorithm to the remaining cities (population 50,000999,999) and remaining attractors. Produces optimal global assignment maximizing total score.
    4. Synthetic attractor overflow: Cities that cannot be matched to a real attractor (attractor pool exhausted) receive a synthetic PlainCenter attractor generated at a position that respects minimum city spacing (15 pixels minimum on 512×256 grid = ~50km minimum separation).
    5. Name fulfillment check: After placement, verify that all atlas_city_names entries for this body have been assigned a position. Log a warning for any unplaced city.
    • Two-tier mismatch flagging: If a matched city-attractor pair has score < 0.35, log a WARNING (below expected quality). If score < 0.15, log an ERROR and flag for manual review. Generation proceeds in both cases; the flags are for content auditing, not hard blockers.
    • Output: Vec<CityPlacement { city_id, position: [row, col], attractor: AttractorType, score: f32 }> written to atlas_city_positions at build time.
  • Rationale: Greedy-first for large/locked cities ensures anchor cities (capitals, corp HQs, wiki-named cities) are placed at terrain features that match their lore role. Hungarian for medium cities finds the globally optimal assignment, not just locally optimal. Synthetic overflow prevents the algorithm from failing on bodies where city count exceeds natural attractor count (dense, flat worlds). The two-tier warning system enables content QA without blocking generation.
  • Ticket: #919, #925
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-195 (CompatibilityMatrix), D-207 (atlas_city_names), D-209 (GeographicAttractor — input), D-210 (terrain_modification_cost — input)

D-212: TerritorialStatus Priority-Ordered Derivation Algorithm

  • Date: 2026-05-01
  • Decision: Each Province (watershed-derived drainage basin, D-205) receives a TerritorialStatus value derived by priority-ordered classification at generation time:
    enum TerritorialStatus {
        CommissionControlled,  // Commission faction_influence ≥ 0.6 in this province
        CorpTerritory,         // Single corporation faction_influence ≥ 0.5
        ContestedZone,         // Two or more factions each ≥ 0.3, no dominant faction
        FrontierUnclaimed,     // No faction with influence ≥ 0.2
        IndigenousHeld,        // Cultural corridor has indigenous autonomy flag
        Derelict,              // population_density < 0.01 AND no faction ≥ 0.1
    }
    
    • Classification applies checks in priority order: CommissionControlled checked first, Derelict last. The first condition that is true sets the status.
    • placed_at_generation: bool flag on Province distinguishes classification at build time (true) from runtime re-classification during simulation (false). Build-time status is the starting state; simulation can change it, and the flag ensures the original classification is recoverable for reset/new-game scenarios.
    • Faction influence values are read from systems.db (economics tables) at build time using the same D-199 economic read pattern.
  • Rationale: Territory status is a high-level descriptor visible to the player on the Atlas overlay (D-191 §7, political zones overlay). It must be derivable from the generation inputs without runtime simulation state. The priority-ordered algorithm ensures clear, predictable classification — no ambiguous provinces. The placed_at_generation flag enables the game to show "how this province was at settlement time" vs. "how it is now."
  • Ticket: #921
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-191 (atlas overlay — political zones), D-199 (economic read set), D-205 (Province — this status is a field on it)

D-213: FoundingOrientation Enum and Spatial Grid Rotation

  • Date: 2026-05-01
  • Decision: FoundingOrientation describes the primary spatial axis of a city's original street grid, derived from the terrain feature that anchored the founding settlement. It controls the rotation of the district grid skeleton.
    enum FoundingOrientation {
        Coastal { facing_degrees: u16 },   // street grid perpendicular to coastline
        RiverAligned { bearing_degrees: u16 }, // street grid parallel to founding river
        TerrainFollowing,                  // grid rotated to follow local contours
        Cardinal,                          // grid aligned to N/S/E/W (commission-planned)
        Free { bearing_degrees: u16 },     // arbitrary bearing (pioneer settlements)
    }
    
    • facing_degrees and bearing_degrees are integer degrees 0359 (0 = North, clockwise). Integer to preserve D-010 determinism.
    • The founding orientation is derived from the matched attractor type (D-211): RiverMouthCoastal; RiverAligned; CoastalAccessCoastal; ValleyFloorTerrainFollowing; PlainCenter + Commission-controlled province → Cardinal; PlainCenter + other → Free.
    • The district skeleton generator (Phase 1) applies FoundingOrientation as the base rotation for the outermost district ring. Interior districts inherit the orientation unless overridden by a PoliticalArchetype modifier.
    • Hard constraint: Maximum ±45° deviation from the parent orientation per district (same limit as D-096 BlockPlacement.rotation_steps). Beyond ±45°, tile-based pathfinding produces movement artifacts.
  • Rationale: Street grids reflect the terrain and founding logic of the original settlement. Roman camps faced cardinal directions. River towns align with the river. Coastal cities face the water. Encoding this as a named enum rather than a raw angle makes the orientation legible in the data model and debuggable during generation.
  • Ticket: #914
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-096 (DistrictLayoutMode — inherits orientation), D-211 (attractor-matching — derives orientation), D-214 (PoliticalArchetype — may override orientation), D-215 (spatial arrangement patterns — uses orientation)

D-214: PoliticalArchetype Enum and Settlement Spatial Character

  • Date: 2026-05-01
  • Decision: PoliticalArchetype classifies a settlement's dominant power structure and its physical expression in district layout:
    enum PoliticalArchetype {
        Commission,   // Top-down Commission planning; rectilinear, institutional core
        Corporate,    // Corp-dominated; commercial density, restricted zones, campus blocks
        Pioneer,      // Self-organized; organic growth, mixed use, ad-hoc infrastructure
        Military,     // Garrison or fortification origin; defensible geometry, restricted perimeter
        Academic,     // University or research origin; campus-quad structure, green space
        Industrial,   // Factory-first; large-footprint industrial blocks, worker residential rings
    }
    
    • PoliticalArchetype is derived at generation time from TerritorialStatus (D-212) + economic_role: CommissionControlled province → Commission; CorpTerritoryCorporate; FrontierUnclaimedPioneer; military economic role → Military; research economic role → Academic; manufacturing + extraction → Industrial.
    • When multiple signals conflict (e.g., Commission-controlled manufacturing hub), TerritorialStatus takes precedence over economic_role for archetype derivation.
    • Spatial effect on district mix: See D-194. Each archetype applies weight multipliers to district type selection.
    • AttractorAssignment disambiguation: OrganicGrowth (a DistrictType value and also an EraCause value) is always unambiguous in context. On DistrictType, it means the district grew without a planning mandate. As EraCause, it means the era tag was acquired through organic settlement expansion rather than a discrete historical event. Both usages are permitted; the type system distinguishes them.
  • Rationale: Power structure should be legible in a city's spatial form without the player reading a wiki entry. Commission cities look different from Corporate cities look different from Pioneer cities — not just in palette, but in street geometry, district type distribution, and building scale. Encoding this as a named enum ensures the distinction is consistent across all generation code.
  • Ticket: #914
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-194 (district mix — archetype modifiers), D-212 (TerritorialStatus — primary input), D-213 (FoundingOrientation — archetype may override), D-215 (spatial arrangement patterns)

D-215: Five Explicit Political Archetype Spatial Arrangement Patterns

  • Date: 2026-05-01
  • Decision: Each PoliticalArchetype maps to one of five spatial arrangement patterns that govern district adjacency and the placement of landmark multi-block reservations:
    1. Radial core (Commission, Academic): Central landmark (civic square, institutional plaza, or university quad) surrounded by mixed-use rings. Transit spokes radiate outward. Districts are denser near center.
    2. Campus grid (Corporate): Restricted campus block occupies 24 blocks in the district interior. Commercial districts ring the exterior. Worker residential on periphery.
    3. Ribbon development (Pioneer, Industrial): Districts string along a linear feature (river, road, industrial rail). No dominant center. Mixed adjacency at every edge.
    4. Fortified perimeter (Military): Restricted and Secured districts at the edge of the footprint. Open access in the interior core. Single controlled access point per district edge.
    5. Hub-and-spoke (transit_hub economic role, any archetype): Transit district at center, all other district types accessible via direct corridors. Maximum 2-district travel between any two districts.
    • The arrangement pattern constrains block adjacency during Phase 1 skeleton generation. Specifically: the first 23 districts placed in a settlement follow the pattern. Later districts are constrained only by the road network, not by the pattern.
    • Arrangement patterns must vary in angular orientation per seed (not just position) — the same archetype's radial core must not always face the same direction across seeds.
  • Rationale: The 14 D-ready items from the generator-architecture workshop established that spatial arrangement should encode power structure. These five patterns are the minimal set to cover the 6 archetypes (Pioneer and Industrial share ribbon development; hub-and-spoke is a cross-archetype pattern for transit-primary cities). Pattern variation in angular orientation prevents players from pattern-matching settlement layout after the first playthrough.
  • Ticket: #914 (types), #899 (implementation — Phase 1 skeleton generator)
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-094 (spatial hierarchy — district sizes), D-194 (district mix — archetype modifiers), D-214 (PoliticalArchetype — pattern assignment)

D-216: BlockIrregularity from founding_age — Layout Age Character

  • Date: 2026-05-01
  • Decision: block_irregularity: f32 is a derived value on each block (range 0.01.0) that controls how much a block deviates from the district's canonical grid. It is computed from founding_age_years and PoliticalArchetype. The formula:
    base_irregularity = (founding_age_years / 1000.0).min(1.0)
    archetype_step = match archetype {
        Commission | Military => -0.3,   // suppresses organic deviation
        Corporate | Academic  => -0.1,
        Industrial            =>  0.0,
        Pioneer               => +0.3,
    }
    block_irregularity = (base_irregularity + archetype_step).max(0.05).min(1.0)
    
    • Minimum 0.05 is enforced — no block is perfectly regular, even new Commission-planned settlements.
    • block_irregularity feeds the BlockPlacement.offset magnitude in DistrictLayoutMode::Organic: max_offset_sim_tiles = (block_irregularity × 16.0) as i16.
    • An old Pioneer settlement (age 800+ years) can have block_irregularity ≈ 1.0, producing maximum ±16 sim tile offsets and ±45° rotations. A new Commission district (age < 50 years) will have block_irregularity ≈ 0.05.
    • All arithmetic uses integer-scaled intermediates wherever possible (age is integer years; archetype_step is stored as integer basis points internally). The f32 in the formula above is for documentation clarity only.
  • Rationale: Age is the single most reliable predictor of urban irregularity in the real world. Old cities that grew organically have crooked streets; new planned cities have grids. Encoding this as a formula rather than a lookup table allows continuous variation along the age axis while preserving the political meaning of the archetype modifier.
  • Ticket: #922
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-096 (DistrictLayoutMode::Organic — consumes block_irregularity), D-194 (district mix — founding_age is an input), D-214 (PoliticalArchetype — archetype_step source)

D-217: Tile Condition Thresholds (0.63 / 0.43 / 0.23)

  • Date: 2026-05-01
  • Decision: A tile's visual condition is derived from the district's prosperity_score (live pressure simulation value, 0.01.0) using four threshold bands:
    Band Condition prosperity_score range Tile visual state
    1 Intact > 0.63 Clean, undamaged, well-maintained
    2 Worn 0.43 0.63 Scuff marks, minor discoloration, partial repairs
    3 Cracked 0.23 0.43 Visible damage, incomplete repair, graffiti
    4 Broken < 0.23 Structural damage, debris, derelict appearance
    • Cache invalidation: A tile's condition only changes when prosperity_score crosses a threshold boundary (from band N to band N±1). This avoids per-tick visual updates. The simulation checks threshold crossings once per game-minute (D-031 day-phase tick rate).
    • Baseline floor: The block's EraCause sets a minimum condition floor:
      • Decay era: minimum Cracked (no tile in a Decay-era block is ever Intact or Worn without an active renovation event)
      • EmergencyExtension era: minimum Worn
      • All other eras: no floor (condition follows prosperity_score freely)
    • Phase 2 application: Chunk fill applies the baseline condition at fill time. Subsequent condition updates from simulation crossing thresholds are applied as ChunkMutations.tile_overrides.
    • Condition thresholds are authored constants, not computed. Any change to the thresholds (0.63 / 0.43 / 0.23) requires amending this D-record.
  • Rationale: Threshold-crossing invalidation is a standard visual LOD technique that avoids expensive per-frame recalculation. The four bands (Intact/Worn/Cracked/Broken) match the visual fidelity budget for the current art direction — more bands require more tile variants per palette. The era-based floor ensures that historical context is always visible: a Decay-era block cannot spontaneously look pristine from a prosperity spike alone.
  • Ticket: #923
  • Raised by: Generation cascade workshop (#897)
  • Cross-reference: D-100 (DamageOverlay — post-condition modification), D-194 (district mix — prosperity_baseline is the seed for prosperity_score)

D-218: WorldTier Enum Canonical Values (Epicenter/Regional/Backwater/Passage/Waypoint)

  • Date: 2026-05-01
  • Decision: The canonical WorldTier enum values are:
    enum WorldTier {
        Epicenter,  // Hub system. Full simulation. High faction pressure. Multi-district cities.
        Regional,   // Regional hub. 14 districts per city. Partial full-budget districts.
        Backwater,  // Small community. Dense isolated settlement. Full sim budget — NOT capped.
        Passage,    // Transit stop. Pass-through. ComplexityTier ceiling: Moderate.
        Waypoint,   // Not simulated until player approaches. ComplexityTier ceiling: Minimal.
    }
    
    • The values Peripheral, Connected, and Core used in generator.rs prior to Sprint 38 are incorrect — they were stubbed values that do not match the workshop design (workshop-outcomes.md §WorldTier and ComplexityTier). They must be replaced with the five canonical values above.
    • ComplexityTier ceiling per WorldTier:
      • Epicenter → Full
      • Regional → Full
      • Backwater → Full (critical: Backwater is network-insignificant, NOT budget-capped; isolated communities can be socially complex)
      • Passage → Moderate
      • Waypoint → Minimal
    • Source of truth: workshop-outcomes.md §WorldTier and ComplexityTier table (generator-architecture workshop, lead decision L-3).
    • All code referencing WorldTier::Peripheral, WorldTier::Connected, or WorldTier::Core must be updated to the canonical values. This includes generator.rs, any tests, and any serialized data that references these variants.
  • Rationale: The three-value stub (Peripheral/Connected/Core) was authored before the generator architecture workshop established the five-value canonical model. The mismatch between the code and the design means any generator code built against the stub types would need rewriting anyway. Correcting it now before the Phase 1 implementation work begins eliminates that rework. The Backwater full-budget exception is architecturally significant: dense isolated communities (mining towns, research outposts) should be as socially rich as regional hubs — their isolation is their drama, not their limitation.
  • Ticket: #900 (bug fix), #912 (full enum implementation)
  • Raised by: Generation cascade workshop (#897). Original workshop-outcomes.md §WorldTier (generator-architecture workshop, lead decision L-3).
  • Cross-reference: D-096 (DistrictLayoutMode — WorldTier is a DistrictSkeleton field), D-097 (GuaranteeAuditResult — tier-conditional guarantees), D-201 (spatial hierarchy — WorldTier assigned at Tier 2 System level)

79 decisions (D-001 through D-218, excluding gaps). Last updated: 2026-05-02 (D-218 — WorldTier canonical values, generation cascade workshop Sprint 38)