Five density classes (Frontier→Compressed) driven by four pressures (utility cost, terrain, economic gravity, political). Amends D-201 tile scale to 1m with 0.5m subtile. Calibrated against real-world settlements (Kiruna, Uzès, Kanazawa, Paris). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
177 KiB
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:
- 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.
- 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.
- 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.
- 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
ObserverSnapshotdata, 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.
- Eliminates entire risk categories: gdext pre-1.0 API churn, Godot version ABI breakage, FFI thread safety (
- 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.SimBridgetrait: abstracts transport.LocalBridge(subprocess, channels) andNetworkBridge(TCP, MessagePack) implement the same interface.
- Protocol versioning policy:
PROTOCOL_VERSIONgates 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 whateverObserverSnapshotthe 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:
- Build Rust simulation as standalone binary (testable via terminal/logs)
- Build Godot renderer as standalone project (hardcoded test data)
- 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):
- Rust test organization = Hybrid.
#[cfg(test)]for unit tests inside modules +tests/directory for integration tests. Both viacargo nextest run. - Godot test framework = gdUnit4 (changed from GUT). Native JSON output, stable headless via
GdUnitCmdTool,GdUnitSceneRunnerfor scene lifecycle tests, organizational maintenance. - 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).
- 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. - Test runner tooling.
cargo-nextest(Rust) + gdUnit4 (Godot) + bash wrapper scripts intest/directory, whitelistable for agent use. - Test output format = JSON summary. Consistent schema across all runners (suite, total, passed, failed, failures array). JUnit XML as secondary CI format.
- #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. - 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.
- Rust test organization = Hybrid.
- 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
KnowledgeGraphcomponent 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 viaEntityRegistryresource for bidirectionalStableId <-> Entitylookup. Knowledge updates flow through event-driven architecture: perception systems emitKnowledgeEventtoKnowledgeEventQueueresource, knowledge update system drains queue and writes toKnowledgeGraphcomponents. Basic decay runs once per game-minute (every 10 ticks per D-031), downgrading confidence levels based onlast_observed_tickage against configurableDecayThresholds. - Sprint 2 scope: Full data structures + direct observation flow + basic decay + observer snapshot integration (#112). Deferred to Sprint 3+: NPC-to-NPC gossip,
ToldBy/Inferredsource generation,Contradictedstate detection,Stalestate logic, knowledge-driven dialogue filtering, monologue triggering, misinformation. - Canonical reference: Full Rust struct definitions at
docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.mdPart 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:surfaceavailable at any level,realat KnowsOf+,secretat KnowsDetails+. - KnowledgeState for contradiction detection: THE FRIEND arc (D-034, D-039 wow moment #3) requires detecting when a
ToldByentry conflicts with aDirectObservationentry. Both entries receiveContradictedstate, triggering monologue event and relationship state shift (PersonOfInterest). Sprint 2 only usesActivestate; 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_ecsEntity(generational index).EntityRegistrymaintains 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.yamland loaded via a dedicated GDScript autoload singleton (UIStrings). UI strings are NOT hardcoded as GDScript constants inclient/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:
- Music — future/empty in v0.1. Reserved for diegetic Meridian music in social spaces.
- Ambient — station hum (D-038 asset 1) + zone overlays (assets 2-4). Continuous soundscape.
- World SFX — NPC footsteps, doors, environmental events. Diegetic world sounds not caused by player.
- Player Actions — player footsteps (assets 5-6), future: combat sounds, item interactions. Sounds player directly causes.
- 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.savin the active game dir). - F6 = quickload (loads
quicksave.savfrom 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_speedfield in ObserverSnapshot. Client readssim_speedand 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 (0–3 steps, 15° increments), variable street width (0.75–2.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
GuaranteeAuditResultwith 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 StructuralFilldepending 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.
DamageOverlaystruct:overlay_type(GasExplosion | Fire | Structural { collapse_direction } | Flooding),epicenter: ChunkLocalPos,radius: f32,intensity: f32,scatter_seed: u64(variation within zone only).RegenerationStrategyenum:LocalOverlay(DamageParameters)for in-playthrough events (MANDATORY),SoftReseed { seed_modifier: u64 }at scenario boundaries only,FullReseedat 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:#c8d8f0open-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 (1–2 z-levels, surface + roof/mezzanine), S2 (3–10), S3 (11–30), S4 (30+). Shadow length is the primary height signal in top-down view (2–40 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 assignRooftopConfig: 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.Dockedstate requiresdock_position,connected_chunk: Option<ChunkCoord>,docked_since: SimTick,scheduled_departure: Option<SimTick>.scheduled_departuremust be populated by the generator; vessels without departure schedules are an error state. Cultural grammar:TransitSocialModifierwithTransitVariant(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 indocs/workshops/generator-architecture/round-4-notes.md§5. Memory: ~0.5–4KB 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
Idlemovement state is the canonical primitive for player-owned stationary installations (space stations, orbital platforms, parked vessels as permanent bases). A MobileChunk inIdlewith noscheduled_departureis 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.
LocalOverlayis the mandatory modification strategy for all events that occur while the player is present.SoftReseedandFullReseedare 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: u8on DistrictSkeleton) remain unsigned — they represent "how many floors", which is always positive. The distinction:base_zis "where does the bottom floor start" (can be negative for basements/subterranean spaces),z_levelsis "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
u8type contradicts the design intent. A deep mine is structurally an inverted skyscraper withbase_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
MobileChunkIdlemovement 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, andLocalOverlaymodification system as vessels. Player construction within a MobileChunk (building rooms, placing equipment) requires the DLC construction system to emit validLocalOverlaymodifications — 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
Idlestate 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 —
Idlestate), 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:
MultiBlockReservationwith negativebase_z(D-110) - Deep mines (shaft-style): Downward
MultiBlockReservation, lazy-loaded viaZLevelLoadState - Deep mines (cave network): Organic-mode district (D-096) with mine-specific template
- Player base in existing building:
LocalOverlaymodifications (D-100/D-109) - Player base as hidden bunker:
MultiBlockReservationwith negative z, generated at world-gen - Player-built space:
LocalOverlay+ construction system (DLC scope) - Stationary installation:
MobileChunkinIdlestate (D-111) - Vessel interior:
MobileChunk(D-108)
- Basements / sub-levels:
- 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:
LocalOverlayis 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/Rstrings 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
TileKindenum (Floor,Wall,Door,Object) and a walkability bool.TileCellinWalkabilityMapstores{ 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:
- Doors — access lists (who can open), open/closed state, locked/unlocked. Currently no tile-level door data;
TileKind::Doorexists but carries no properties. - Containers — contents, capacity, searched state. Currently handled by entity
ObjectType::Containeron separate entities, not tiles. Containers should remain entities, not tile properties. - Damage state —
DamageOverlay(D-100) modifies tiles post-generation. Damage needs to degrade tile properties (walkability, visual, material) without replacing the base tile type. - 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.
- Trigger zones — tile-level triggers for entry/exit events (zone transitions, alarms, dialogue triggers). Currently handled by
ZoneMapat zone granularity, not per-tile. - Material properties — footstep sound, movement speed modifier, surface type for particle effects. Currently all tiles produce the same footstep sound.
- WallBackside (D-099) — structural classification behind wall surfaces. Already defined as an enum but not yet integrated into tile data.
- Doors — access lists (who can open), open/closed state, locked/unlocked. Currently no tile-level door 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), optionalwall_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,Rare reserved palette keys that map to current behavior. New tile types use additional characters or a separate palette layer. - Sparse override map (YAML):
overrideskey 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:
TilePaletteresource:BTreeMap<char, TileType>loaded at startup. Immutable after load.TileCellextended:{ palette_id: char, walkable: bool, kind: TileKind, material_id: u16 }. Material ID is a compact index into the palette's material table.TileOverrideMapresource:BTreeMap<(i32, i32, i32), TileOverride>for per-tile overrides. Sparse — only tiles with overrides consume memory.- ECS queries:
WalkabilityMapremains the primary interface for movement/pathfinding (unchanged API).TilePaletteprovides material/visual data when needed (snapshot construction, sound system).TileOverrideMapprovides door state, access lists, damage overlays.
- Tile palette (YAML, per-district or global): defines tile types by string ID. Each type specifies:
- 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:
ContentPluginloads palette YAML first, then location tiles. Theapply_location_tiles()function resolves each character via palette lookup instead of the current hardcoded match. Unknown characters fall back toFloorwith a warning (same as current behavior). Overrides are loaded after tiles and applied toTileOverrideMap. - Migration effort for existing locations (5 files):
- Zero-migration path: The default palette defines
F/W/V/Rwith 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 withoutpalette: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.
- Zero-migration path: The default palette defines
- 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.
WalkabilityMapAPI 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
PlatformInfoautoload (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 callOS.*directly. PlatformInfo owns: power state (withPowerProfileenum: FULL, BATTERY, POWER_SAVER), memory queries, platform identity, and platform-dependent file paths. Power state is polled on a 30-second timer with apower_profile_changedsignal; memory is refreshed on demand. ThePowerProfileenum 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 singleSprite2Dper entity. Under this decision,EntityRendereris extended to instantiate aCharacterCompositorscene (Node3D subtree) instead. The compositor API is specified indocs/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_uppervariant 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 #746–751, 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
Themeresource. 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:
ImplantPanel—PanelContainerwith theme override, serves as root for any implant overlayImplantHeader—HBoxContainer(title label + subtitle label)ImplantSeparator—HSeparatorwith theme overrideImplantDataRow—HBoxContainer(key label + value label, two-column)ImplantTextBlock—RichTextLabelfor wrapping narrative textImplantStatusBadge— colored dot + labelImplantProgressBar— two-tone, no gradientsImplantTabRow— uppercase labels, underline-active patternImplantExpandable— 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
HudGroupsautoload. 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. Emitsgameplay_occludedsignal when a fullscreen app covers gameplay — renderers extendGameplayRendererbase 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_summaryfield insystems.dband all references throughout the codebase are renamed toplanet_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_summary→bodies.planet_class), schema SQL, all Rust atlas code, wiki table headers (Biome→Class), 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/mapat 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.pyline 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 3–4: 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-voicebinary) - 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
Texture2Dwith 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 a512 × 256storage 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 bysettlement_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, whererow ∈ [0, h)andcol ∈ [0, w)(row is the first axis to match NumPy convention and the flood-fill / A* / cost-grid code thattooling/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}—kindiscapitalorcity;nameis empty when awaiting gemma_naming.py (#833).roads[]:{id, name, kind, path: [[row, col], …]}—kindiscommercialby default for generated roads; hand-authored roads usehighway,rural, etc.railroads[]: same shape asroads[]; generated defaultkindispassenger_freight.pois[]:{id, name, kind, center: [row, col]}— generated POIs arekind: "transit"; hand-authored POIs useinstitutional,cultural,corporate, etc.rivers[]:{id, name, path: [[row, col], …]}oceans[]:{id, name, kind, center: [row, col], area_fraction}wherekindislake|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 equirectangularlat°N/S, lon°E/Wstring 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 bysettlement_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— reusesplanet_simulation.simulate(), addscity_placement.py,infrastructure_gen.py,gemma_naming.py,markers_writer.py make atlas-generateruns 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
- Navigation chain works end-to-end (Reach → system → planet → regional)
- Regional map content complete for all inhabited bodies
- Population-scaled depth (core systems rich, frontier sparse)
- City data panel works (click any city, see data + economics link)
- Economics panel integration works (link opens Phase 2 panel filtered to node)
- All 9 MVP overlays present and functional
- Atlas is read-only (no verbs execute from map)
- 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
versionfield, thePROTOCOL_VERSIONconstants on both server (server/src/bridge/types.rs) and client (client/scripts/protocol/protocol.gd), and the version-mismatch guard inProtocol.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 inMessagepack.decode()still rejects malformed payloads. - Raised by: Jeroen, sprint-36 client triage. Triggered by stale
test_protocol_version_is_19assertions 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:
- 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. - 10×9 economic multiplier table — rows are 10
economic_rolevalues (manufacturing, financial, agricultural, extraction, service_mixed, institutional, transit_hub, research, military, residential); columns are 9DistrictTypevariants. Each cell is a weight multiplier (0.0–3.0) applied to that district type's base probability for cities of that economic role. - Political archetype modifiers —
PoliticalArchetypeshifts 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.
- Population tier guarantees — minimum district counts enforced by city size. Population tier is
- 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
GeographicAttractoris a terrain feature that increases city placement score at nearby positions. SevenAttractorTypevariants:RiverMouth,CoastalAccess,RiverCrossing,ValleyFloor,PassEntrance,LakeShore,PlainCenter. ACompatibilityMatrixis a 10×7 scoring table (10economic_rolevalues × 7 attractor types) whose cells are float weights (0.0–3.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 aSettlementClassthat 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). NameLockedsettlements bypass both thresholds — they are always simulated regardless of population (handles narrative-significant small towns).EconomicTriggeredsettlements collapse to ghost state when their triggering economic condition lapses (e.g., a mining outpost depopulates when the mine is exhausted).OrganicGrowthsettlements are not inatlas_city_namesat generation time; they are written to the table during simulation when a settlement emerges organically.
- Active threshold (applies to
- 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.0–1.0, used as economic pressure state seed) is derived at generation time from four components:- Economic role base — lookup per
economic_rolevalue: 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. - Population log-scale bonus —
0.04 × floor(log10(pop / 1_000_000 + 1)), capped at +0.12. Larger cities are generally more prosperous. - 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).
- 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_baselineis 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.
- Economic role base — lookup per
- 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 1–2 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.dbdata (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, andfaction_influencefrom the snapshot. These are read-only inputs to Phase 1 skeleton classification and Phase 2 condition application.
- Prohibited: Generator code must not modify
- 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:economic_role— primary function of the city (determines DistrictType distribution via D-194)prosperity_baseline— starting economic health (0.0–1.0, see D-197)population— city population (determines ComplexityTier ceiling, BlockSkeleton density)dominant_faction— faction with highestfaction_influenceat this location (affects Institutional and Restricted district bias)founding_age_years— years since settlement founding (drives BlockIrregularity via D-216, era distribution)settlement_class—SettlementClassenum value (D-196, determines whether to generate at all)
- Fields 1–5 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:
- Build-time (Python pipeline): Runs
make regen-db. Producessystems.dbtables includingatlas_body_heightmaps,atlas_city_names,atlas_province_boundaries,body_radius_km. Output is a static artifact committed to the repo. Never runs during gameplay. - 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
BodyWorldStatecache (D-203). Transparent to main tick thread. - 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
BodyWorldStatecache (always populated before this tier runs).
- Tier boundary rules:
- Build-time outputs are read-only at runtime.
- Runtime-background tasks read from
systems.dband write toBodyWorldStateonly. - Runtime-on-demand reads from
BodyWorldStateand writes to the active ECS world (chunk tile data, NPC spawns). - No tier may write to a higher tier's outputs. No circular dependencies.
CityGenerationContextstruct (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 (0–7) footprint_radius_km: f32, founding_orientation: FoundingOrientation, world_tier: WorldTier, } - Build-time (Python pipeline): Runs
- 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 ~50–500km Province boundaries (watershed-derived, D-205), biome zones 5 Settlement ~1–30km 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 6–8 are locked by D-094 (district spatial hierarchy). This decision formalizes Tiers 1–5 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
SettingTypeenum onDistrictSkeletonis 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 6–8), 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.dbas a BLOB in theatlas_body_heightmapstable. 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')) );datais afloat32little-endian BLOB. Size:width × height × 4bytes. Canonical: 512×256×4 = ~512KB per body.- Values are normalized elevation in [0.0, 1.0].
sea_levelis 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_heightmapsbuild-time step in the asset pipeline (D-191 §9 pipeline order). It is read-only at runtime.
- Rationale: Storing heightmaps in
systems.dbkeeps 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:
BodyWorldStateis a BevyResourceholding the Layer 1–2 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 1–2 data includes: processed heightmap (float32 grid, ~512KB pre-downsampled to ~8KB working resolution), river network (
RiverNetworkstruct: river cells, confluences, mouths), drainage basin polygons, attractor list, province boundary references. - Eviction policy: On cache overflow, evict the body with the oldest
last_accessedtimestamp. 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 REALcolumn is added to thebodiestable insystems.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_kmfield onCityGenerationContext(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_classlookup table with canonical radii:super_earth: 8,000 kmearth_like: 6,371 kmsub_earth: 4,500 kmocean_world: 6,500 kmarid: 5,800 kmice_world: 3,000 kmgas_giant: 50,000 km (no settlements)moon: 1,737 kmother/ unknown: 6,371 km (Earth default)
- Fallback is applied at query time, not stored back. The column remains NULL until authoritative data is available.
- Compute
- 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 asmarkers.json(D-191 §8 canonical format). area_pctis 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: 4–12 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_boundariesat generation dispatch time and cached inBodyWorldStateasdrainage_basins(D-203). They are not re-computed at runtime.
- Boundaries are stored as pixel-space polylines in the same
- 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 fromsystems.db) scans NPC dialogue output and news ticker text. When a scan match hits, the referenced body is queued atLowpriority 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
GenerationCompleteevent to the main tick thread via acrossbeamchannel. The main thread drains this channel once per tick.
- Thread count:
- 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_namestable replaces the authored city positions inmarkers.json. Going forward,markers.jsonfiles contain only topographic features (rivers, oceans, mountain ranges — per D-191 §8 canonical format). City positions, road networks, and rail networks are NOT authored inmarkers.json; they are generated from the terrain data and stored inatlas_city_namesand 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' );nameand 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_idlinks to thecorporationstable when a city is a corporation's headquarters or major hub city.reserved = 1rows are scenario-specific cities that must not be relocated by the generation algorithm; the generator places other cities around them.markers.jsonauthored city data (hand-written city center positions in the 6 hand-authored templates: Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade) is migrated toatlas_city_namesand treated asreserved = 1rows. 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 cellsconfluences: Vec<(u16, u16)>— positions where two or more rivers mergemouths: 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
RiverMouthattractors). The flow accumulation threshold of 200 was chosen empirically against the Lendel heightmap to produce ~8–15 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
AttractorTypetags are extracted from the heightmap + river network and stored asVec<GeographicAttractor>inBodyWorldState. Each attractor has a position[row, col]and astrength: f32(0.0–1.0) derived from local terrain quality.- Extraction rules per type:
RiverMouth: cells inriver_network.mouths. Strength =flow_accumulation[cell] / max_flow_accumulation(normalized). Always high-value.CoastalAccess: cells within 3 pixels of a sea/ocean polygon (fromoceans[]in markers.json), not alreadyRiverMouth. 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 10–60% 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 tolakepolygons 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
SubBiomeVarianton the attractor for use by the ZonePalette modifier system (D-101).
- Extraction rules per type:
- 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 asub_biome: SubBiomeVarianttag that classifies the local terrain more finely than the top-levelSettingType. This drives two systems: ZonePalette modifier selection (which visual variant to use) andterrain_modification_cost(how expensive it is to build infrastructure at this location).SubBiomeVariantvalues: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
BodyWorldStatealongside 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_costgives 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 fromatlas_city_names, settlement placement runs a five-phase matching pipeline:- Score matrix build: Compute a
city_count × attractor_countscore matrix. Each cell =CompatibilityMatrix[economic_role][attractor_type] × attractor.strength × (1.0 / terrain_modification_cost). - Tier A greedy assignment: For each city with
SettlementClass::NameLockedor population ≥ 1,000,000, assign the highest-scoring unoccupied attractor using greedy selection. These cities must be placed first to anchor the spatial layout. - Hungarian algorithm for Tier B+C: Apply the Hungarian algorithm to the remaining cities (population 50,000–999,999) and remaining attractors. Produces optimal global assignment maximizing total score.
- Synthetic attractor overflow: Cities that cannot be matched to a real attractor (attractor pool exhausted) receive a synthetic
PlainCenterattractor generated at a position that respects minimum city spacing (15 pixels minimum on 512×256 grid = ~50km minimum separation). - Name fulfillment check: After placement, verify that all
atlas_city_namesentries 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 anERRORand 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 toatlas_city_positionsat build time.
- Score matrix build: Compute a
- 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 aTerritorialStatusvalue 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:
CommissionControlledchecked first,Derelictlast. The first condition that is true sets the status. placed_at_generation: boolflag onProvincedistinguishes 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.
- Classification applies checks in priority order:
- 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_generationflag 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:
FoundingOrientationdescribes 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_degreesandbearing_degreesare integer degrees 0–359 (0 = North, clockwise). Integer to preserve D-010 determinism.- The founding orientation is derived from the matched attractor type (D-211):
RiverMouth→Coastal;RiverAligned;CoastalAccess→Coastal;ValleyFloor→TerrainFollowing;PlainCenter+ Commission-controlled province →Cardinal;PlainCenter+ other →Free. - The district skeleton generator (Phase 1) applies
FoundingOrientationas the base rotation for the outermost district ring. Interior districts inherit the orientation unless overridden by aPoliticalArchetypemodifier. - 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:
PoliticalArchetypeclassifies 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 }PoliticalArchetypeis derived at generation time fromTerritorialStatus(D-212) +economic_role:CommissionControlledprovince →Commission;CorpTerritory→Corporate;FrontierUnclaimed→Pioneer; military economic role →Military; research economic role →Academic; manufacturing + extraction →Industrial.- When multiple signals conflict (e.g., Commission-controlled manufacturing hub),
TerritorialStatustakes precedence overeconomic_rolefor archetype derivation. - Spatial effect on district mix: See D-194. Each archetype applies weight multipliers to district type selection.
AttractorAssignmentdisambiguation:OrganicGrowth(aDistrictTypevalue and also anEraCausevalue) is always unambiguous in context. OnDistrictType, it means the district grew without a planning mandate. AsEraCause, 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
PoliticalArchetypemaps to one of five spatial arrangement patterns that govern district adjacency and the placement of landmark multi-block reservations:- 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.
- Campus grid (Corporate): Restricted campus block occupies 2–4 blocks in the district interior. Commercial districts ring the exterior. Worker residential on periphery.
- Ribbon development (Pioneer, Industrial): Districts string along a linear feature (river, road, industrial rail). No dominant center. Mixed adjacency at every edge.
- 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.
- 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 2–3 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: f32is a derived value on each block (range 0.0–1.0) that controls how much a block deviates from the district's canonical grid. It is computed fromfounding_age_yearsandPoliticalArchetype. 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_irregularityfeeds theBlockPlacement.offsetmagnitude inDistrictLayoutMode::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 haveblock_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.0–1.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_scorecrosses 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
EraCausesets a minimum condition floor:Decayera: minimum Cracked (no tile in a Decay-era block is ever Intact or Worn without an active renovation event)EmergencyExtensionera: 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.
- Cache invalidation: A tile's condition only changes when
- 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
WorldTierenum values are:enum WorldTier { Epicenter, // Hub system. Full simulation. High faction pressure. Multi-district cities. Regional, // Regional hub. 1–4 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, andCoreused 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→ FullRegional→ FullBackwater→ Full (critical:Backwateris network-insignificant, NOT budget-capped; isolated communities can be socially complex)Passage→ ModerateWaypoint→ 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, orWorldTier::Coremust be updated to the canonical values. This includes generator.rs, any tests, and any serialized data that references these variants.
- The values
- 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
Backwaterfull-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)
D-220: Settlement Density Model — Tile Scale, Density Classes, and Vertical Pressure
-
Date: 2026-05-03
-
Decision: Settlement footprint, building height, and basement depth are all driven by a single density scalar on each settlement, derived from population, available land, terrain constraints, and economic pressure. This decision establishes the tile scale, the density classification system, and the rules for how density shapes the built environment.
Tile scale (amends D-201 Tier 0):
Unit Dimensions Real-world size Notes Subtile 1×1 0.5m × 0.5m Rendering unit — character positioning, furniture placement, visual detail Tile 1×1 1m × 1m Spatial unit — generation pipeline, pathfinding, district grids Chunk 64×64 tiles 64m × 64m Streaming unit (was 32m at 0.5m/tile; now 64m at 1m/tile) Block 128×128 tiles 128m × 128m 2×2 chunks District 512×512 tiles 512m × 512m = 0.262 km² 4×4 blocks Each tile contains 2×2 subtiles. The generation cascade operates in tiles. The renderer subdivides to subtiles for visual fidelity. All higher tiers (Province, Area, Body) remain as defined in D-201.
Density classes:
Density is measured in people per km² of settlement footprint. It is the primary driver of settlement character — not population alone. The same population at different densities produces completely different settlements.
Class Density (ppl/km²) Horizontal character Vertical character Infrastructure cost Who builds at this density Frontier ~400 Large gaps between structures, open ground dominant, private roads 1 floor, no basement Very high per-capita (long utility runs, private maintenance) The very rich (villa estates, corp executive compounds, prestige vanity builds) OR the very poor (pre-utility squatter settlements, homesteads with no services). Frontier density is a luxury or an absence, never the default. Settled ~1,500 Planned grid, moderate spacing, shared streets, visible utility corridors 1-2 floors, optional basement Efficient — the utility-cost-optimized default Default for most Reach colonies. Every meter of sewer, power conduit, and road is new capital expenditure on a virgin world. Settled density is what you get when infrastructure cost constrains the plan. Kiruna is the archetype. Established ~3,000 Mixed residential, shared walls appearing, streets narrowing, green space intentional not incidental 2-3 floors, basement common Amortized — mature infrastructure absorbing incremental growth Mature settlements. Second-generation growth over an existing utility grid. The settlement has existed long enough for the infrastructure investment to pay off and densification to begin. Tiel, Périgueux. Dense ~6,000 Shared walls dominant, narrow streets, minimal private outdoor space, commercial ground floors 3-6 floors, 1-2 basement levels (parking, storage, utilities) High per-area but efficient per-capita Economic hubs. Transit nodes, extraction chokepoints, terrain-constrained sites where the jobs pull people in faster than the settlement can spread. Kanazawa, Uzès. Density is a signal that economic gravity is compressing the settlement. Compressed ~15,000 Vertical dominant, street canyons, multi-use buildings, public space carved from built volume 6-12+ floors, 2-3 basement levels (metro, parking, utilities, subsurface logistics) Very high per-area, requires vertical infrastructure (elevators, pressurized systems, multi-level utilities) Extreme economic pull. Major corp HQs, gate-adjacent trade hubs, capital worlds. Rare in the Reach. The land itself is too valuable for anything but vertical. Paris, Amsterdam inner ring. What drives density — the four pressures:
-
Utility cost (compressing force). The dominant pressure on new colonies. Infrastructure is expensive per meter. This pushes settlements toward Settled as the floor, not Frontier. Every colony pays this tax unless someone absorbs the cost deliberately.
-
Economic pull (compressing force). Jobs, trade, resources, transit — anything that draws people to a specific location faster than the settlement can grow outward. The stronger the pull, the higher the density. Corp HQ worlds, gate-adjacent systems, extraction sites all have high economic pull. This is the force that pushes settlements from Settled through Dense to Compressed.
-
Terrain constraint (compressing or dispersing force). Coastal strips, canyon floors, mountain passes compress settlements along one axis (Bisbee: 2.5km × 300m). Open plains allow radial spread. River valleys elongate. The terrain doesn't set the density class directly — it shapes the footprint geometry within that class.
-
Wealth/status (dispersing force — at a premium). The only force that pushes density DOWN below the utility-cost floor. Low density in an otherwise dense settlement is conspicuous consumption of space — private utility runs, private road maintenance, private security perimeters. Villa estates, executive compounds, prestige campuses. This is Frontier density as luxury, not as poverty.
The U-shaped wealth curve:
Density has a U-shaped relationship with wealth. The very rich and the very poor both live at low density — but for opposite reasons. The rich can afford the infrastructure cost of space. The poor live where no infrastructure exists. Everyone in between clusters at Settled-to-Dense because that's what utility economics demands.
The player reads this instantly: low-density district on a dense world = money. Low-density district on a frontier world = nothing built yet. Same tile spacing, completely different story — and
prosperity_baseline(D-197) disambiguates. High prosperity + low density = wealth. Low prosperity + low density = absence.Density → vertical pressure mapping:
Density drives building height and basement depth via simple thresholds. These are tendencies, not hard limits — individual buildings vary, but the district-level character follows the pattern.
Density class Typical above-ground floors Typical basement levels Vertical pressure description Frontier 1 0 Single-story. Horizontal only. Settled 1-2 0-1 Mostly single-story, occasional two-story. Basement where terrain allows. Established 2-3 1 Two-story default. Three-story at intersections and commercial streets. Basements standard. Dense 3-6 1-2 Multi-story default. Ground-floor commercial. Structured parking appears underground. Compressed 6-12+ 2-3 Highrises. Street canyons. Multi-level subsurface: parking, metro, utilities, logistics. The transition from "buildings grow out" to "buildings grow up" happens at Dense. Below Dense, the settlement solves population pressure by adding more districts horizontally. At Dense and above, horizontal expansion is constrained (by terrain, by utility cost, by land value) and vertical growth begins.
The transition from "no basement" to "buildings grow down" happens at Established. Below Established, basements are optional amenities. At Established and above, basements become structural necessities — storage, utilities, parking that can't fit at grade.
Density derivation:
The density scalar for a settlement is derived from existing systems.db data:
base_density = SETTLED_DEFAULT // 1,500/km² — the utility-cost floor // Economic pull increases density if economic_role in [transit, commercial, manufacturing]: base_density *= 1.5–2.0 if is_corp_hq_world: base_density *= 1.5 if gate_adjacent: base_density *= 1.3 // Terrain constraint modulates if terrain_constraint_factor > threshold: // coastal strip, canyon, etc. base_density *= 1.2–1.8 // Wealth disperses (for specific districts within a settlement) if district.prosperity_baseline > 0.8: effective_density *= 0.4–0.6 // villa district within a dense city // Clamp to class boundaries density_class = classify(effective_density)The exact multipliers are tunable. The structure — base at Settled, economic pull compresses, wealth disperses — is the locked architectural decision.
Settlement footprint formula:
footprint_km2 = population / effective_density district_count = ceil(footprint_km2 / 0.262) grid_side = ceil(sqrt(district_count))Not all districts within the grid are built. A Settled-density village of 2,000 people covers ~1.3 km² = 5 districts in a 3×3 grid, but only 5 of 9 grid cells contain structures. The remaining 4 are open ground, agricultural land, or wilderness. The fill ratio increases with density class:
Density class Typical grid fill ratio What fills the empty cells Frontier 20-40% Wilderness, private grounds, no development Settled 50-70% Agricultural plots, utility corridors, planned expansion reserves Established 70-90% Parks, low-density transition zones, institutional grounds Dense 90-100% Nearly full — remaining gaps are intentional public space Compressed 100% Full. No gaps. Every cell is built. Reference calibration (real-world sanity check):
Real settlement Pop Real area Real density Model class Model area Match quality Wadenoijen, NL 695 0.10 km² 6,950/km² Dense 0.12 km² Close Kerk-Avezaath, NL 1,250 0.40 km² 3,125/km² Established 0.42 km² Close Bisbee, AZ 5,000 1.50 km² 3,333/km² Established 1.67 km² Close Kiruna, SE 17,000 11.40 km² 1,491/km² Settled 11.33 km² Near-exact Périgueux, FR 30,000 9.82 km² 3,055/km² Established 10.00 km² Near-exact Tiel, NL 39,000 13.00 km² 3,000/km² Established 13.00 km² Exact Kanazawa DID, JP 466,000 59.00 km² 7,898/km² Dense 77.67 km² Reasonable Paris proper, FR 2,040,000 105.00 km² 19,429/km² Compressed 136.00 km² Reasonable The model is calibrated to real-world settlements. Discrepancies come from the square-grid assumption (real settlements elongate along rivers, coasts, and roads) and from the discrete density class boundaries (continuous real densities mapped to five bins).
Interaction with other D-records:
- D-194 (district mix): District types are still determined by the three-component mix. Density determines how much space those districts occupy and how they're spaced.
- D-197 (prosperity_baseline): Disambiguates the wealth curve. Same low density reads as luxury (high prosperity) or absence (low prosperity).
- D-201 (spatial hierarchy): Tile scale amended from 0.5m to 1m with 0.5m subtile. Chunk real-world size doubles from 32m to 64m. All higher tiers unchanged.
- D-215 (spatial arrangement patterns): The five archetype patterns operate within the footprint that density determines. CompanyTown spine is longer at Settled density, compressed vertically at Dense density.
- D-216 (BlockIrregularity): Irregularity affects street grid geometry within a district. Density affects building height and spacing within that geometry. Orthogonal — a Dense district can be Organic (irregular streets, tall buildings) or Grid (regular streets, tall buildings).
-
-
Rationale: Population alone cannot determine settlement character. A village of 2,000 at medieval European density (Kerk-Avezaath, 3,200/km²) and a homestead spread of 2,000 at frontier density (400/km²) produce a 10× footprint difference. In the Reach, utility cost on virgin worlds is the dominant constraint, making Settled (~1,500/km²) the natural floor. The density scalar unifies footprint calculation, vertical pressure, and inter-building spacing into a single derivable parameter. The tile/subtile split preserves 0.5m visual fidelity for character movement and furniture placement without inflating the generation pipeline's spatial grid.
-
Ticket: (pending — implementation tickets to be created when cascade Tier 3/4 work begins)
-
Raised by: Jeroen + Claude, design session 2026-05-03. Grounded against real-world settlement data (Wadenoijen, Kerk-Avezaath, Uzès, Tiel, Périgueux, Kiruna, Kanazawa, Paris, Bisbee, Alice Springs, Brasília).
-
Cross-reference: D-194 (district mix), D-197 (prosperity_baseline), D-201 (spatial hierarchy — tile scale amendment), D-215 (spatial arrangement patterns), D-216 (BlockIrregularity)
-
Dissent: None
80 decisions (D-001 through D-220, excluding gaps). Last updated: 2026-05-03 (D-220 — Settlement Density Model, design session)