Six sections: connection model (attach SR_PORT/9876 ~500ms else spawn --port 0 + LISTENING parse, auto-attach-else-spawn); Reader connection class (ConnectionRole on StartupMessage, serde-default Player; no character spawn, NO ObserverSnapshot, permitted-message matrix, drop-not-disconnect, 0-1 Player + 0-N Readers, Player-only shutdown-on-disconnect); app shell (dedicated atlas_standalone.tscn, trivial make atlas target); data browser (separate implant/browser app per Jeroen, six registry entities v1, WIRE-ONLY extending the T-949 precedent — no client SQLite); save/load seam (seed picker now, save picker slot Phase 5+); trading seam (TradingReader superset, idempotency tokens, loopback-only auth assumption recorded). Tyre (lead) + Oscar (connection sections, his activation). Validate + sync clean (388 records, 0 broken). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
426 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 T-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.
- T-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 (T-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 T-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 T-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: T-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 T-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: T-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-238), 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-238)
- 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.
- Amendment (2026-07-06, T-1088): the "client-side Tween interpolation (100–150 ms)" sentence is scoped to the 2D renderer henceforth. A fixed-duration tween conflicts with the D-053 stance cadences (200–800 ms/step) — dash-then-stand stutter at every stance below Sprint. The 3D presentation layer interpolates per D-248 (per-leg constant velocity keyed to the stance throttle). Everything else in this record (server-authoritative discrete tiles, octant-only facing on the wire, TilePresence occupancy) is unchanged and remains binding for both renderers.
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-238), 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.
- Amendment (2026-05-22, D-222): canonical vocabulary. This 0.5m "sim tile" is now the Subtile — the granularity the server simulation, entity positioning, and render detail run on. The Tile = 1m (2×2 subtiles) is the unit sizes are quoted in and the one the world-generation cascade, pathfinding, and grids operate on. The dual-scale model and the 0.5m simulation granularity are unchanged; only the naming primacy flips. The Chunk/Block/Quarter/District ladder built on the Tile is canonical in D-222.
- 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 T-444). Sim tile size remains 0.5m; visual presentation changes from 1:1 to 2:1 retina factor.
- Amendment (2026-06-17): The dual-scale coordinate model (0.5 m Subtile / 1 m Tile) and the 2×2-sim-tile geometry minimum are unchanged and load-bearing for the 3D architecture — cover/LOS/collision still map 1:1 to what the player sees. The "Sprites: 2×2 sim tile footprint" bullet and the "sprites look right" rationale are pre-Sprint-28 sprite-era: in-world entities/objects are now rendered as 3D (D-149 / D-244), so there is no pixel sprite footprint — the 2×2 figure persists only as the visual-tile proportionality the 3D entity occupies. The spatial math survives; only the rendering technology changed.
- Cross-reference: Tile-based movement (D-054), shadowcasting (D-238), 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.
- Amendment (2026-05-22, D-222): naming + scale superseded. The unit this record calls a District (4×4 blocks) is renamed the Quarter (now 512m at Tile = 1m); District is promoted to a new 2048m tier (4×4 quarters). Tier counts and nesting structure here are unchanged — only the names and the metre values (per the Tile = 1m / Subtile = 0.5m scale of D-220). See D-222 for the canonical ladder and the lore-vs-code rule.
- 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 T-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 (T-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 (T-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)
- Amended 2026-05-25 (D-229/D-231 — guarantee-audit engine hook): The audit runs AFTER step-3 fill completes for a district, before the skeleton is marked "complete"; on a failed mandatory check, fill regenerates with adjusted parameters. The mandatory-check set is keyed on
ComplexityTier, notWorldTier(T1 = all inhabited; T2 and T3/A-1..A-4 = Full only). Signature:run_guarantee_audit(skeleton: &DistrictSkeleton, building_tags: &[BuildingPropertyTag], street_graph: &StreetGraph, seed: SeedChain) -> GuaranteeAuditResult. Tier-1 checks (all inhabited):social_hub_present(≥1 building with entry_class ∈ {Public, Commercial} AND zone ∈ entertainment/market/transit);informal_zone_present(≥1 Organic + Public, or open ground in an Organic district);encounter_corridor(street graph has a continuous Public traversal path crossing the district). Tier-2 checks (full-complexity):traffic_chokepoint(a bottleneck node whose removal disconnects the graph);institutional_space(admin/judicial/checkpoint, Restricted);insider_space(Restricted + Hidden or non-primary-street door);economic_node(≥1 Commercial);breach_only_zone(≥1 BreachOnly). Tier-3 / A-1..A-4 (pure graph/geometry over step-3 tags + street graph — NO interior generation): A-1 elevated vantage (max above_ground ≥3 with unobstructed LOS to a chokepoint); A-2 egress multiplicity (≥2 distinct unique-street connections across the perimeter); A-3 temporal opacity (≥1 TemporalWindow door → reduced traffic in closed hours); A-4 non-institutional route (a Public→insider-space path exists that avoids all institutional buildings).GuaranteeAuditResultgains a bool per check above, retaining its existingrooftop_discovery_zone(D-106) andhorizon_view_corridor(D-102) fields. Raised by: Gestalt, economic-built-world workshop round 2.
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 (T-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 (T-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 (T-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.
- Modifier axis A (
HeritageRoot) superseded 2026-05-25 by D-232 → replaced byFlavorTagFilter; axes B/C and faction/climate/condition/season unchanged.
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 (T-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 (T-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 (T-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 (T-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 (T-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 T-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)
- Editorial note (2026-07-06, T-1088): the opening "30° tilt (60° from horizontal)" contradicts this record's own preset list — the preset list is authoritative: pitch values are measured from horizontal (0° = horizontal), gameplay default −30° from horizontal, frontal −5°, overhead −80°. Implemented as such in the T-1088 sandbox camera. Also note "45° rotation" here means the single static map rotation (one Transform3D on the tile-grid root); stepped player camera rotation was never decided (parked under Q-084) and requires a new record before implementation.
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 T-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 | World generation (tile/chunk/block) — deterministic multilayer seed→tile cascade; each layer viewed as a map in the implant Atlas (no in-world rendering) | Deterministic walkable-world data + per-layer Atlas maps + asset catalog |
| 5 | Player control & in-world rendering — character, walls/stairs/doors, lighting, drawn on generated tiles (no test map) | Player viewport with final-version assets on the generated world |
| 6 | Detail coloring — room-level content, cultural architecture | Only when the world is walkable |
- Amendment (2026-05-22): Phases 4 and 5 swapped — world generation now precedes player control. The rule: no player-control or in-world rendering work begins until the generator can deterministically seed-generate every tile of every world via the full multilayer cascade. The original Phase 4 "2-floor test map" is dropped — test layers are produced by the generator itself once layer-drawing begins; we start drawing the world only when generation knows what to draw. Generation progress is viewed as per-layer maps in the implant Atlas (the Phase 3 deliverable, already built), not via an in-world renderer. The existing in-world rendering code is left as-is until Phase 5 — neither built upon nor removed before then. Rationale: building player systems against a throwaway test substrate means rebuilding them against real generated tiles later; gating player work on deterministic generation avoids that waste. Epics T-749 (now Phase 5) and T-750 (now Phase 4) and the CLAUDE.md cascade table are updated to match.
- Amendment (2026-06-12): Build order within and after the cascade clarified (Jeroen). The world is built outside-in: (1) geo layer; (2) the economic layer drawn onto the world — cities, buildings, streets, blocks, and the inter-settlement road/rail network; (3) building templates; (4) door boundary contracts defining the seam between exteriors and building interiors. Only then (5) the background NPC layer and building interiors (Phase 6), and only after that does gameplay/scenario design resume ("making it a game again" — tracked in the post-cascade gameplay parking epic, outside any phase). Player control and visual rendering (Phase 5) ramp in parallel once outside generation produces walkable exteriors — the Phase-5 trigger is "outsides generate deterministically and are walkable", not "all Phase-4 polish complete"; T-962 holds the gate and is updated to this trigger. Guiding statement: "Before this is a game I want it to be a Reach a character can travel through." NPCs, scenarios, and gameplay systems are conceived, designed, and built only after the traversable world exists. This amendment absorbs the v0.2 scope cluster — D-114, D-115, D-117, D-118, D-120 are marked superseded by this record, with each one's surviving design substance noted on the record itself.
- 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 T-745, Epics T-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).
- Amended 2026-07-13: generation-layer maps (D-226/T-960) bottom out at settlement/quarter-skeleton granularity — chunk/tile/voxel fill is never a planetary map layer; it is harness-verified and inspected in-world in Phase 5 (see the D-226 amendment of the same date).
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, gate distance, economics panel link) — shadow economy zone dropped per §7 amendment (2026-05-22)
- Economics panel link: click-through to Phase 2 economics panel pre-filtered to that node — primary Phase 2/3 integration point
7. Overlay System — 8 MVP Overlays
- Amendment (2026-05-22): dropped the
shadow economytoggleable overlay (9 → 8 overlays). Shadow economy is an underwater simulation modifier —shadow_economy_intensity(D-174) feeds derived signals such ascollection_efficiencyand signal 7official_coverage_ratio(D-181), but it is not a user-navigable data point, so it does not warrant a player-facing atlas overlay or City Data Panel field. The simulation layer (D-174) is unaffected. Also removed from §6 (city data panel) and §10 (completion criteria) for consistency. - 5 always-on: terrain, infrastructure, named features, gate/spaceport POIs, political zones
- 3 toggleable: population density, production zones, 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-05-22, D-223): superseded — markers.json is reduced to names only (a flavored name pool); it no longer carries position arrays or topographic-feature geometry. River/mountain positions derive from the heightmap + drainage (D-208); settlement positions from the economic sim + placement (D-211). The 6 hand-authored templates and their
reservedpinning are removed (bodies stay as ordinary named bodies). The pixel-space schema below is retained as historical reference only. - 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 (T-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 8 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 (T-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 T-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 T-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: T-920
- Raised by: Generation cascade workshop (T-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 weight matrix gives graduated preference, not binary requirement.
- Amended 2026-06-03 (T-955): the matrix weights and the whole placement-scoring path are integer basis-points, not f32 (D-010 determinism / D-227 save-critical). When T-955 wired
match_citiesinto the deterministic generation cascade, the original f32 scoring became a live cross-platform divergence risk (a near-tie score comparison or the Hungarian's f32 reductions can round differently per platform → a different world from the same seed). Now:CompatibilityMatrix.weightsarei32bps (10000 = 1.0×; the examples above are 28000 / 30000 / 25000 …),GeographicAttractor.strengthandterrain_modification_costare bps, andcell_score/ the Hungarian /CityPlacement.scoreuse integer arithmetic. The 0.0–3.0 affinity semantics are unchanged; only the representation is now integer. - Ticket: T-919, T-925, T-955
- Raised by: Generation cascade workshop (T-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: T-913
- Raised by: Generation cascade workshop (T-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: T-920 (consumer of prosperity_baseline)
- Raised by: Generation cascade workshop (T-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: T-915 (CityGenerationContext reads economic snapshot)
- Raised by: Generation cascade workshop (T-897)
- Cross-reference: D-026 (simulation tiers), D-200 (CityGenerationContext), D-206 (background generation queue)
- Amended 2026-05-25 (D-233 — structural-fill / condition-overlay two-pass split): The "Phase 2 condition application" language is replaced by a formal two-pass model. Pass 1 — Structural fill (frozen; reads t=0 initial-economics only; re-derivable from
seed + initial_economics_snapshot): zone-type per block, building footprint shape+position, building-type vocabulary tags (BuildingTag),founded_era, operations-surface extent (bulk industries), and the labor-demand signal feeding adjacent residential blocks. Pass 2 — Condition overlay (the sanctioned rolling-economy consumer; a paint layer OVER the frozen fill; refreshable on a cadence):BuildingConditionState = New|Maintained|Worn|Derelict|Abandoned;OccupancyState = Full|Partial|Vacant;VegetationEncroachment(Abandoned in appropriate sub-biomes, D-210); feeds the D-100 tileDamageOverlay. Hard wall: the condition overlay CANNOT change aBuildingTag(a mine plant becomes an abandoned mine plant, never an office). The fill generator's signature isfill_chunk(ctx: CityGenerationContext, seed: SeedChain)— NO access toPressureState, price signals, or tâtonnement output. The condition overlay is a SEPARATE Bevy system, triggered byEconEvent(D-180), with its own read set. Cross-ref D-233, D-100, D-180, D-210, D-217. Raised by: Burnelli/Tyre/Gestalt, economic-built-world workshop round 2.
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: T-915 (CityGenerationContext implementation)
- Raised by: Generation cascade workshop (T-897)
- Cross-reference: D-196 (SettlementClass), D-197 (prosperity_baseline), D-198 (economic simulation independence), D-200 (CityGenerationContext struct), D-237 (authored specialization layer — read-set extended)
- Amended 2026-05-25 (D-229/D-232/D-233):
CityGenerationContextgainsmorphology_zone,flavor_profile/architecture_flavors,dominant_bulk_class,dominant_production_ubiquity(the record already permits >6 fields). - Amended 2026-05-31 (D-237 — authored specialization layer): the read set gains
economic_specializationandcultural_specialization(both new columns onsystem_economy).dominant_faction(field 4) is now sourced from authored values onsystem_factionswhere present, heuristic fallback otherwise.economic_specializationis the upstream source ofdominant_bulk_class/dominant_production_ubiquity(resolved viaspecialization_vocabulary, D-233 re-amendment);cultural_specializationfeeds the D-232 template pool. Still bounded — three authored fields, all NULL-safe with deterministic fallbacks.
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_city_names,atlas_province_boundaries,body_radius_km. Output is a static artifact committed to the repo. Never runs during gameplay. (Amended T-963, D-202:atlas_body_heightmapsis no longer produced — elevation is now a per-body 16-bitheightmap.pngfile, not a DB table.) - 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: T-915
- Raised by: Generation cascade workshop (T-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 Province ~50–500km Province boundaries (watershed-derived, D-205), biome zones 5 Settlement ~1–30km radius City footprint, district layout 6 District 2048×2048 tiles (2048m, 4.19 km²) 4×4 quarters — large urban division (D-222) 7 Quarter 512×512 tiles (512m, 0.262 km²) 4×4 blocks — settlement footprint cell, Phase 1 skeleton grid (D-222) 8 Block 128×128 tiles (128m) Generator planning unit, 2×2 chunks (D-222) 9 Chunk 64×64 tiles (64m) Streaming/serialization unit (D-222) - Tiers 6–9 (District → Chunk) are the sub-settlement spatial hierarchy, canonical in D-222 — renamed/resized from the original D-094 ladder (the old 512m "District" is now the Quarter; District is now 2048m), at the Tile = 1m / Subtile = 0.5m scale of D-220. This decision formalizes Tiers 1–5 with equivalent lock status.
- Tier 3 heightmap resolution (512×256 equirectangular working grid; 1024×512 PNG) is the canonical format. Deviation requires amending D-191. Amended (T-963, D-202): the canonical stored heightmap is now a per-body 16-bit grayscale
heightmap.pngat 1024×512 carrying native elevation (the prior PNG was a 1024×512 RGB relief, now renamedreliefmap.png). PNG dimensions are unchanged (1024×512); Layer 1 downsamples to the 512×256 working grid. This amendment is the explicit deviation gate being satisfied — format/content changed, resolution preserved. - 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. - Amended 2026-06-14 (D-243): Tier 4 renamed Region → Province — it always was the watershed/political tier (D-205), and the name "Region" is now reserved for D-243's ~205 km metric containment cell (the top hard block of the absolute scale ladder + the climate/weather lockdown scale). Province is an overlay painted across regions, not a containment rung; the two sit at overlapping scales but are different kinds (irregular lore-bearing boundary vs fixed metric grid cell). D-243 owns the metric containment ladder (voxel→chunk→block→quarter→district→region); Tiers 1–5 here (Galaxy/System/Body/Province/Settlement) are organizational scopes/overlays, not metric rungs, and the ~1 km RegionProfile scale this record's tier-4 collided with is dropped (its carrier role moves onto the district, D-243 §5).
- The
SettingTypeenum onDistrictSkeletonis the interface between Tier 5 (settlement planning) and the skeleton cell (the 512m Quarter, Tier 7 —DistrictSkeletonis pending rename to match D-222).
-
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: T-912 (WorldTier enum), T-913 (SettlementClass)
-
Raised by: Generation cascade workshop (T-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.
- Amendment (2026-05-23, T-963): the DB-BLOB store is superseded by a per-body file-based 16-bit grayscale
heightmap.pngstored next to the body's other assets (path frombodies.terrain_reference). Rationale for the change: raw float grids committed inside a binarysystems.dbare the exact binary-merge-conflict trap the asset-pipeline rule warns against, and ~512KB×N bloats the DB; a per-body file matches D-203's on-demand model and keepssystems.dblean. Concretely:- Naming fix: the existing color hypsometric render (today's
heightmap.png, 1024×512 RGB) is renamedreliefmap.png— it is a relief visualization, not elevation. Display-only. - Canonical elevation: a new
heightmap.png= 16-bit grayscale (luminance = normalized elevation), 1024×512 (2× per axis / 4× the cells of the old 512×256 sim grid — the PNG dimensions are unchanged from the prior canonical 1024×512 in D-201; only the content changed from an RGB relief to native 16-bit elevation). 1024×512 bounds install size (~190 MB across 267 inhabited bodies) while sub-pixel detail is synthesized by the lower cascade layers. Single source of truth — the reliefmap and all computed geography derive from it, so it is bit-identical/deterministic by construction. - Multi-resolution: the stored heightmap is high-res for the lower layers (region/block/tile sample local detail); Layer 1 (continental drainage/basins/mountain-ranges) calls
BodyHeightmap::downsampleto theGRID_W×GRID_H = 512×256working resolution first, decoupling continental compute cost (~45ms) from stored resolution. - Rust loader:
heightmap.rs::load_heightmap_pngreads the 16-bit grayscale PNG (via thepngcrate), normalizes to f32 [0,1]; rejects RGB (a reliefmap can't be misread as elevation).sea_levelis stored in the PNG as atEXtchunk (the heightmap is self-describing), with a caller-supplied default as fallback. atlas_body_heightmapsis dropped;import_heightmaps.pywrites the PNG file instead of a DB row. The bake runs in the content pipeline (numpy/scipy) once; the runtime cascade is pure Rust loading the file.- Implementation status (T-963): Consumer done —
heightmap.rs::load_heightmap_pngreads the 16-bit grayscale PNG +sea_leveltEXt chunk, rejects RGB, downsamples for Layer 1. Producer done —import_heightmaps.pyis the bake (rename legacyheightmap.png→reliefmap.pngfor all bodies incl. Sol; for non-Sol inhabited bodies write a fresh cleanreliefmap.png+ 16-bitheightmap.pngfromsimulate()at the bumped 1024×512 grid).atlas_body_heightmapsdropped via MIGRATION_SQL + removed fromsystems-schema.sql. Godot client (atlas_viewer.gd) loadsreliefmap.pngfor display. Sim determinism guarded bytest_sim_determinism.py.
- Naming fix: the existing color hypsometric render (today's
- 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. (Superseded by the T-963 amendment above — the file-based model won out because the DB-as-single-source goal conflicts with binary-merge-conflict avoidance and DB size; a per-body committed PNG is itself a queryable, diffable-by-render asset.) - Ticket: T-901 (schema), T-906 (import), T-916 (Rust loader)
- Raised by: Generation cascade workshop (T-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: T-917
- Raised by: Generation cascade workshop (T-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: T-905 (schema), T-910 (populate from planet_class fallback)
- Raised by: Generation cascade workshop (T-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: T-904 (schema), T-907 (populate from watershed analysis)
- Raised by: Generation cascade workshop (T-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: T-924 (background queue), T-926 (SystemNameIndex)
- Raised by: Generation cascade workshop (T-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
- Amendment (2026-05-22, D-223): superseded in part. markers.json no longer retains topographic-feature geometry — it is reduced to names only (a flavored name pool). River/mountain positions are derived from the heightmap + drainage (D-208); settlement positions come from the economic sim + placement (D-211). The
reserved=1pinning and the 6 hand-authored templates (Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade) are removed — their bodies remain as ordinary named bodies, only the authored machinery is gone. See D-223. - Amendment (2026-07-16, D-242): the
corp_idcolumn and the "corp HQ cross-reference" (T-909,populate_atlas_city_names_corps) this record'sDecisionsection describes below are removed. That insert path (kept alive through D-223's amendment above) had noUNIQUE(body_id, name)and produced duplicate co-named "cities" — oneatlas_city_namesrow per corp HQ (e.g. 10Groombridgerows on GJ380c). The corp↔settlement relationship now lives with the corp (corporations.headquarters_body/headquarters_city_id), not as a row in the city pool.atlas_city_names.corp_idis left in the schema (additive-safe) but is permanentlyNULLgoing forward — see D-242. - 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: T-902 (schema), T-908 (populate from wiki), T-909 (corp HQ cross-reference)
- Raised by: Generation cascade workshop (T-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 the 512×256 Layer-1 working grid (D8 runs at 512×256, downsampled from the 1024×512 stored heightmap per D-202 amended T-963). - 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: T-918
- Raised by: Generation cascade workshop (T-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: T-925 (types), T-919 (matching pipeline that consumes these)
- Raised by: Generation cascade workshop (T-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: T-919 (attractor matching — uses terrain_modification_cost)
- Raised by: Generation cascade workshop (T-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: T-919, T-925
- Raised by: Generation cascade workshop (T-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." - Amended 2026-06-05 (T-956 — implementation): two changes from the original spec.
- New variant
AutonomistHeldadded betweenFrontierUnclaimedandIndigenousHeld. The original four control buckets (Commission/Corp/Contested/Frontier) predate the richer faction canon (wiki/factions/): the Compact of Westphalia is a self-governing autonomist bloc that rejects Concord Assembly authority — it governs its systems firmly, so it is neitherCommissionControlled(it is the Assembly's rival), norFrontierUnclaimed(it is not ungoverned), nor locallyContestedZone(the Compact is locally dominant).AutonomistHeldis its bucket, and gives the Compact its own colour on the political-zones overlay. - Derivation source. The numeric per-faction
faction_influencethresholds the original record specifies are not present in the data — only a single authoreddominant_factionper system exists (D-237's 8-value vocabulary). So the implementation mapsdominant_faction → TerritorialStatusinstead (attractor_matching::territorial_status_from_faction), grounded in faction canon:concord_assembly/veil_institute→CommissionControlled(the Assembly is the Reach's central government; the Veil Institute is Assembly-funded and -aligned);syndic_dominant→CorpTerritory;compact/compact_sympathetic→AutonomistHeld;disputed/mixed→ContestedZone;independent/NULL/unknown →FrontierUnclaimed.IndigenousHeldandDerelictremain unreachable fromdominant_factionalone (they need the cultural-corridor autonomy flag / population density) — deferred.dominant_factionis system-level, so status is uniform across a body's provinces for now; it is still stored per-basin onDrainageBasin.territorial_status(forward-compatible for per-province faction data).placed_at_generationis not yet modelled (no runtime re-classification exists yet).
- New variant
- Ticket: T-921, T-956 (implementation + amendment)
- Raised by: Generation cascade workshop (T-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-237 (authored
dominant_factionsource), D-214 (PoliticalArchetype — consumes this)
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.
- Amendment (T-1076 §4, 2026-07-16 —
RailHeadFacingvariant added; the D-242-gated follow-on): the enum gains a sixth variant,RailHeadFacing { bearing_degrees: u16 }— the street grid faces the settlement's rail head. Unlike the five attractor-derived variants, it is assigned post-placement by the Layer-2 road-graph pass (road_graph::assign_railhead_orientations, run in the cascade immediately afterbuild_road_graph): every settlement that is a high-connectivity junction (RoadGraph::high_connectivity_junctions(), incident-edge degree ≥ 3 — the criterion from this record's workshop source, paula-round3.md) has its attractor-derived orientation overridden toRailHeadFacing, withbearing_degreesthe octant-snapped compass bearing (0 = N, clockwise; one of the eight 45° octants — integer-only math, D-010; the diagonal band is the|minor|·2 > |major|integer approximation, sound because the consumer snaps octants to quarter-edges anyway) from the settlement toward the dominant incident edge (longestlength_cells, ties to the lowest edge index) — the direction the freight frontage faces. The Layer-4 skeleton consumes it viaskeleton_gen::railhead_edge— the same octant→cardinal-edge snap ascoastal_edge, feeding the same D-234b flush-frontage machinery: blocks on the rail-facing quarter edge present flush to the rail head the way waterfront blocks present flush to the quay (coastal wins if a settlement somehow carries both; variants are exclusive by construction). Deterministic: a pure function of the (already deterministic) road graph. - Ticket: T-914; T-1076 §4 (RailHeadFacing)
- Raised by: Generation cascade workshop (T-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-242 (the road-graph hub refinement this variant shipped under; T-1076)
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.
- Amended 2026-05-31 (D-237 — authored specialization layer): for named systems
dominant_faction(a D-214 derivation input viaTerritorialStatus/D-199) is now an authored value (8-value vocabulary:concord_assembly | compact | compact_sympathetic | syndic_dominant | veil_institute | independent | disputed | mixed) rather than one the heuristic guesses from hop-distance/currency; the existing derivation remains the fallback for unauthored systems. The archetype mapping itself is unchanged — it now reads a more trustworthy faction for the ~40–60 named systems where the heuristic was demonstrably wrong (e.g. Groombridge resolvessyndic_dominant→ Corporate, not the hop-2 default that would yield Commission).lattice_commissionis deliberately not a faction value — the Commission is a regulator, not a governing faction (ACB and Bastion areconcord_assembly). - Implemented 2026-06-05 (T-956):
attractor_matching::political_archetype(territorial_status, economic_role)lands the derivation, stored per settlement onCityPlacement.political_archetype.TerritorialStatusprecedence is enforced (a Commission-controlled manufacturing hub →Commission, notIndustrial); statuses that don't dictate an archetype (ContestedZone/IndigenousHeld/Derelict) fall through toeconomic_role, and the newAutonomistHeld(D-212 amendment) →Pioneer(self-organized, no central planner). The "spatial effect on district mix" (D-194 weight multipliers) is consumed later by the Quarter-skeleton generator (T-957). - Ticket: T-914, T-956 (archetype derivation + storage)
- Raised by: Generation cascade workshop (T-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-237 (authored
dominant_factionsource)
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.
- Implemented 2026-06-05 (T-956): the
ArrangementPatternenum (the five patterns) and its derivation (attractor_matching::arrangement_pattern, fromPoliticalArchetype+ transit_hub override) land here, and the chosen pattern is stored per settlement onCityPlacement.arrangement_pattern. The block-adjacency enforcement (constraining the first 2–3 quarters' layout) is the Quarter-skeleton generator's job and is deferred to T-957; the per-seed angular variation rides onFoundingOrientation(D-213, seed-derivedFreebearing). - Ticket: T-914 (types), T-956 (enum + derivation + storage), T-899/T-957 (skeleton-gen enforcement)
- Raised by: Generation cascade workshop (T-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: T-922
-
Raised by: Generation cascade workshop (T-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)
-
Amendment (2026-05-26 —
founding_age_yearsbackfill, ticket T-1000):founding_age_yearsis backfilled on every inhabited body intooling/economy-db/import_economics.pyMIGRATION_SQL viaCOALESCE(events-first, wave-fallback). Events-first: the system's authoredhistorical_events.age_yearsforevent_type = 'colonial_charter'— the canonical founding event authored during the economics built-world workshop. 9 systems in the live DB carry a colonial_charter row, driving 11 inhabited body values; 5 of those diverge meaningfully from their wave fallback (e.g. GJ 144 = 580 vs wave-1 fallback 600, GJ 338B = 590, GJ 380 = 480). Wave-fallback — canonical founding-edge of each wave's range (docs/design/systems-framework-final-miri.md:495–499):settlement_waveera range (years ago) founding-edge fallback wave_1Core Founding 500–600 600 wave_2Trade Expansion 300–500 500 wave_3Working Reach 100–300 300 wave_4Frontier Push 40–100 100 wave_5Activation Edge 0–40 40 originSol — predates the wave taxonomy — NULL (Sol excluded per D-236) Founding-edge (not midpoint) so a wave-1 body reads as ~600 years old, matching the canonical Reach age. ±50yr drift from per-system reality is negligible for the
founding_age_years / 1000arithmetic above.
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: T-923
- Raised by: Generation cascade workshop (T-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: T-900 (bug fix), T-912 (full enum implementation)
- Raised by: Generation cascade workshop (T-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.
- Amendment (2026-05-22, D-222): the 512m unit this table calls District is renamed the Quarter; District is promoted to a new 2048m tier (4×4 quarters). The Tile/Subtile scale above is unchanged. Throughout this record — including the footprint formula (
district_count = footprint / 0.262) and the density examples — "district" denotes the 0.262 km² Quarter cell, not the new 2048m District. See D-222 for the canonical ladder and the lore-vs-code rule.
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).
- Amendment (2026-05-22, D-222): the 512m unit this table calls District is renamed the Quarter; District is promoted to a new 2048m tier (4×4 quarters). The Tile/Subtile scale above is unchanged. Throughout this record — including the footprint formula (
-
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
D-222: Spatial hierarchy and naming — Subtile to District, lore vs code
-
Date: 2026-05-22
-
Decision: Canonical naming and sizes for the sub-settlement spatial hierarchy. This record is the single source of truth for the ladder; it supersedes the dimensions/naming scattered across D-094 (hierarchy), D-201 (tier table), D-220 (tile scale), and D-066 (dual-scale grid), which are amended to point here.
The ladder (each tier nests cleanly in the one above):
Unit Side Composition Real-world analogue Used for Subtile 0.5m — a footstep server simulation, entity positioning, render detail Tile 1m 2×2 subtiles a doorway the unit all sizes are quoted in; generation cascade, pathfinding, grids Chunk 64m 64×64 tiles half a block streaming / serialization unit (technical) Block 128m 2×2 chunks a city block (~100m real) generator planning unit Quarter 512m 4×4 blocks (16) a superblock (≈ Barcelona superilla, 0.262 km²) the cell a settlement footprint is tiled into District 2048m 4×4 quarters (256 blocks) a real urban district (4.19 km²) large urban division (above) fluid — borough / sector / city not a fixed tier — settlements vary too much; groupings above District are settlement-specific Nesting rhythm: Chunk→Block is 2×2 (Chunk is a technical sub-block streaming unit); Block→Quarter→District is a uniform 4×4 (the legible "human" ladder).
Lore ≠ code (load-bearing): the tier names above are code/generation units — fixed grid cells. The same words used in lore, narrative, and UI ("Sova Transit District", "the market quarter") are free-form region labels with no obligation to map to a code tier. A narrative "district" may span several code Quarters, sit inside one, or ignore the grid entirely. Reviewers and the clerk must not reconcile a lore name to a code tier — they are different registers that happen to share vocabulary.
Rename from prior records: the 512m cell that D-094 / D-201 / D-220 called a District is now the Quarter. "District" is promoted to the 2048m tier (4×4 quarters), matching the real-world scale of a named urban district. The tile scale (Tile = 1m, Subtile = 0.5m) and the doubled footprints established by D-220 are unchanged. Code that uses "District" for the 512m skeleton unit (e.g.
DistrictSkeleton) is now misnamed → see follow-up ticket. -
Rationale: The old hierarchy named only Chunk/Block/District and jumped from a 512m "District" straight to the 1–30km Settlement, leaving the 512m cell wearing the name of a unit two rungs too large — a real district is kilometres and many neighborhoods, not 16 blocks. Grounding against real sizes (city block ~100m; Barcelona superblock 400m / 9 blocks; real district several km) puts Block at 128m (correct), the 512m cell at Quarter/superblock, and a true District at 2048m (4.19 km²). Leaving everything above District fluid matches reality — boroughs/sectors/cities vary too much to force a fixed generation tier, and the Reach's settlements range from frontier homesteads to compressed capitals. Separating the lore register from the code register lets narrative use "district" naturally without dragging the generator's grid into it.
-
Raised by: Jeroen + Claude, design session 2026-05-22. Grounded against real-world block/superblock/district scales.
-
Cross-reference: D-094 (spatial hierarchy — renamed/superseded), D-201 (tier table — superseded for the sub-settlement tiers), D-220 (tile scale — Tile/Subtile retained; its "district" footprint cell is the Quarter), D-066 (dual-scale — Subtile is the 0.5m grid), D-093 (Sova Transit District — a lore district, not bound to the code tier)
-
Dissent: None
D-223: Authored content as a flavored name pool — markers names-only, hand-authored templates removed
-
Date: 2026-05-22
-
Decision: Per-planet authored content feeds the deterministic generator as a flavored name pool only — never as authored geometry or pinned positions. This supersedes the topographic-geometry retention and the
reserved=1pinning of D-207, and the hand-authored-template references in D-191 §8.markers.json → names only. A body's
markers.jsoncarries a flavored pool of names (river names, mountain names, settlement names) and no positions or geometry. Positions are not authored anywhere; they are derived or generated:- Rivers — courses come from the deterministic D8 drainage (D-208) on the fixed heightmap; the largest rivers take names from the pool.
- Mountains — positions are a given from the fixed heightmap; they take names from the pool.
- Settlements — the economic sim runs, the largest population areas are found, the capital and cities are picked by the placement pipeline (D-211), and they take names from the pool.
The 6 hand-authored templates are removed. Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade were a superseded hand-authored-showcase direction that kept resurfacing and complicated the landscape. Their hand-authored marker positioning, the
reserved=1pinning, and the "6 templates" framing/special-casing are all removed. The bodies themselves remain real places — their wiki pages, lore, corporations (e.g. comptoir-lendel), and names stay and feed the name pool; deleting them would only force regenerating equivalent content. Only the authored machinery is tossed. After this there is no distinguishable category of "template" left — the six are processed by the exact same pipeline and data model (fixed heightmap + names-only markers) as every other body, with no special code path, schema flag, or pinning. They differ only in content richness (more developed lore/names/economic profile), which is a difference of degree shared by any notable body, not a difference of kind. The concept dissolves; that is why it can be removed wholesale rather than carefully migrated.No pinned positions anywhere. Determinism comes from fixed heightmaps + deterministic drainage + deterministic economic sim + seeded placement — not from authored anchors. The lore-vs-code split (D-222) governs how pool names attach to generated features: names are free-form labels on generated geometry, not bound to grid cells.
Sub-city authored set-pieces (e.g. D-093 Sova Transit District, station interiors) follow the same principle when Phase 4 reaches that scale — generated geometry, lore names/roles attached, nothing pinned — to be applied (and prior authored layouts superseded) at that point.
-
Rationale: Authored positions created a hand/procedural split that (per D-207's own rationale) was impossible to query, diff, or validate. Reducing authored content to a name pool removes that split entirely while preserving the flavor that makes places feel hand-made — names follow culture and region, geometry follows terrain and economics. The templates were leftover scaffolding from before fully-generative placement; removing them simplifies the pipeline without losing any canonical place.
-
Implementation: tracked under Phase 4 (epic T-750) — strip markers to names, remove template machinery +
reservedpinning, update the atlas pipeline/schema, regen. Historical archives (sprints, workshops, audits, CHANGELOG, atlas proposals) are left untouched. -
Implementation status (T-951, 2026-05-22): done. All 2,398
markers.jsonreduced to a names-only pool (25,264 names; 349 city names across 271 bodies). The Python atlas geometry generator (generate_atlas.py) and the LLM naming cluster (gemma_naming.py,naming_core.py,apply_name_fixes.py, their tests/QA, thefix_fewshot_bleed/prune_atlas_featuresgeometry tools, the redundantimport_city_names.py, and therun-atlas-naming.shrunner) were retired — the procedural server cascade (Phase 4) supersedes them. The Gemma prompting methodology is preserved in docs/gemma-naming-methodology.md. Shared atlas-DB utilities moved totooling/planet-gen/atlas_common.py;import_economics.pyis now the sole regen-db generator that owns the atlas index — it loads the names pool intoatlas_city_namesand empties the 8 geometry tables (atlas_cities/roads/railroads/pois/rivers/oceans/mountain_ranges/body_grids), which the cascade fills.SUPERSEDED (D-242, T-1075): both are now install-baked at import time (a documented rank-size derivation frompopulation/kind/settlement_classonatlas_city_namesare deferred to placement (T-955)bodies.population;settlement_classdefaultsPopulationBudgetwith a smallNameLockedoverride list) — see D-242.TheSUPERSEDED (D-242, T-1074): that insert path produced duplicate co-named "cities" (noreserved=1corp-HQ cross-reference stays (corp HQ names are real places)UNIQUE(body_id, name)) and is removed; the corp↔settlement relationship now lives oncorporations, not as a city-pool row — see D-242. A latent duplicate-accumulation bug in name population (no clear + no unique constraint) was fixed with a deterministic rebuild. Sol (systemGJ 0) is permanently exempt from the normal generators: it uses real Earth/Mars/Luna geography via the offlinesol_import.py(left in place for future scripted integration), so its bodies keep geometry-bearingmarkers.jsonas preserved positional config and are skipped by the names-pool importer — Sol names will come from its own integration, not the cascade.make regen-dbgreen. -
Raised by: Jeroen, 2026-05-22 — resolving the open question on merging preconfigured content into the deterministic cascade.
-
Cross-reference: D-207 (superseded — names-only, no reserved pinning), D-191 §8 (markers format — names-only), D-208 (drainage → river courses), D-211 (settlement placement), D-199 (economic read set), D-222 (lore≠code names on generated geometry), D-242 (corp-HQ cross-reference removed + population/settlement_class baking — both supersede notes above)
-
Dissent: None
D-224: SeedChain — deterministic seed-derivation contract
-
Date: 2026-05-23
-
Decision: All deterministic generation — the Phase-4 world cascade now, and NPC / storyteller / economic generation later — descends from a single master world seed through one shared type,
SeedChain, living atserver/src/seed.rs(top-level, because seeds are broader than the atlas). It is the only sanctioned way to derive a child seed; the ad-hoc pre-mixing (seed.wrapping_add(C)) that was inskeleton_gen.rs/district_mix.rsis removed, and the doc-comment references to a "SeedChain" in those files become real.Mixing primitive.
splitmix64— already used byEntityRng::from_seed_and_id(simulation/rng.rs), chosen there specifically to avoid thewrapping_addcollision class where(seed=0,id=N) == (seed=1,id=N-1)— is promoted to a sharedpub(crate)function inserver/src/seed.rsand reused. One canonical mixer for the whole codebase. (EntityRngkeeps its own domainless combine —splitmix64(world_seed) ^ splitmix64(stable_id)— for now; re-expressing it asderive(Npc, stable_id)would be a deliberate, stream-changing migration, not a free cleanup, so it is left as a follow-up.)Contract:
pub struct SeedChain(u64); // Copy pub enum SeedDomain { Body, Layer1Topography, Layer3Settlement, Layer4Quarter, Block, Npc /* … */ } // u64 tags impl SeedChain { pub fn root(world_seed: u64) -> Self; // top of the chain pub fn for_body(world_seed: u64, body_id: &str) -> Self; // root + derive(Body, fnv1a_64(id)) pub fn derive(self, domain: SeedDomain, id: u64) -> Self; pub fn atlas_rng(self) -> AtlasRng; // integer-only LCG stream (D-010) pub fn seed(self) -> u64; // raw — for SimRng/ChaCha or further derive() }Derivation (load-bearing — pinned, because changing it changes every generated world):
derive(domain, id) = splitmix64(self.0 ^ splitmix64(domain as u64)) ^ splitmix64(id). Properties: deterministic; domain-separated (distinctSeedDomaintags never share a stream); full avalanche (splitmix64 on each input); integer-only (D-010 T-4); chainable (root → Body → Layer3Settlement → Quarter → Block). The output is well-distributed, soAtlasRng::newis fed the derived seed directly — no| 1or golden-ratio pre-mix guard.Body identity (the
Bodydomain id). Bodies are identified by a stringbody_id, not a numeric StableId, soSeedDomain::Bodyis keyed by FNV-1a (64-bit) ofbody_id— the repo's standard deterministic&str → u64convention (matchingTemplateId/TriangleId).SeedChain::for_body(world_seed, body_id)(root(world_seed).derive(Body, fnv1a_64(body_id))) is the single sanctioned path; callers must use it rather than inventing their own string→u64 hash, or they would silently derive divergent worlds from the same seed — the very class of nondeterminism this record exists to kill, one level up.Stability guards.
SeedDomaincarries explicit#[repr(u64)]discriminants and is append-only; the unit testseed_domain_discriminants_are_pinnedfails CI if any is renumbered (which would re-roll every world).AttractorTypelikewise carries explicit#[repr(u8)]discriminants because it is castas u8as a sort key (features.rs); reordering it would change attractor ordering and flip the cascade golden.Scope of effect (verified 2026-05-23): SeedChain changes only the RNG-using layers — the existing
skeleton_gen.rs(Layer 4 block placement) and the future Layer-3 settlement placement (T-955). It does not affect Layer 0 heightmaps (produced by the Pythonplanet_simulationpipeline, seeded separately via--seed, committed asheightmap.pngfiles) nor Layer 1 (drainage/features/subbiomeare RNG-free — pure functions of the heightmap). The T-952 Layer 0→1 golden fixtures are therefore SeedChain-independent and can be captured in any order relative to the SeedChain work. -
Rationale: Three seeding paths had drifted apart —
AtlasRng(LCG, "callers pre-mix"),EntityRng(correct splitmix64 mixing), and ad-hocwrapping_addin atlas callers — while the code already named a SeedChain that didn't exist. A single typed derivation chain with domain separation makes every sub-stream reproducible from one world seed, eliminates the(seed,id)collision classwrapping_addinvites, and gives the determinism harness (T-952) a stable contract to verify against. Promoting one mixer prevents two divergent implementations. -
Implementation: T-952 (Phase 4, epic T-750) —
server/src/seed.rs(incl.for_body/fnv1a_64),SeedChainthreaded through the atlas RNG callers, an extensible cascade harness (run_cascade/CascadeSnapshot/CascadeLayer), and a golden-seed regression test (SHA-256 ofheightmap.png+ JSON-serializedLayer1Output, run at 256×128 so the river network is non-empty — JSON not msgpack, to match the diffablegolden_suite.rsconvention). Pre-Phase-5: no savegames exist, so the seed-stream change needs no migration; D-202'sschema_versionlineage covers future changes once saves exist. -
Raised by: Jeroen + Claude,
/whats-nextrefinement of T-952, 2026-05-23. -
Cross-reference: D-010 (determinism — integer-only, seed→identical output), D-200 (three-tier execution model), D-208 (RNG-free drainage), D-223 (names-only pool — placement uses seeded RNG),
simulation/rng.rs(EntityRng / splitmix64 precedent) -
Dissent: None
D-225: Atlas layer-stream proxy — compute-on-demand, mod-first (resolves Q-098)
-
Date: 2026-05-23
-
Decision: How per-body generation-cascade layer data reaches the Godot client for the Phase-4 Atlas progress viewer (T-960). Resolves Q-098. Derived layer data is never baked into the install — that would both bloat the install (~100MB+ for ~267 bodies) and make modded bodies second-class. Instead a server-side layer-stream proxy computes on demand from moddable source files and streams to the client:
(1) Transport — existing IPC stream + additive message tag. Not a second socket, not a per-frame envelope rewrite. The bridge today carries no message-type discriminator (server→client is always
ObserverSnapshot, client→server alwaysVec<PlayerInput>). The atlas request/response ride the same TCP stream as new message types, disambiguated structurally in v1 (a snapshot hasentities/tick; the atlas messages do not). A fullBridgeMessageenvelope-everywhere migration is deferred — it would be a needless wire break, and client+server co-ship (D-005/D-192) so it can be done later as cleanup.(2) Single framed MessagePack response, not chunked.
Layer1Outputis ~hundreds of KB worst case (512×256), well under the 16 MB frame cap. The raster is not streamed — the client already loadsreliefmap.pngfrom disk; the proxy streams only the computedLayer1Output(rivers, basins, attractors + sub-biome). MessagePack matches the rest of the bridge.(3) Mod-first source resolution. A new
BodySourceResolverwith an ordered search: mod dirs (override) → base install (wiki/..., the floor). It reads the body's relativeterrain_referencefromsystems.db(read-only) and returns the first existing<root>/<terrain_reference>. First-party and mod bodies flow through the identical resolve→run_cascade→stream path — theheightmap.pngis the sole source of truth, no baked layer data. v1 wires the base root only; the resolver type + search order exist and are tested with a synthetic mod root, so the seam is mod-first from day one.(4) Cache via background queue on miss (D-203/D-206). Request →
BodyWorldStateCachelookup. Hit → serialize and return synchronously (a serialize, no compute). Miss → enqueue anImmediateAnalyzeBodyon theGenerationQueue(D-206 background tier) and replyPending; push the layer response when the completion lands. The ~45 ms cascade must never run on the tick thread (D-203: no blocking CPU/DB for heightmap data on main). The client shows the existing "TERRAIN DATA PENDING" panel until the response arrives. Eviction → recompute (always valid — determinism guaranteed).(5) Whole
Layer1Outputper response; client composites additive overlays. The layers are produced together in one drainage pass, so per-layer requests save no compute and only add round-trips. Overlay toggles (heightmap + rivers + attractors + sub-biome shown together) are a pure client-side render concern (the existing_overlay_visibilitypattern). The request carriesbody_id+up_to_layer(a forward-compat seam; v1 honorsTopography). -
Critical-path dependency: the proxy is inert until
gen_queue.rs::run_work_item'sAnalyzeBodyactually runsrun_cascade→ buildsBodyWorldState→ populates the cache (today a documented stub, deferred from T-142), andGenCompletion::BodyAnalyzedcarries the computed state, not justbody_id. This activation is the long pole and is tracked as its own ticket blocking T-960. -
Rationale: Baking privileges first-party content (a mod body cannot ship baked artifacts it cannot produce) and adds install bloat. Computing from the moddable heightmap on demand — the cascade is deterministic and ~45 ms, and D-200/D-203/D-206 already provide the background-compute + LRU tiers — keeps mods first-class, adds zero storage, and uses the architecture as intended. Reusing the existing IPC stream (vs a second socket) avoids a parallel connection lifecycle for an occasional, user-initiated, latest-wins-irrelevant request.
-
Deferred / spun off: the full
BridgeMessageenvelope-everywhere migration (later cleanup, not this ticket); the mod content catalog — mods adding new bodies need body rows +terrain_referencediscoverable, butsystems.dbis binary / source-canonical (D-189) and mods cannot append to it → Q-099. D-225 resolves mod file resolution only; base-install resolution is enough to ship T-960. -
Implementation: T-960 (client viewer + proxy) plus the
AnalyzeBodyactivation ticket. New surface:server/src/atlas/source_resolver.rs,server/src/atlas/layer_proxy.rs;gen_queue.rsrun_work_itemactivation +GenCompletionpayload; bridge receive/send branching; clientprotocol.gd/sim_bridge.gd/atlas_viewer.gd. -
Raised by: Jeroen (mod-first directive) + Tyre (design pass) + Claude,
/whats-nextrefinement of T-960, 2026-05-23. -
Amendment (2026-06-12, extension constraint — fable-ous.md S-10):
decode_inbound(bridge/mod.rs:38-74) distinguishes the two inbound message types purely by MessagePack shape (array =Vec<PlayerInput>, map =AtlasLayerRequest) and serde ignores unknown map fields — so a future second map-shaped inbound type with overlapping required fields would silently mis-decode as an atlas request. Constraint: the next inbound message type must introduce a tagged envelope (or a required marker field) rather than extending shape-based detection. -
Cross-reference: Q-098 (resolved by this), Q-099 (mod content catalog — spun off), D-191 (Atlas viewer), D-166 (per-layer Atlas progress viewer), D-200 / D-203 (three-tier execution, LRU cache), D-005 / D-192 (client+server co-ship — no version handshake), D-224 (SeedChain — feeds the cascade the proxy runs)
-
Dissent: None
D-226: Live-pause inspection harness — agent-navigable, real-UI debug/review
-
Date: 2026-05-24
-
Decision: A multi-layer harness that reuses the real client UI so both a human and an automated agent can review/debug deterministic server-computed state, by attaching to the live (auto-pausing) server. It supersedes the offline file-dump idea floated during D-225: the review tool is the production tool (zero divergence), inspecting live data through the real path rather than stale dumped artifacts. The stack, built bottom-up:
(1) Live-pause substrate. The client attaches to the running server; entering a fullscreen implant app (the Atlas) auto-pauses the sim. Pause freezes the world-advancing tick phases (Movement / Simulation / Economy / Storyteller / Knowledge / TickAdvance) but keeps PreInput (the gen-drain), Input (to receive resume), Snapshot and PostSnapshot (the bridge) alive — so the UI keeps fetching data while the world is frozen. Built on the existing
TickRate::Paused+ theinput.rspaused-allowlist seam, triggered byHudGroups::gameplay_occluded(D-170). For static Layer-1 geography pause is only a compute-saver; for dynamic state (economics) a stable frozen snapshot is essential.(2) Data — layer-stream proxy (D-225). Streams
Layer1Output(later economics state, save state) from server to client on demand, mod-first, no bake.(3) Human-visual viewer (T-960). The Atlas renders cascade layers as additive, toggleable overlays (relief base + rivers + drainage basins + attractors; shape encodes attractor type, color encodes sub-biome), extending the existing
OVERLAY_DEFS/AtlasOverlayBarwith agenerationoverlay group + a left-side legend panel. Buttons startpending/locked and unlock as each layer's data arrives ("grows as each layer lands", D-166).(4) Agent-navigable channel. A client-side
AtlasAgentInterfaceexposing JSONobserve(current data state + a walkable UI affordance tree) andact(named semantic intents —select_body,open_regional,set_overlay,back, … — backed by the same handlers a click calls, not pixel coordinates; the map uses_gui_input). Runs headless. Turns human-eyeball review into an agent-automatable QA sweep across the whole Reach (and later economics/saves).(5) Capture — reuse, don't reinvent. Interactive screenshots reuse the existing
tests/run-visualcapture primitive (frame-delayedsave_png, proven bycharacter_creation.gd's standalone capture). The agent channel adds interactive explore-and-capture; the existing scenario/flow visual-golden regression role stays distinct and unchanged. Clear hover state before capture.Consumers (build geography now; the others adopt the pattern in their own phases — do not build them now): Layer-1 geography (Phase 4) → economics (dynamic; pause essential) → save-game inspection (load → pause → inspect;
save_state.rssubstrate).Amended 2026-07-13 (T-960 rescope — Si audit ratifications):
- (a) Overlay mechanism simplified. The per-button
pending/locked +notify_gen_layer_ready(id)unlock mechanism in item (3) was never built; the shipped pattern — plain always-toggleable buttons plus a single per-body "generating" pending indicator — has carried two layer deliveries (the L1 triad, then T-1046's district grid) and is ratified as the standard. Stablegen_*button ids remain the contract. - (b)
gen_l0_heightmapdropped. Relief already renders via the always-onterrainoverlay (D-191 §7); a second generation-labeled L0 toggle would duplicate it. - (c) Legend panel stands. The left-side legend (shape/color key for attractor types and sub-biomes) remains an unshipped, in-scope deliverable of item (3)/T-960.
- (d) No tile-level Atlas map. The planetary Atlas maps generation layers down to settlement/quarter-skeleton granularity only; chunk/tile/voxel fill (L5) is verified by the believability/derivation harnesses and inspected in-world in Phase 5 — never as a planetary map layer (at most aggregate stats). Ratifies T-1046's implementation precedent; matching D-191 amendment + CLAUDE.md Phase-4 wording updated the same day.
Amended 2026-07-16 (T-1112 — coarse quarter-footprint Atlas layer, the L4 skeleton on the planetary map): the fourth generation-cascade layer to land on the Atlas, sitting between T-1046's
district_grid(D-239 coarse morphology) and item (d)'s hard ceiling. Design only — implementation is a follow-up ticket (touch points named in §4 below, none built here).-
(1)
QuarterFootprintLayerdata shape. Source of truth isBodyWorldState.quarters: BTreeMap<QuarterId, QuarterWorldState>(QuarterWorldState { skeleton: QuarterSkeleton, block_tags }), whereQuarterSkeleton.blocks: [[BlockSkeleton; 4]; 4]carrieszoning: ZoningType,district_type: DistrictType,density_pct: u8,landmark: Option<LandmarkSlot>per block, pluscorridors: Vec<CorridorSpine>at the quarter level. Critically,QuarterWorldStatecarries no independent spatial position —QuarterIdis a content-addressable hash (SeedChain::for_body(world_seed, body_id).derive(SeedDomain::Layer4Quarter, city_id).seed(),plugin.rs::build_skeleton_work_item), not a coordinate. The only spatial anchor a quarter has is thecity_idit was generated for — whichSettlementLayer/CityPlacement(T-960 §2, D-211) already carries as(city_id, position). So the layer is keyed bycity_id, joined to a quarter by recomputing the same deterministicquarter_idderivation the dispatch path already uses (a pure function of already-public inputs — no new field needed anywhere) and looking it up instate.quarters. This also means no city-outline geometry is derived or served — at planetary-map projection a 512 m quarter is roughly 1/80th of a heightmap pixel (a district ≈ 2048 m is already sub-pixel at ~40–78 km/px, D-243), so there is no real silhouette to trace; the layer is aggregate stats anchored at the existing L3 settlement position, mirroringRoadGraphLayer/SettlementLayer's established "trim the internal struct to what an overlay needs" pattern rather than inventing outline geometry with no data behind it.Per settlement-with-quarters, the served aggregate is:
pub struct QuarterFootprintEntry { pub city_id: u64, pub density_avg_pct: u8, // basis-point mean of BlockSkeleton.density_pct, 16 blocks pub dominant_district_type: DistrictType, // mode across 16 blocks; ties → lowest declaration-order variant pub dominant_zoning: ZoningType, // mode across 16 blocks; same tie rule pub landmark_count: u8, // count of Some(LandmarkSlot) across 16 blocks (max 16) pub corridor_count: u8, // QuarterSkeleton.corridors.len(), clamped to u8 } pub struct QuarterFootprintLayer { pub entries: BTreeMap<u64, QuarterFootprintEntry>, // keyed by city_id, D-010 determinism }Five fields earn their place:
density_avg_pctand the two dominant-mode fields are Araminta's color/shape encoding inputs (§3);landmark_count/corridor_countare inspection-only (the D-226(d) ceiling forbids them as a map-visible channel — they surface in the existing city-click sidebar instead, anImplantDataRowaddition, not a new draw call). Rejected from the set: per-block detail (violates the ceiling outright),reservations/social_sitescounts (no consumer identified — Araminta's encoding doesn't need them and nothing else asked), and a float density (D-010 integer discipline —density_pctis alreadyu8basis-point-flavored on the source struct, so the mean staysu8, nof32anywhere on the wire).dominant_district_type/dominant_zoningserialize as their named enum variant (serde default), following theRoadGraphLayer/SettlementLayerprecedent (RoadNodeKind,MaintenanceAuthority— neitherrepr(u8)-pinned, serialized as names) rather thandistrict_grid'sas u8byte-packing, which was specific to a densecols×rowsarray whereMorphologyZoneis deliberatelyrepr(u8)-pinned for that purpose; a handful of per-settlement aggregate fields have no such packing need.BTreeMap<u64, _>throughout for D-010 determinism, matchingblock_tags' ownBTreeMap<(u8,u8), _>precedent on the source struct. -
(2) Hard ceiling (binding). This layer is the concrete instance of item (d)'s "at most aggregate stats" clause: five scalar fields per settlement, quantized
u8, no per-block zoning/street/tag detail ever reaches the wire, and no chunk/tile/voxel data is touched (this layer reads onlyQuarterSkeleton/BlockSkeleton, neverFillChunkoutput — a different generation phase entirely, D-230). If a future ticket wants finer planetary-scale detail than this, the answer is "no" per item (d), not "extend this struct" — the settlement/quarter-skeleton granularity ceiling applies to this layer by construction, not by restraint that could erode. -
(3) Overlay encoding (Araminta). New overlay id
gen_l4_quarters(labelQTR,group: "toggle", matching thegen_*convention inOVERLAY_DEFS). No outline is drawn (per §1) — the layer is a density-scaled glyph anchored at the existing L3 settlement position, drawn in the same pass immediately after_draw_gen_settlementsso it reads as "on top of" the city dot it annotates. Square side scales offdensity_avg_pct(e.g.4.0 + density_avg * 6.0px at 1.0 zoom, clamped[4.0, 12.0]) — dense build reads as a bigger block, sparse as smaller, without pretending to show real shape. Shape carries identity, color carries intensity (the same convention as_sub_biome_color/MORPHOLOGY_COLORS): shape =dominant_district_type, a small corner-notch glyph family on the filled square (plain = mixed/no clear dominant, corner tab top-right = commercial, corner tab bottom-right = industrial, small diamond cutout center = civic/landmark — capped at 3–4 variants, a coarse skeleton read, not a legend of everyDistrictType); color =density_avg_pcton a single-hue intensity ramp within the existing settlement-gold family (COLOR_SETTLEMENT→COLOR_SETTLEMENT_CAPITAL-adjacent bright gold at high density), so the new layer reads as part of the settlement-marker family rather than a competing hue, since it always co-renders besidegen_l3_settlements.landmark_count/corridor_countare not a visual channel (§2's ceiling) — they surface asImplantDataRows in the existing city-click sidebar panel. Zoom gating reusesSETTLEMENT_LABEL_MIN_ZOOM = 2.0(no new threshold): below it, glyph draws at minimum size with color only (the notch is illegible at a few px anyway); at/above it, full size with the dominant-type notch visible. Legend entry (GENERATION_LEGEND):{ "overlay_id": "gen_l4_quarters", "title": "QUARTER FOOTPRINT — L4 (color = density, shape = dominant type)", "rows": [ {"glyph": "▪", "color": Color(0.55, 0.48, 0.30, 0.6), "label": "low density"}, {"glyph": "▪", "color": Color(0.94, 0.82, 0.38, 1.0), "label": "high density"}, {"glyph": "◪", "color": Color.TRANSPARENT, "label": "dominant type (corner tab)"}, ], } -
(4) Six wiring touch points (follow-up ticket, not built here).
AtlasLayerResponsegainsquarter_footprints: Option<QuarterFootprintLayer>+ abuild_quarter_footprint_layer(state, placements)function mirroringbuild_district_grid's "empty source →None" contract (server/src/atlas/layer_proxy.rs);ZoningTypegainsPartialOrd, Ordderives mirroringDistrictType's T-994 precedent (server/src/simulation/generator.rs— declaration order is not a stability-pinned wire format on either enum, so the additive derive is safe), which is what makes §1's lowest-declaration-order tie-break computable fordominant_zoning(PR #179 review:DistrictTypealready carries the derives,ZoningTypedoes not — the tie rule itself is unchanged);protocol.gdpassthrough for the new field (mirrors the existingdistrict_grid/road_graph/settlementsfields); agen_l4_quartersentry inOVERLAY_DEFS(client/ui/implant/apps/atlas/atlas_viewer.gd); a_draw_gen_l4_quarters()function imitating_draw_gen_district's "readviewer.get_generation_quarter_footprints(), guard onDictionary, draw" shape (atlas_marker_overlay.gd); and theGENERATION_LEGENDentry above (atlas_legend_panel.gd).layer_proxy.rswas mid-concurrent-edit for T-1113'sregion_gridaddition at design time — read-only pass, no conflict expected (both land as new siblingOptionfields onAtlasLayerResponse, following the same one-field-per-layer pattern the growth-ceiling note on that struct already anticipates naming T-1112 and T-1113 as the last two candidates).
- (a) Overlay mechanism simplified. The per-button
-
Rationale: Reusing the real UI — rather than a parallel offline renderer or dumped files — means the debug/review surface never diverges from what ships, and a dropped artifact can't go stale. Agent-navigability converts qualitative "does the synthesis look natural?" review from a manual eyeball pass into an automatable sweep that flags the few outliers for a human. The harness rides seams that already exist (
TickRate::Paused, the paused-allowlist,gameplay_occluded, the bridge framing, therun-visualcapture primitive) — a naming-and-contract exercise, not a new subsystem. -
New surface: server pause-gating (run-conditions on the world phases keyed to a pause command); client
AtlasAgentInterface(observe/act, Control-tree walker) + its local transport; the generation overlay rendering + selector + legend; interactive capture wired torun-visual. -
Implementation: Phase 4 (epic T-750), built bottom-up — auto-pause substrate, T-969 proxy (D-225), T-960 viewer, agent channel, agent capture. Geography is the first consumer.
-
Raised by: Jeroen + Claude (design), with Tyre (channel/pause/headless architecture) + Araminta (overlay encoding + affordance UX), 2026-05-24.
-
Cross-reference: D-225 (layer-stream proxy — the data path), D-166 (per-layer Atlas progress viewer), D-191 (Atlas viewer), D-169 / D-170 (implant components / HUD occlusion —
gameplay_occludedtrigger), D-200 / D-203 (execution tiers / LRU cache), Q-099 (mod content catalog),tests/run-visual(capture primitive),save_state.rs(save-inspection consumer). T-1112 amendment additionally: D-222 (Quarter terminology — the 512m unit this layer surfaces), D-234 (footprint geometry — the block-subdivision source the aggregates summarize), D-243 (quarter = 512m rung, and the containment ladder that makes a quarter sub-pixel at planetary projection — Araminta's no-outline rationale), D-010 (determinism — integer-only aggregates,BTreeMapkeying). -
Dissent: None
D-227: Deterministic-rebuild world model — derive-don't-store, cache, tile mutators, volumetric
- Date: 2026-05-25
- Decision: The walkable world is a pure function of
seed + atlas, materialised on demand and cached, never persisted as a tile grid.- Derive-don't-store.
subtile(x,y,z) = derive(seed, atlas, position)— deterministic (D-010), recomputed on demand, held in a transient by-chunk/region cache for performance, evictable (eviction → recompute, always valid). No generated tile/voxel is ever saved. - Volumetric, in voxels. Unit vocabulary, fixed here: a voxel is the 1 m cube (the 3-D framing of D-222's tile); a subvoxel is its 0.5 m subdivision — eight per voxel, the finest derivation/dig granularity, in all three axes. Derivation is 3-D — surface and subsurface — over this grid. The heightmap/atlas sets the top; below it a deterministic geology model (strata by depth → bedrock; per-region lithology; ore / aquifer / cave as 3-D noise) fills downward. Digging reveals
derive(...)for newly-exposed subvoxels — never "generated on dig"; the hole merely stops hiding seed-math. - Floors are semantic and variable-height, not a fixed voxel count. A floor — a walkable story, the z-level of D-049 / D-110 — groups voxels and defaults to 3 voxels (3 m) but is not locked: a cathedral nave or a hangar is one ~10 m floor, and that must be expressible. The voxel/subvoxel substrate is uniform and continuous in z; a floor exists wherever the volume is walkable void, and that void can be built (buildings), excavated (basements, mines), or natural (caves, lava tubes, caverns — derived in the geology model as 3-D voids). A cave is as much a floor as a basement; only truly solid rock is floorless. z-level addressing indexes stories whose real height in voxels varies.
- Vertical extent is physical, not a floor count. The old "±50 floors" framing was a stored-grid artifact (each z-level was a filled, stored layer with memory/fill cost). Derive-don't-store eliminates that cost, so: down is bound by the body's own geology (crust → bedrock → pressure/heat-impassable interior, a per-body depth tied to radius /
max_elevation); up is bound by max built-structure height (towers are built/authored or player-placed, not natural-derived) over free-to-derive air. This supersedes any ±50 cap — none was ever recorded as a D-record; it survived only as a 50-floor-skyscraper example in the generator-architecture workshop. - Persistence has two parts, and only one is mutators. (a) Tile mutators capture direct physical gameplay — the player or sim physically altering the world (pave a road, build a wall, dig a strip-mine, rocket-launcher the bank, fell a tree). Save =
seed + sparse mutator log; load = re-derive base + replay. (b) Live simulation state (the rolling economy now; NPC / storyteller state later) is history, not seed-derivable, so it is saved — but as sim-state, not as mutators. The derived economic base is handled by cache expiration — recomputed on expiry, and force-evicted on an event (e.g. the player tanks a system's economy) — while the rolling state persists alongside the save. The physical world stays re-derivable; the living state is the small saved delta on top. (Mutator op schema → Q-103.) - Determinism reclassified safety-critical. Because mutators reference derived state, any derivation drift (a non-deterministic algorithm, an f32 comparison/ordering,
HashMapiteration) desyncs the whole save — not merely a cosmetic difference. D-010's integer-only + ordered-collection discipline is load-bearing for saves, not just for golden tests.
- Derive-don't-store.
- Rationale: A world that stores its tiles cannot scale to body-sized 3-D volumes and bloats saves; a pure-function world with a transient cache + a sparse mutator log scales to any size, makes saves trivially small, and is the only model under which "dig anywhere, to any depth" is free (the subsurface was always computable — digging just reveals it). It also forces the determinism discipline the whole cascade needs anyway. The downward floor cap fell because it was solving a problem — per-layer storage cost — that derive-don't-store eliminates.
- Open sub-questions: the geology-model fidelity (simple depth-horizon stack vs tectonic-grade folding/faults) and how far
FloorMaterialis derived now vs deferred to the city layers (both tracked in D-228 / Q-101); the mutator op schema (Q-103). - Implementation: Phase 4+ (epic T-750). The caching substrate exists at the atlas level (
BodyWorldStateCache, D-203/D-225); the tile/voxel cache, mutator overlay, and geology derivation land as the cascade reaches the tile layers. No migration needed pre-save (D-202schema_versionlineage covers future drift once saves exist). - Raised by: Jeroen (derive-don't-store, volumetric, drop-the-floor-cap directives) + Claude, atlas-derivation workshop, 2026-05-25.
- Cross-reference: D-010 (determinism — now save-critical), D-222 (subtile/tile/chunk hierarchy), D-110 (signed z-levels), D-225 (layer-stream proxy + cache pattern), D-203 (LRU cache tier), D-224 (SeedChain — feeds
derive), D-228 (composite tile schema — the derived value type), Q-101 (refinement contract), Q-103 (mutator op schema), Q-104 (floor↔voxel-z mapping) - Dissent: None
D-228: Composite tile schema — orthogonal axes, derived shape, region-level morphology
- Date: 2026-05-25
- Decision: A tile's "type" is not a flat enum but a small bundle of orthogonal axes (all derived per D-227 — this is the cache's value type, not stored state). Named types ("fjord wall", "mountain pass", "river bank") are derived display labels, never stored: a fjord wall and a sea cliff are identical to the simulation (
Cliff + Rock + DeepWater), differing only in name.- Per-subtile axes:
TerrainMaterial(permanent natural ground — Soil / Sand / Gravel / Rock / Wetland / Lava) ·FloorMaterial(built surface over the ground — None / Concrete / Pavement / Carpet / Metal / …) ·Vegetation(ground cover — Barren / Grass / Scrub / Thicket / Forest / Crop / Cleared / …) ·Water(local depth state — Dry / Shallow / Deep) ·elevation(scalar, metres).SnowandIceare notTerrainMaterial— they are seasonally dependent, so they live in the seasonal cover overlay (Q-105), present only when the region's seasonal state puts them there (permanent only where climate never melts them — poles, glaciers). TheFloorMaterial/Vegetationvocabularies stay open, extended by the layers that own them. FloorMaterialandVegetationare override stacks, not single derivations. Each resolves in precedence wild/natural (biome + climate) → economic (managed: farmland, plantation, paving, clearance — from the settlement layer) → user (mutators: chopped, planted, built, demolished); the topmost present layer wins — the same derive-don't-store + mutator pattern (D-227) as the rest of the world.Vegetationcarries cover/concealment, the movement sound profile, and economic yield (timber / crops) on one axis — and managed crops follow the region seasonal cycle (sown → growing → ripe → harvested → fallow), so farmland visibly turns over with the year (Q-105).Wateris a dynamic depth state, plus derived flow. The axis is depth/wetness only; the water feature semantics (river / ocean / lake / sea / tidal-flat / delta) are the region morphology zone (below), not a per-tile value, and flow direction derives from the D8 network where moving water applies. Depth is time-varying via a deliberately cheap, deterministic, clock-bound water-height — a seasonal term phased by hemisphere (latitude sign) and a tidal term that exists only if the body has a moon (no satellite → no tide), amplitudes as body parameters. The water-height is a region property computed once per phase (not per tile, not per frame); per-tile flood state is then justregion-water-height vs local elevation. So floodplain / tidal-flat / seasonal-river emerge rather than being placed — a lowalluvial-plainfloods at high water and recedes at low. The static world stays static; only this region water-height carries the clock overlay (sim-time as the derivation input), recomputed on phase change — the dynamic-state path D-226 anticipates. Passability derives fromWater+elevation+materialtogether. (Model → Q-105.)- Derived, not stored: shape / geometry (flat / angled / drop / cliff) =
f(elevation-step × material)— sand slumps to an angle of repose, rock breaks to a vertical face on the same 1 m step; drives the rendered tile geometry. Tactical form (crest / hollow / channel / bank — cover, sightline, concealment) read from elevation curvature + water adjacency on demand. Storing either would mean updating 8 neighbours per elevation change, and both are clean functions of data the tile already holds. - Region-level (shared by all tiles in a region, not stored per-tile): morphology zone — fjord / delta / meander-reach / alluvial-plain / open-ocean / lake / sea / … — the whole-shape decision a single tile cannot see; it (a) governs which tile-tags get laid down, (b) carries affordances a tile can't (this inlet is a sheltered harbour), and (c) owns the water-feature semantics (river vs ocean vs lake vs sea) the per-tile
Wateraxis deliberately omits. Morphology resolves top-down (region → tile), never bottom-up — a tile cannot know it is in a fjord from its neighbours; deriving it per-tile would produce the squaring the model exists to avoid. Sub-biome, province, and the region's seasonal/clock state are likewise region properties — the season is computed once per region per phase and inherited by its tiles. That single cheap seasonal state drives water-height (flooding / tides), seasonal snow cover, weather, and the farmland crop cycle alike — one calculation, many dynamic overlays (→ Q-105). - Separate axes: biome (climate / sub-biome — region-level, authority unresolved, Q-100; distinct from, but informs, the per-tile
Vegetationcover), resource hints (bitmask: fertile / mineral / timber / groundwater). - Cohesion matrix (anti-squaring at the material layer): intra-region material + sub-biome scatter (a dirt patch in grass, a lone rock in beach sand) comes from a global, position-keyed continuous noise field — never per-chunk — so transitions never reveal chunk/grid seams. Straight lines appear only when authored (roads, plazas, field edges); a straight line must always have a placed cause, never be a generation-grid artifact. (Algorithm → Q-102.)
- Per-subtile axes:
- Amended 2026-07-02 (D-246 — intra-class micro-habitat mosaic): the
VegetationandTerrainMaterialvocabularies (left above "open, extended by the layers that own them") gain the values the sub-chunk micro-mosaic distributes over —Vegetation::Meadow(forb-rich herbaceous),Vegetation::Deadfall(dead woody debris — obstruction + fuel, no canopy concealment),Vegetation::Lichen(crustose/pioneer biological crust on rock/lava);TerrainMaterial::Hardpan(compacted flat crust — hard footing, does not slump like sand),TerrainMaterial::Scree(loose angular rock debris — unstable footing; the derived name is "talus" at a cliff foot, "scree" on an open slope). These are new values on existing axes, not a new axis — the composite schema is unchanged. They are front-loaded in one amendment (not grown incrementally) because each value is a distinct selectable outcome in the mosaic's weighted palette, so adding one later re-rolls the deterministic realization of every affected voxel. The full per-class vocab→axis table and the palette-key rule live in D-246. - Rationale: A flat enum explodes combinatorially (≈5×5×8 ≈ 200 mostly-incoherent variants), can't be queried by axis ("all water-adjacent tiles"), and grows a new variant for every new mechanic. Orthogonal axes are set independently by different cascade passes (material from sub-biome, water from drainage, shape from elevation), independently queryable, cheap to serialize, and compose without explosion. Names are visual/narrative and belong downstream of the simulation.
- Open sub-questions: how far the
FloorMaterial/Vegetationvocabularies are enumerated now vs deferred to the settlement layers; the exact morphology-zone vocabulary; the dynamic water-height model (→ Q-105). - Implementation: Phase 4+ (epic T-750), as the cascade reaches the tile layers. Today's
TileKind(D-049 render stack) is the seed of the per-subtile axes;SubBiomeVariant(D-210) is the region biome axis. - Raised by: Jeroen (FloorMaterial, shape-from-material, cohesion-matrix directives) + Claude, with the atlas-derivation workshop four — Tyre (composite data-structure), Gestalt (tag model + tactical form), Nigel (morphology-as-character), Burnelli (economic distinctions), 2026-05-25.
- Cross-reference: D-227 (derive-don't-store — these axes are the derived value type), D-010, D-210 (sub-biome — region biome axis), D-222 (subtile/tile), D-049 (z-stack render —
TileKindseed), D-208 (D8 — flow-direction source), D-226 (dynamic-state inspection — the water overlay), Q-100 (biome authority), Q-101 (refinement contract), Q-102 (cohesion-matrix algorithm), Q-105 (dynamic water-height) - Dissent: None
D-229: Building-property-tag schema — the step-3 fill output
- Date: 2026-05-25
- Decision: Define
BuildingPropertyTag, the typed step-3 output that replaces theStringstubs (era,society_profile,zone_palette,chunk_layout) on the skeleton. One tag per building footprint placed in a block; written once at plan time (inside the extendedGenerateSkeleton, D-230), read-only thereafter by three consumers — FillChunk (D-230), the guarantee audit (D-097), and the Phase-6 interior generator (D-231). Fields:zone_type_id: ZoneTypeId(Box<str>matching a RONid) — what the building is; the wire to the 31 D-142 zone-type RON files. Refined fromdistrict_mix.rs's per-blockZoningTypevia a generator lookup(ZoningType + economic_role + seed) → ZoneTypeId.footprint: TileRect— integer tile-space rect within the 128-tile block (D-010 integer-only).extent: FloorExtent— floor/basement extent; resolves Q-104 (below).entry_class: BuildingEntryClass— physical access character:Public | Commercial | Restricted | BreachOnly. NamedBuildingEntryClass, not "access tier", to avoid colliding with D-028's relational dialogue layers. Derived fromzone_type × layout_mode (D-096) × prosperity (D-197): Commission-Grid → formal/logged/corporate-or-authority credentials; Organic → social/reputation/unlogged credentials. U-curve degrade: low prosperity on a normally-Commercial zone →BreachOnly(derelict).flavor_ref: ArchitectureFlavorRef— which trait template characterizes this building (D-232); no rolling-economy read. (Amended 2026-07-08, T-994/T-1003: now an enum, not a bare index —InVocabulary(u8)(index into the body's closedtrait_selection, resolved via the phase-2 district-dominant pick)| Swerve(tag)(the rare out-of-vocabulary deviation draw, or the sparsity escape hatch). The original(seed + zone_type) → indexmechanism was the pre-three-phase-draw stopgap.)era: ConstructionEra(Founding | Established | Modern | Derelict) +era_cause: EraCause— feeds ZonePalette modifier axis C (D-101) and sets the D-217 condition floor. Derived fromfounding_age_years + prosperity_baseline + seed; a body carries mixed-era buildings (founding period anchors the distribution; seed scatters outliers).initial_condition: TileCondition— frozen-amber snapshot fromprosperity_baseline(D-197/D-217). The rolling condition overlay (D-198) paints over this; it never mutates the tag.FloorExtent { base_floor: i8, floor_count: u8, heights: FloorHeightProfile }whereFloorHeightProfile = Uniform(u8) | Variable(Vec<u8>). Q-104 resolution (the D-110 ↔ D-227 bridge): two pure functions —floor_at_voxel_z(z) -> Option<i8>andvoxel_range_for_floor(f) -> Option<(i32,i32)>— map D-110 floor-index addressing onto D-227 physical voxel-z. DefaultUniform(3)(3 voxels ≈ 3 m/floor, per Jeroen); a cathedral/hangar isUniform(10); a mixed-use stack isVariable([5,3,3,3,3]). TheVariablebranch carries per-floor memory only when floors actually differ.
- Rationale: A typed tag is the single contract that lets the atlas render a building, the guarantee audit validate a district, and Phase 6 seed an interior — all from one frozen object. String stubs cannot carry any of that. Orthogonal fields (what / where / how-tall / who-may-enter / cultural / era / condition) compose without a combinatorial enum explosion, matching D-228's axis discipline at the building scale.
- Implementation: Phase 4+ (epic forthcoming). Replaces the stub fields on
BlockSkeleton/QuarterSkeletoninserver/src/simulation/generator.rs. - Amended 2026-06-05 (T-957 — authored zone-type selection table): the
(ZoningType + economic_role + seed) → ZoneTypeIdlookup is made concrete for the planetary cascade. A third input,setting(SettingType), is added as a tweaker (not a surface/station switch — station & orbital bodies run a separate cascade per Q-109 and own the station-only idsresidential_station/extraction_space/port_space/rural_orbital, which this table never selects).zone_type_for(zoning, role, setting, seed)builds a candidate slice from the base table below, applies the setting tweaker, then deterministically seed-picks one id. Every base cell is non-empty (no empty slices — mirrors D-195's no-zero rule); an unknown role falls back to the ZoningType default.- Base table (planetary variants): Commercial →
[commercial_market, entertainment_hospitality](financial→[diplomatic_elite, commercial_market]; transit_hub→[commercial_transit, commercial_market]; service_mixed→ +entertainment_venue). Residential →[residential_surface](agricultural→[rural_agricultural, rural_pastoral]; extraction→[residential_dispersed, residential_surface]). Industrial →[industrial_manufacturing, industrial_freight](manufacturing→[industrial_manufacturing, industrial_processing]; extraction→[extraction_surface, industrial_processing]; agricultural→[industrial_processing]). Administrative →[administrative_civil](institutional→ +administrative_judicial, diplomatic_elite; research→[research_station, administrative_civil]; service_mixed→ +medical_facility). Transit →[port_surface](transit_hub→[commercial_transit, port_surface]; manufacturing|extraction→[industrial_freight, port_surface]). Recreational →[entertainment_venue, entertainment_hospitality](research|institutional→ +archaeological_site). Restricted →[security_checkpoint](military→[military_garrison, security_checkpoint, detention_facility]; research→[research_station, security_checkpoint]; institutional→[detention_facility, security_checkpoint]). Mixed →[residential_surface, commercial_market, administrative_civil]. settingtweaker (post-pass): Maritime/Water →port_surface⇒port_maritime(+port_fishingfor Transit),rural_*⇒rural_aquaculture,extraction_surface⇒extraction_platform. Agricultural → biasrural_agricultural/rural_pastoralinto Residential/Mixed. Wilderness → surfacewilderness_frontier/residential_dispersedat the frontier. Urban/other → base unchanged.- Entry-class U-curve threshold (gap fill): the "low prosperity on a Commercial zone → BreachOnly" degrade uses the D-217 bands —
prosperity_baseline_bps < 2300(the D-217 Broken band) →BreachOnly. Era follows this record as written (founding_age_years + prosperity_baseline + seed); the round-3 "distance-to-origin" note is dropped (not in the data model). FloorExtent uses the D-220 density-class floor midpoints with seed-jitter within the class range. Doors are not populated here —doors: Vec::new(); door derivation (D-231) is T-979.
- Base table (planetary variants): Commercial →
- Raised by: Tyre (schema + FloorExtent/Q-104), Gestalt (BuildingEntryClass, derivation), Miri (flavor_ref), economic-built-world workshop round 2, 2026-05-25.
- Cross-reference: D-142 (zone-type taxonomy), D-096 (layout mode), D-197 (prosperity baseline), D-217 (tile condition), D-110 (signed z-levels), D-227 (voxel substrate), D-101 (ZonePalette), D-028 (dialogue access — name disambiguated), D-230, D-231, D-232, Q-104 (resolved here)
- Dissent: None
D-230: FillChunk two-phase execution model — background-plan / on-demand-derive
- Date: 2026-05-25
- Decision: Building fill runs in two phases, split by latency budget. Plan phase (background, no budget): building-property tags + footprints are produced inside the extended
GenerateSkeletonRayon task — it already holds the fullCityGenerationContext, so footprint subdivision + tag assignment stay contiguous with the skeleton work. Output:DistrictWorldState { skeleton: DistrictSkeleton, block_tags: BTreeMap<(u8,u8), Vec<BuildingPropertyTag>> }, stored inBodyWorldState.districts: BTreeMap<DistrictId, DistrictWorldState>—BTreeMapeverywhere for D-010 determinism. Derive phase (FillChunk, on-demand, <5 ms): pure geometric shell derivation — read the cachedDistrictWorldState, and for each tagshell_derive(seed, footprint, extent, z) → {Void | Wall | FloorSlab | Roof}per voxel; fill interstitial tiles (street / open space) fromdensity_pct+ the D-215 pattern; applyinitial_condition. Cost estimate: ~40 k voxels/chunk × ~10–15 ns = 0.4–0.6 ms, well under budget even for tall skyscrapers. Pre-condition:FillChunkis only dispatched afterGenCompletion::SkeletonGeneratedfor that district has been processed; ifblock_tagsis absent, re-enqueue atHighand warn — never a blocking read on the main thread. - Rationale: The expensive, variable planning work (district mix, tag assignment, footprint layout) has no place under a per-chunk latency budget; doing it once in the background and reducing
FillChunkto cache-read + rectangle-containment + z-range lookup is what makes on-demand fill trivially fast and re-derivable (D-227). - Code-gap flagged (→ implementation ticket, independent of this workshop):
GenCompletion::SkeletonGeneratedcurrently returns onlycity_id: u64; it must carry theDistrictWorldStateback so the main thread can insert it intoBodyWorldState.districts. The completion routing is incomplete today regardless. - Implementation: Phase 4+. Amends D-200 (extends the runtime-background execution tier).
FillChunk/ChunkGenWorkerare no-op stubs today (gen_queue.rs,workers/stubs.rs). - Raised by: Tyre, economic-built-world workshop round 2, 2026-05-25.
- Cross-reference: D-200 (three-tier execution — amended), D-203 (LRU cache), D-225 (layer-stream proxy), D-227 (derive-don't-store), D-215 (arrangement patterns), D-220 (density), D-010 (determinism), D-229
- Dissent: None
D-231: DoorSpec and InteriorDescriptor — the step3→step4 boundary and the Phase-6 seed
- Date: 2026-05-25
- Decision: The door is the boundary descriptor between the generated exterior (step 3) and the lazy interior (step 4, Phase 6) — not yet an interactive object (that is Phase 5). Each building tag carries
doors: SmallVec<[DoorSpec; 4]>(≥ 1).DoorSpec { facing: CardinalDirection, door_class: Main|Service|Emergency|Hidden, entry_class: BuildingEntryClass, initial_state: Open|Closed|Locked|Sealed, credential: None|TemporalWindow(hours)|Corporate(corp)|Resident(block)|Authority|Social(f32), connects_to: Street(id)|AdjacentBuilding(block)|Interstitial, interior_descriptor: InteriorDescriptor }. Door-count derivation: 1Mainminimum;Serviceif the zone has a logistical function;Emergencyiffloor_extent.above_ground ≥ 2;Hiddenseeded by zone_type (research ~60% / admin ~20% / residential ~5%) —Hiddencarries the D-106 Rooftop-Bar discovery layer. Initial-state derivation: Public → Open/Closed, Commercial →TemporalWindow(zone hours), Restricted → Locked + credential, BreachOnly → Sealed. Mutation semantics (D-227): initial state is frozen-amber derived; runtime changes (faction lockdown, a picked lock) are tile mutators over the frozen base, never edits to the tag — the base is always re-derivable.InteriorDescriptor { zone_type_id, entry_class, floor_extent, era, flavor_ref, prosperity, layout_mode }is the complete Phase-6 seed: a future interior generator produces a deterministic floor plan from this descriptor +SeedChainwith no other system queried. The voxel at a door position is a conditionally-passable solid — solid until Phase 6 activates the interior, then void. - Rationale: Phase 6 (D-166) defers interiors, but its seed must be fixed now or step 3 cannot guarantee a generable interior later. Carrying the descriptor on the door (the exact threshold where generation will fire) keeps the lazy-interior contract local and self-sufficient, matching the planet-down-cascade "descriptor + catalog behind the door" rule.
- Implementation:
DoorSpec/InteriorDescriptorstructs land in Phase 4 (planned on the tag); the door-open fill is Phase 6.ChunkMutations/TileOverridealready exist as the mutator layer. - Raised by: Gestalt, economic-built-world workshop round 2, 2026-05-25.
- Cross-reference: D-229, D-097 (guarantee audit reads connectivity), D-106 (rooftop clause / hidden door), D-110, D-227 (mutators), D-142, D-217, D-166 (Phase-6 deferral)
- Dissent: None
D-232: Architecture-flavor — trait-template catalog (economically gated, seed-drawn, wiki-biased)
- Date: 2026-05-25 (round 3 — supersedes the round-2 per-body-Gemma-array draft of this record)
- Decision: The architecture-domain completion of D-167 (which retired the 7 abstract heritage roots and made the wiki the cultural register but never built the wiki→generator mapping). Cultural flavor is economically founded, seed-randomized from a shared trait-template catalog, with the wiki biasing the draw for hero bodies (Jeroen's model). This replaces both the round-2 per-body Gemma arrays and the rejected dimensional per-axis draw.
- The catalog (holistic templates, not per-axis traits). A single shared
architecture_trait_catalog.toml→trait_templatestable. Each template is a coherent bundle — a template defines a relationship between its axes, so it is never decomposed:{ tag, label, cultural_description, allow: [ObjectTag], block: [ObjectTag], zone_affinity: {DistrictType→weight}, era_scope, eligibility (two-tier, below), base_weight, visual_bundle (→ D-235) }. Per-axis mixing is forbidden (it produces incoherent grammar, e.g. stone walls + flat roof). - Two-tier eligibility — and role is per-layer, not global. Hard gates (
bulk_class,prosperity,production_ubiquity) exclude a template from the pool when failed. Weight modifiers (economic_role,dominant_faction,founding_age,geographic_sector,morphology_zone) multiplybase_weightbut never exclude. Key principle: the same input plays different roles at different layers.morphology_zoneis a hard gate on street geometry (D-234 — a fjord cannot have radial streets) yet only a soft weight on cultural-template eligibility. Economics founds and hard-gates what a building is (D-233) but only weights how it is characterized.geographic_sector(corridor) is always a soft weight, never a gate — "corridors are tendencies, not borders"; a hard corridor gate collapses the pool to ~7 templates/corridor → ~100 % within-corridor collision. - All eligibility numbers are integer basis-points (
min_prosperity_bpsetc.), neverf32— D-010 determinism, now save-critical under D-227. - The draw (three phases). (1) Body vocabulary (background, at skeleton time): hard-gate filter → weight (mods × wiki-bias) →
SeedChain-seeded weighted draw of K templates → stored astrait_selection: Vec<String>on the skeleton. K is locked tocomplexity_tier: Full = 5, Moderate = 3, Minimal = 1, Empty = 0 (Nigel's birthday math: K=4 fails within-corridor uniqueness at a 35-template catalog; K is not a range and not keyed to prosperity). The draw is coverage-aware — it must cover the body's actual district-type mix, not draw 5 templates that all starve the civic district. (2) District-dominant (at fill): each district draws one dominant template from the body's vocabulary, weighted byzone_affinity, applied whole to that district (Araminta's composition rule); secondaries surface on outbuildings/secondary streets and in their own high-affinity districts. The body vocabulary is closed by default (a mining town's civic hall looks like that town's civic — a feature). (3) Within-template variation: the fill seed picks within each template'sallow-lists per axis. - Deviation system — closed by default, rare tasty swerves. On top of the closed vocabulary, a rare per-building wildcard can draw a coherent whole template from outside the body's vocabulary (the Shinto temple in Amsterdam — a complete foreign building, never an axis-scramble). The swerve is cultural only; the building's function still passes the normal economic hard gates. Three sources, two opposed active drivers: foreign import (another corridor's grammar) driven up by cosmopolitanism / centrality / transit / Epicenter tier; heritage callback (the body's own corridor heritage pool) driven up by remoteness / isolation / conservatism (the cut-off Latin world that reaches back to haciendas); and the passive past-vogue holdover (just old, see era below). The sparsity escape hatch is the same mechanism triggered by necessity rather than dice — when the closed vocabulary genuinely cannot serve a district, it reaches the full catalog.
- Corridor = a two-part pool. Each corridor is authored as a baseline character (its default cohesive look — e.g. East-Asian: cyberpunk density + utilitarian industry) plus a heritage sub-pool (deep-history callbacks — East-Asian: fishing-village/temple enclaves; Iberian/Latin: haciendas, colonial-revival) tagged so the remoteness dial draws specifically from it. A shared cross-corridor pool feeds the foreign-import swerves.
- Era = maintenance/wear, NOT a material-tech ladder. The Reach is post-space-travel throughout; there is no stone→concrete→glass progression — fashion cycles fast and arbitrarily. A building's construction era (D-229, from distance to the founding origin) reads primarily as age/wear, realized through the condition layer (D-217 / D-198 — maintenance modulates how much wear shows). The "different-looking old building" is just the past-vogue holdover — the temporal sibling of the spatial swerve, one deviation system with sources in elsewhere and elsewhen. (This retires the round-2 era-band material-progression and the
era_fallback-as-tech-ladder;era_fallbacksurvives only as the asset-resolution fallback chain, below.) - Logical catalog vs visual-asset layer (the incremental-content split). The logical catalog — tags, eligibility,
allow/block, token references — is cheap data, authored complete and frozen at launch; the draw reads only this, so derivation is deterministic and stable forever with no catalog versioning needed. The visual-asset layer is a fallback hierarchy: every specific texture/material token declares a generic parent it degrades to (temple_wall_wood→ genericwood_wallplaceholder until the specific art ships, then it upgrades in place). Themes/textures are patched in incrementally behind stable tokens — a shipped world's structure is frozen at birth (the temple was always a temple), only its render fidelity sharpens. Versioning re-enters only if a genuinely new logical template is added post-launch (minimized by authoring the catalog generously up front). - Wiki bias (hero bodies only, ~30–40). A sparse
atlas_body_trait_biasrow:pin(mandatory, counts toward K),boost(≤ 3× weight),suppress(≥ 0.33×, never 0 — preserves second-playthrough surprise). Bias is per-body, never per-corridor. Non-hero bodies (the ~240 remainder) run the identical algorithm with no bias — economics + corridor weight + seed. - CI guardrails (Nigel, build-time validation): ≥ 5 templates eligible after hard gates per economic class; no single template > 60 % of pool weight after modifiers; bias is per-body; the catalog grows ≥ 1 template/dimension per new corridor or archetype.
- Catalog population: ~25–35 templates at floor, 40–45 target. Core hand-curated (Miri authors cultural meaning + eligibility; Araminta authors the matching
visual_bundle), then a bounded Gemma corpus-distillation pass (one read of all wiki, propose new templates, human-gated) — not per-body generation.
- The catalog (holistic templates, not per-axis traits). A single shared
- Channel separation from D-233 (held): D-233 decides what a building is (vocabulary, coverage); D-232 decides how it is characterized. Same inputs (e.g.
bulk_class) used non-conflictingly — D-233 as hard function gate, D-232 as soft cultural weight. They compose at fill. - Amended 2026-05-31 (D-237 — authored specialization layer): the template draw now reads an authored
cultural_specialization(new column onsystem_economy) that selects/biases the template pool when a system's cultural character diverges from its corridor baseline. The field carries two value sub-types in one column — activity/character values (agrarian,industrial_heritage,institutional,scholarly, etc.) and heritage values (scottish,vietnamese,zulu,french_provencal, etc.); heritage values take precedence where present. This is a Phase-4 correctness fix, not just flavor: without it, a system whose founding heritage diverges from the corridor (e.g. Vietnamese-founded Dài Lộ in the east_reach Korean/Japanese corridor) draws the wrong cultural templates.NULL= use the existing corridor-pool algorithm unchanged. Consistent with the held channel separation —cultural_specializationis a D-232 cultural-weight input, never a D-233 function gate. Singular landmarks (e.g. Groombridge's GSH within afinancial_hubdistrict) are expressed via D-222 multi-block reservation + anatlas_body_trait_biashero pin, not the system-level field. - Amended 2026-07-08 (T-1003 — deviation/swerve system implemented, driver inputs pinned): the two opposed drivers now map to real fields (
server/src/atlas/trait_swerve.rs): foreign-import scaled up byWorldTier::Epicenter/Passage(transit),dominant_faction = "mixed"(cosmopolitanism), and road/rail-graph node degree (centrality); heritage-callback scaled up by road-graph isolation (degree ≤ 1), remote tier (Waypoint/Backwater), andfounding_age_yearsbands (conservatism). Rates are integer bps: base 100 bps/building per driver, hard cap 300 bps — placeholder constants pending Nigel/Burnelli calibration; adist_ly-percentile remoteness input is deferred until the read-set carries it. The wildcard result is recorded asArchitectureFlavorRef::Swerve(tag)(out-of-vocabulary by construction); pools are hard-gate-eligible only (cultural-only rule held). The sparsity escape hatch is implemented as the same mechanism, necessity-triggered at the phase-2 district-dominant pick (deterministic max-weight, no dice). The passive past-vogue holdover stays on the D-217 condition layer, as decided. - Amended 2026-07-08 (T-994 — phase-2 "district" pinned to the D-243 tier): the word "district" in the phase-2 district-dominant draw means the D-243 2 048 m District cell (4 quarters), not the 512 m Quarter — this record was written after D-222 renamed the 512 m unit to Quarter, but the pin was never made explicit and the shipped code had no District-tier representation at all (caught in the 2026-07-07 /whats-next refinement; the "one template per district, applied whole" composition rule reads at 2 048 m). Implementation (T-994): the dominant template per
(DistrictType)is pre-resolved at L3→L4 dispatch time, seeded by(SeedChain::for_body, district-cell position, district type)— so any settlements whose quarters share a District cell independently derive the identical dominant template with no cross-settlement coordination, andassign_block_tagsis a pure lookup. Phase 1's body K-draw is likewise seeded fromSeedChain::for_body(never the per-settlement chain), preserving the closed-vocabulary invariant. - Supersedes (architecture/generator domain only): D-104 (
HeritageGrammarOverlay+ per-root data → the catalog +allow/block), D-105 (heritage-root→informal-zone lookup → flavor-filtered selection; the three zone types survive), D-101 modifier axis A (HeritageRoot→ catalog draw; axes B/C + faction/climate/condition/season unchanged), D-107 (per-root trauma decay → per-template/condition; the "trauma intensifies culture" principle survives). Also retires the round-2atlas_body_culture/atlas_body_culture_eratables. - Storage:
trait_templates(the catalog) +atlas_body_trait_bias(sparse, hero bodies). The per-body draw result lives astrait_selection: Vec<String>on the skeleton — re-derivable from catalog + economics + bias +SeedChain, no Gemma in the hot path.CityGenerationContextcarriestrait_selection+morphology_zone(replacing the round-2flavor_profile; D-199 amend). - Source-location deferred: the human-authored source home (catalog file +
bias.json) rides on Q-107 (wiki → Atlas content-set consolidation). The generator-facing tables are invariant to it, so the fill seam is unblocked regardless. - Implementation: Phase 4+ (mechanism + sparse catalog); visual themes fill in across Phase 5+ and post-launch behind the fallback chain.
ObjectTag/material vocabulary is Miri + Araminta co-maintained. - Raised by: Jeroen (the trait-template / economically-founded / wiki-biased model + the swerve, corridor-pool, era-as-maintenance, and logical/asset-split refinements), with Miri (lead synthesis), Burnelli (eligibility), Nigel (draw + variety math + guardrails), Araminta (visual bundle + composition), economic-built-world workshop rounds 2–3, 2026-05-25.
- Cross-reference: D-167 (corridor cultural system — completed here), D-223 (Gemma naming pipeline + corridor mixing), D-142 (zone types — the function baseline filtered by templates), D-228 (morphology zone), D-233 (economic channel — what vs how), D-234 (morphology→street — the hard-gate layer), D-235 (visual bundle + fallback hierarchy), D-217/D-198 (condition — where era's wear lands), D-224 (SeedChain — the draw PRNG), D-199 (read-set — extended), D-237 (authored
cultural_specialization— template-pool selection), D-101/D-104/D-105/D-107 (superseded in part), Q-106 (era reframed), Q-107 (source location) - Dissent: None
D-233: Economic signal → block-fill vocabulary — BulkClass × ProductionUbiquity
- Date: 2026-05-25
- Decision: Within a block of a given district type (D-194), the building vocabulary, coverage density, and interstitial character derive from two commodity signals for the settlement's dominant commodity, both read from the
systems.dbcommodity catalog (D-184) at build time — seed-independent, no D-199 tier split, no rolling-economy touch:BulkClass = BulkSolid | BulkLiquid | PrecisionDense | Perishable | NonPhysicalProductionUbiquity = Ubiquitous | Common | Specialist | MonopolySource- Coverage:
BulkClasssets a roofed-coverage fraction (NonPhysical 0.85–0.95 → PrecisionDense 0.75–0.90 → Perishable 0.50–0.65 → BulkSolid 0.25–0.40 → BulkLiquid 0.20–0.35), scaled bydensity_class(D-220) andprosperity_baseline(D-197). For bulk industries the non-roofed remainder is operations surface (haul roads, ore pads, conveyor runs, tank berms) — tagged built economic infrastructure, not interstitial open space. - Vocabulary: each
(BulkClass × production_tier)draws from a frozen building-vocabulary pool (e.g.mine_head/conveyor_run/tailings_area;tank_farm/flare_stack;cleanroom_facility/qc_lab;field_shed/cold_store;office_tower/civic_hall). A mine block places mine buildings, full stop. - Concentration/dispersion rule:
ProductionUbiquitysets the spatial spread of zone-type blocks, not per-block weight —Ubiquitousscatters small instances through mixed-use (the settlement doesn't read as "a water town");MonopolySourceconcentrates contiguous block groups (the mine is the settlement, everything else is support). - Residential follow-on: every non-residential production block emits a labor-demand signal sizing adjacent residential blocks (BulkSolid extraction ~3.0× → BulkLiquid ~2.5× → Perishable ~2.5× → PrecisionDense ~2.0× → NonPhysical ~1.5×); housing character from
prosperity_baseline(>0.7 market-rate → 0.4–0.7 standard worker → <0.4 company barracks/squatter).
- Frozen-amber: the vocabulary is t=0-derived and immutable; the condition overlay (D-198) paints Maintained/Worn/Abandoned but cannot change a
BuildingTag— a mine processing plant becomes an abandoned mine plant, never an office. - Amends D-199 (adds
dominant_bulk_class,dominant_production_ubiquity,morphology_zonetoCityGenerationContext) and D-184 (back-fills thebulk_classenum values it named but left open). Enforcement:fill_chunk(ctx, seed)holds noPressureStatereference. - Refined 2026-05-26 (batch refinement):
BulkClass's 5 values are built-form archetypes — a projection of the 8 commodity cargo-types incommodities.toml(bulk/standard/compact/oversized→BulkSolid[size is orthogonal to bulk-form; a future axis if it earns visual payoff],precision→PrecisionDense,liquid→BulkLiquid,perishable→Perishable,non_physical→NonPhysical). The 8 stay on commodities for the economic model — no fidelity is lost; the 5 are only the built-form driver. The settlement's dominant commodity is derived from its highest-output location-bound production chain (D-178), not an authored role→commodity table — so the built form reflects the already-authored production/trade data (where sector specialization lives) and varies body-to-body for free. - Rationale: Floor-area-to-output ratio is the capital structure of an industry, not a style choice — services maximize floor, bulk extraction is mostly open operations surface. Driving coverage and vocabulary from
BulkClassmakes settlements that share an economic role look physically distinct by what they make, and the ubiquity rule gives the settlement its identity (a MonopolySource town vs background infrastructure). - Implementation: Phase 4+.
bulk_class/production_ubiquitylookups added to the economics import; consumed in the plan phase (D-230). - Raised by: Burnelli, economic-built-world workshop round 2, 2026-05-25.
- Cross-reference: D-194 (district mix — upstream), D-184 (commodity catalog — amended), D-199 (read-set — amended), D-220 (density), D-197 (prosperity), D-142 (zone types — downstream), D-176, D-180 (EconEvent — overlay trigger), D-198 (condition overlay — amended), D-229
- Dissent: None
- Re-amended 2026-05-31 (D-237 — authored specialization layer): The settlement's
dominant_bulk_classanddominant_production_ubiquityare now sourced from the authored specialization layer (D-237) rather than derived directly from the highest-output production chain. Derivation chain: (1) Ifsystem_economy.economic_specializationis authored → resolve viaspecialization_vocabulary(economic_specialization)→(commodity_id, production_ubiquity_override)→(BulkClass, ProductionUbiquity). Use the vocabulary-specifiedProductionUbiquity(which may override the catalog default per the equal-or-higher rule). (2) Ifeconomic_specializationis NULL → run the deterministic-varied heuristic (D-237 §fallback: seed-hashed weighted draw over the vocabulary usingeconomic_role+ corp HQ signals + noise term) → same projection. In both cases the resolved(BulkClass, ProductionUbiquity)populatesCityGenerationContext.dominant_bulk_classandCityGenerationContext.dominant_production_ubiquity. Frozen-amber constraint unchanged:fill_chunk(ctx, seed)holds noPressureStatereference. Within-system body variation (weighted draw over body-level candidates anchored to system commodity) remains seed-hashed. Groombridge clarification: The vocabulary expresses district-level character. Singular MonopolySource-class landmarks within a Specialist district (e.g. GSH within Groombridge'sfinancial_hubcluster) are expressed via D-222 multi-block reservation + D-232 hero-element pin, not via composite vocabulary values.
D-234: Morphology zone → street and footprint constraints
- Date: 2026-05-25
- Decision: The D-228 region morphology zone constrains street geometry and footprint subdivision so terrain shapes settlement form — the variety guardrail that stops the generator placing a radial-core city in a fjord. Two rules:
- (a) Permitted street patterns by morphology — a lookup that gates the D-215 archetype arrangement patterns: fjord/canyon → ribbon or hub-and-spoke only (streets linear along the terrain axis, radial impossible); delta/braided → hub-and-spoke following channels (bridges as forced nodes); alluvial-plain → any pattern; island → hub-and-spoke (perimeter access priority); mountain-pass → ribbon only (elevation steps as block boundaries).
- (b) Waterfront footprint rule — any block adjacent to a water morphology feature uses pier/quay geometry on the water-facing edge (no standard setback, dock-orthogonal subdivision, access priority toward the water) and standard street frontage on inland edges. Applies uniformly wherever a block touches a water feature — body-independent, zone-independent. This is what makes port towns present to the quay differently than to the street.
- Street geometry obeys the ±45° pathfinding cap (D-096) — no curves; straight lines appear only where authored (the D-228 cohesion rule). Fills the Step-1
chunk_layout/corridors/access_pointsstubs.
- Refined 2026-05-26 (street-network algorithm — two layers): streets generate in two layers, mirroring real cities and the Civ road lineage. Arterials (
corridors) = a minimum-spanning / least-cost graph over the district's key nodes (access-points, reservations, landmarks) — emergent trunk topology (Civ6-style, "roads where traffic wants to go"), ±45°-snapped; this is the node/edge graph the guarantee audit (D-097) reads (a chokepoint is an arterial bottleneck; an encounter-corridor is an arterial through-route). Ribbon = MST on linearly-constrained nodes; hub-and-spoke = MST with a forced centroid hub. Local streets (chunk_layout) = a ±45° grid lattice within each block (Civ4-style), modulated by the D-096 Grid/Organic mode (offsets + rotation). Voronoi was rejected — its arbitrary-angle edges violate the ±45° cap and its irregular cells break the axis-alignedTileRectfootprint fast-path (D-229), degrading per-chunk fill from rectangle-containment to polygon rasterization. (Civ1–4's "road on every tile" is the anti-pattern this avoids; the arterial/local split is the city-scale fix.) - Rationale: Morphology zone and
architecture_flavorwere added to the context (D-199) as inputs without consumers — two economically-similar bodies otherwise produce topologically identical street networks and identical block subdivision (Nigel's same-y-grid failure). These two rules give morphology structural teeth so the fjord port and the delta port are categorically different cities, not reskins. - Implementation: Phase 4+. Consumed in the plan phase (D-230) when laying streets and subdividing blocks.
- Implemented 2026-06-06 (T-957): the street network + footprint subdivision land in
skeleton_gen.rs(absorbing T-976 into T-957 — one coherent walkable-quarter deliverable).AccessPoint/CorridorSpine/ChunkLayoutare now typed structs (wereStringstubs): access nodes from road-entry octants + reservation gates; arterial corridors as a morphology-gated graph (Ribbon for fjord/canyon/mountain-pass, HubSpoke for delta/island/enclosed water, Prim-MST mesh otherwise), ±45°-snapped (a); per-block local lattice modulated by D-096 Grid/Organic. Footprint subdivision is a density-scaled BSP into axis-alignedTileRects with D-233 BulkClass roofed-coverage. Waterfront rule (b): the water-facing quarter edge (from the settlement'sCoastalfounding orientation, D-213) drops its street setback to 0 so buildings present flush to the quay. The water bearing itself is now extracted in Layer 1 (TerrainAnalysis::water_bearing, 8-octant integer) and fed into D-213 founding orientation (T-956's0stub is gone for coastal/river settlements). Pending dependency: the waterfront rule readsCityGenerationContext.founding_orientation, whichcity_context_readerstill stubs toCardinal— real per-settlement orientation only reaches quarter generation once the Layer-3 placement → Layer-4GenerateSkeletondispatch is wired (it must copy the placement's founding orientation into the context). That cross-layer dispatch is the remaining integration; the rule is correct and tested, awaiting its input pipeline. - Raised by: Nigel, economic-built-world workshop round 2, 2026-05-25.
- Cross-reference: D-228 (morphology zone), D-215 (arrangement patterns — gated), D-096 (layout mode / ±45° cap), D-220 (density), D-229, D-213 (founding orientation — water bearing), Q-106, Q-109
- Dissent: None
D-235: Building exterior visual grammar and material vocabulary
- Date: 2026-05-25
- Decision: The visible-form layer over the tags.
(template × zone_type × density) → BuildingExteriorTag { wall_material, roof_form, setback_tier, facade_rhythm, color_range }, wheretemplateis the district's dominant trait template (D-232), derived in three steps: (1) the template'sallow/blockfilters the available material + roof token set (a template is a coherent bundle — its axes are chosen together, never mixed across templates); (2)zone_typebiases within the filtered set (probabilistic — the template can override, e.g. anallow: [stone_base]industrial block stays stone, not corrugated metal); (3)densitysetssetback_tier(Dense →zero_lot→ … → Frontier →campus), which drives interstitial type (void / court / garden / plaza / dock_slip / market_pad / open_lawn). The fill seed then picks within each axis's allow-list. Era is NOT a material filter here (D-232 reframe): the Reach is post-space-travel throughout, with no stone→concrete→glass tech ladder — material choice comes from the template (fashion), and era reads as maintenance/wear via the condition layer (D-217), with the occasional out-of-vogue building handled as a deviation (D-232's past-vogue holdover), not a material rule.- Exterior vocabulary (extends D-228's FloorMaterial axis):
WallMaterial(stone_cut/stone_rough/fired_brick/clay_render/heavy_timber/pile_timber/timber_frame/reinforced_concrete/corrugated_metal/steel_panel/composite_panel/precision_glass/smart_facade),RoofForm(pitched_steep/pitched_shallow/flat/composite_curved/corrugate_shed/dome),FacadeRhythm(bay_window/grid_panel/solid_punched/arcade/open_front/blind_wall),StreetSurface(cobble/packed_earth/poured_slab/elevated_boardwalk/dock_plank/rail_embedded — derives from district + density + template, the same filter as buildings, for a consistent world). Color is template-bounded (cultural palette cue), seed-selected within range — always within the template's register. - Fallback hierarchy (the incremental-content mechanism, D-232). Every specific texture/material token declares a generic parent it degrades to:
temple_wall_wood→ genericwood_wallplaceholder until the specific asset ships, then it upgrades in place. The logical token a building uses is fixed at generation (deterministic, frozen); only its rendered fidelity sharpens as themes/textures are patched in. This is where incremental content lives — an asset-resolution concern, not a generation one — so no catalog versioning is needed. (era_fallbackfrom the round-2 draft survives only in this generalized form — a fallback chain, not a tech ladder.) - Worked example: a fjord port (template
fjord_maritime→ stone base + steep roof + zero-lot + solid-punched + cobble) and a delta port (templateriver_delta→ pile-timber + shallow roof + arcade + boardwalk) share density and zone types yet read as completely different cities. - Amended 2026-07-07 (T-995 — ObjectTag vocabulary ratified, resolves Q-049): the
WallMaterial/RoofForm/FacadeRhythm/StreetSurfaceexample token lists above never shipped. The canonical ObjectTag vocabulary is the shipped 28-templatearchitecture_trait_catalog.toml(T-1005) material palette, now formalized as a machine-readable registry atwiki/economics/object_tag_vocabulary.toml, importer-validated byeconomy_import/traits.py(V-TT-03 existence, V-TT-04 fallback-graph). The ratified tags, by axis: wall (10) —concrete_wall stone_wall brick_wall rendered_wall stucco_wall timber_wall rammed_earth_wall steel_frame glass_curtain_wall composite_panel; roof (7) —flat_roof pitched_roof corrugated_roof clay_tile_roof terraced_roof vaulted_roof green_roof; facade (8) —regular_facade ornamental_facade industrial_glazing arcade_facade shuttered_facade screen_facade colonnade lattice_screen; street (7) —paved cobble packed_earth canal_way elevated_walkway heavy_haul boardwalk; plus the four fallback-terminal generic placeholders —generic_wall generic_roof generic_facade generic_street. Every specific tag's registry entry declares its generic fallback parent directly (the per-templatefallbackmaps in the catalog remain illustrative/non-exhaustive documentation, not the validated source).
- Exterior vocabulary (extends D-228's FloorMaterial axis):
- Formally retires Araminta's generator-architecture Round-4
D-READY-9ten-root heritage-modifier TOML system — superseded by the D-232 trait-template catalog (body-specific draw, allow/block-bounded, no root taxonomy). - Rationale: The template is the differentiator, but it needs a concrete visual vocabulary to act on, filtered consistently across walls, roofs, facades, and streets or the world reads incoherent. The "old quarter vs new development" texture comes from wear + occasional past-vogue holdover (D-232), not from a material-technology ladder — because in a post-space-travel setting there is no such ladder. The fallback hierarchy lets the logical world be complete and frozen at launch while the art catches up over patches.
- Implementation: Phase 4+ (the token logic + Atlas-level data); textured render + the bulk of the theme library are Phase 5+ and post-launch, behind the fallback chain.
ObjectTag/material vocabulary is Miri + Araminta co-maintained. - Raised by: Araminta, economic-built-world workshop round 2, 2026-05-25.
- Cross-reference: D-228 (composite tile / FloorMaterial — extended), D-232 (flavor — the filter), D-217 (condition / heritage markers), D-142 (zone types), D-220 (density → setback), D-106 (height tiers)
- Dissent: None
D-237: Authored per-system specialisation layer — economic_specialization + cultural_specialization + dominant_faction
-
Date: 2026-05-31
-
Decision: Each inhabited star system carries three authored per-system identity fields, statically compiled from
wiki/economics/system_specialization.tomlat build time viaimport_economics.py. These fields constitute the lore-authored identity layer above the heuristic and are the primary source for D-233/D-232/D-214 generator inputs for named systems. All three fields are static (t=0), deterministic (D-010), and NULL-safe (NULL = heuristic fallback runs).(1)
system_economy.economic_specialization TEXT(new column). A 27-value curated vocabulary compiled fromwiki/economics/specialization_vocabulary.tomlinto aspecialization_vocabularyDB table. Each value maps to(commodity_id, production_ubiquity_override_or_null)→(BulkClass, ProductionUbiquity)for D-233. Scale is encoded in the value (no separate scale column):breadbasketpinsProductionUbiquity = Specialistwhere the catalog default would beUbiquitous;terroir_agriculturepinsMonopolySource. Equal-or-higher rule: aproduction_ubiquity_overridemay only be equal or higher concentration than the commodity's global default (CI hard error V-SES-03). Authored for ~80–100 named systems; deterministic-varied heuristic fallback for ~200 unnamed systems (seed-hashed weighted draw over the vocabulary: W_ROLE_PRIMARY=8,000 bps, W_CORP=5,000 bps, W_NOISE=400 bps to every vocabulary value; same seed+system_id always produces the same value; the noise term ensures ~5–10% of unnamed systems draw an off-primary value, preventing all unnamed agricultural worlds from being identical).Relationship to
economic_base_primary(the seam — clarified 2026-06-01):system_economy.economic_base_primary/_secondaryis pre-existing free-text prose (e.g."fusion_fuel, financial_services") rendered into the wiki index.md "Industries/Exports" infobox bywiki_sync.py.economic_specializationis a deliberate parallel representation at higher fidelity, NOT a duplicate: the prose is uncomputable GTTR flavor for human readers; the enum is the machine-actionable projection the D-233 generator consumes. They are the same concept (what the system produces) at two resolutions. The two must stay mutually consistent —economic_specializationshould never contradict theeconomic_base_primaryprose for the same system (the T-1016 content pass authors the enum from the same lore the prose describes, and CI soft-warning W-SES-03 flags prose/enum divergence). Unification onto one source was considered and rejected: the prose carries multi-sector nuance and narrative voice the enum cannot, and the enum carries the deterministic (BulkClass × ProductionUbiquity) projection the prose cannot — collapsing either direction loses information. systems.db is canonical; index.md infobox is a read-only DB projection; only the index.md prose sections (Supply Dependency / Faction Notes / …, round-tripped bywiki_syncPROSE_SECTIONS) are authored in-place.(2)
system_economy.cultural_specialization TEXT(new column). Directs D-232's template pool when a system's cultural character diverges from corridor baseline. Single field carrying two value sub-types: activity/character values (agrarian,industrial_heritage,institutional,scholarly,artistic,financial_technocratic,cosmopolitan,compact_cooperative, etc.) and heritage-type values (scottish,vietnamese,zulu,afrikaans_cape,tagalog,chinese,italian_northern,french_provencal,norse_compact, etc.). Heritage values take precedence when both apply.NULL= use corridor default (D-232 existing algorithm). Authored for ~60–100 systems where the corridor default would produce wrong D-232 architectural draws in Phase 4 (not a thin Phase 6 concern: a Vietnamese-founded system in the east_reach Korean/Japanese corridor draws the wrong templates without an explicit pin). Does NOT replacesystem_culture.cultural_register(prose NPC/dialogue voice); consumed by the physical generator only.(3)
system_factions.dominant_faction TEXT— populated, not added. Column already exists (D-199 field 4); VALUES for named systems are authored in this pass viasystem_specialization.toml. 8-value vocabulary:concord_assembly | compact | compact_sympathetic | syndic_dominant | veil_institute | independent | disputed | mixed.lattice_commissionis dropped — the Commission is a regulatory body under Assembly authority, not a governing faction; ACB and Bastion areconcord_assembly. Authored for ~40–60 named systems; existing Commission-presence + currency-zone derivation serves as fallback.Groombridge landmark mechanism: Where a system's authored identity includes a singular MonopolySource-class institution within a Specialist-scale district (Groombridge:
financial_hub/Specialist + GSH clearing house), the district character is encoded as Specialist in the vocabulary. The singular landmark is expressed via D-222 multi-block reservation + D-232atlas_body_trait_biashero pin — not via a composite vocabulary value. The vocabulary expresses district-level character; landmark-level identity is the D-232/D-222 mechanism.CI guardrails: 6 hard errors (vocabulary FK integrity, inhabitance coverage, production_ubiquity monotonicity, vocabulary constraint for all three fields); 8 soft warnings (MonopolySource review list, distribution balance, prose/field divergence, D-172 faction/currency alignment, Compact monoeconomy flag, authoring gap lists). Full CI spec in
docs/workshops/system-economic-specialization/workshop-outcomes.md §8.Source:
wiki/economics/system_specialization.toml(per-system entries) +wiki/economics/specialization_vocabulary.toml(vocabulary definition). Both added toIMPORT_ECONOMICS_SOURCESfor meta-stamp tracking.system_economygains two new columns viaCOLUMN_MIGRATIONSinimport_economics.py;specialization_vocabularyis a new read-only reference table. -
Rationale: The pre-existing D-233 heuristic (
economic_role+ corp HQ +planet_class→ dominant commodity) fails predictably for: (1) output-vs-input confusions (Cygni B: heuristic sees ore inputs, not freight_hauler outputs); (2) political mandates (Groombridge: financial infrastructure is an institutional choice, not geographic necessity); (3) terroir-locked production (D-177 systems: catalog defaults are globally ubiquitous, not locally monopolistic); (4) service economies (Prometheus, Keid: no heuristic path from available signals to the correct identity). The three-field authored layer resolves all four failure modes. Three fields are the minimum that changes how a system reads without redundancy: economic → D-233 block fill, cultural → D-232 template selection, faction → D-214 political archetype.cultural_specializationis a Phase 4 correctness requirement, not Phase 6 enrichment. -
Raised by: System economic-specialization workshop, 2026-05-31. Burnelli-Sheldon (schema, vocabulary, CI, fallback algorithm), Miri (must-pin list, scale gap, D-177 terroir pins, cultural heritage vocabulary), Paula (faction vocabulary, Compact rules, heritage-divergence cultural scope, D-172 compliance), human checkpoint (dual-axis design, scale-in-vocabulary, faction authored, Groombridge landmark mechanism, deterministic-varied fallback).
-
Dissent: None.
-
Cross-reference: D-233 (re-amended — economic block-fill now sourced from this layer), D-184 (commodity catalog), D-199 (CityGenerationContext read-set — extended), D-232 (architecture-flavor template draw — consumes
cultural_specialization), D-214 (PoliticalArchetype — consumesdominant_faction), D-172 (currency zone alignment —dominant_faction = compactvalidates), D-174 (shadow economy intensity —compactelevates), D-177 (productivity constraints — informs monopolistic pins), D-220 (density), D-197 (prosperity), D-222 (multi-block reservation — Groombridge landmark mechanism), D-178 (rolling economy — not touched; this is t=0 authored layer only)
D-239: Tile derivation contract — coarse→fine refinement chain (resolves Q-101)
-
Date: 2026-06-07
-
Resolves: Q-101
-
Decision: The walkable tile is materialised by a three-carrier refinement chain, each stage a pure deterministic function of
(seed, atlas, body-params, position)per D-227:RegionProfile(~1 km) →ChunkContext(64 m) →VoxelColumn(1 m). Each scale boundary is its own derivation with its own failure modes.(1) Determinism — no authoring at the derivation layers, ever. L1–L5 derivation has zero per-body override hooks. All authorial control lives upstream at the body-parameter / atlas / system-specialization layer; all gating params (
RIVER_THRESHOLD— a derived per-body-class value(hydrosphere, tectonic_activity, precipitation_class) → threshold, not the global200;tectonic_class;GlaciationGrade;precipitation_class) are derived from body params (stellar type / orbit / hydrosphere / lithology / temperature history), never authored. Lore-anchored bodies (Kallast = plains, Velen = coast, Cygni B = volcanic) are honoured by setting params and stand as validation cases — if a body reads wrong, fix its params, never patch the derivation. Validation caveat: derived terrain can only honour a lore body if its params permit it (e.g. tidal flats require a moon param for the D-228 tidal term — verify Velen's params before treating its coast as a contract).(2) Climate — district temperature is the primitive; moisture is a separate primitive. A simple scalar district temperature in °C, resolved per 2×2 km district, nullable, derived from: the sun (luminosity + insolation), the planet (the district's latitude + elevation lapse rate + orbital/axial phase = the season term, and day-phase = the diurnal term), and the atmosphere (greenhouse → the base, and heat-retention → the diurnal-swing amplitude; thick air = small day/night swing, thin = large). No atmosphere → temperature is
null, and the entire climate/vegetation/weather branch is simply absent (an airless body's surface ice is geology per D-227, not climate). Tiles inherit their district's temperature. Moisture is the second primitive (water availability, fromhydrosphere). Everything climatic derives from temperature (+ moisture): precipitation = f(temp, moisture); vegetation/treeline = temperature bands × elevation; long-term / seasonal-minimum temperature →GlaciationGrade; the cheap region seasonal/clock state (Q-105) is literally temperature(time). This formalises D-210's temperature proxy into the keystone scalar. No authored climate inputs anywhere.(3) Freeze & snow — a scattered phase transition, transient in the marginal band. Ice/snow is not a hard
temp < 0contour but a band with deterministic, spatially-coherent scatter (seed-noise → a ragged, natural freeze line + microclimate; clustered patches, never per-tile dice), branched by surface type:- Fresh / "sweet" water (lakes, rivers): scatter band +5 °C → −10 °C (district mean) — the upper +5 is night-frost (the diurnal swing) reaching the coldest / most-exposed tiles even when the mean is above zero; −10 = frozen across the whole day/night cycle.
- Salt water (oceans): its own lower, wider band (seawater onset ≈ −2 °C) with a distinct pack-ice spatial pattern (sheets + leads, not a lake skinning over).
- Snow (land): the land parallel, gated on moisture (cold + wet → snow accumulates; cold + dry → bare frozen ground), same scattered band.
- Transient: because temperature is clock-bound (season + day/night), in the marginal band ice/snow forms in the cold phase and melts in the warm phase — dawn frost burns off, a stream iced at dawn is crossable by noon. Passability is therefore time-of-day dynamic. (Forward contract to Q-105.)
(4) Anti-"squaring" (warp). A global position-keyed domain-warp field suppresses grid/seam artifacts — a stateless, hash-based pure function of
(seed, body_id, position)(no lookup table / thread-local cache, so order-independent across threads/platforms). It keeps f64 sub-metre precision through to the final voxel coordinate, then quantises by truncation to the integer voxel address (a cast, not a comparison → IEEE-754-deterministic across targets). The ±8 m warp range makes FMA-contraction ULP variance (~1e-15 m) unable to shift the rounded voxel, so no platform guards are needed. The warp is position math, not a structural decision — D-010 integer discipline on all material/morphology decisions is preserved.(5) Morphology — 8 families via a strict decision tree over integer
RegionProfileinputs: LavaField · FjordWall · CliffCoast · BraidedDelta · DuneStrand · IncisedGorge · MeanderReach · AlluvialPlain (fallback). Addstectonic_classtoRegionProfile. MountainPass is a zone label sharing IncisedGorge geometry. Hard gates (boolean, pre-selection, in tree order): fjord requiresGlaciationGrade ≥ 2; LavaField requirestectonic_class = Volcanic; lithology bounds slope/form (see laws).(6) Frozen 17-zone
MorphologyZonedisplay vocabulary (derived labels per D-228, enum→label map frozen). The canonical freeze point is the RustMorphologyZoneenum; adding/renaming/removing a zone requires a D-record amendment (reviewer-enforced; classifier tuning that merely re-classifies a region is not a vocabulary change and stays free). The 17: open ocean, lake, tidal flat, dune strand, cliff coast, fjord, delta, estuarine, alluvial plain, river bank, meander reach, braided plain, valley floor, mountain pass, alpine, volcanic, wetland. Zones ≠ families: four (tidal flat, estuarine, alpine, wetland) are derived sub-classifications from family + elevation/water-height, not distinct generator families.(7) Zone seams — prevent incompatible, allow valid (never patch). The boolean gates are discontinuities, so "continuous inputs → continuous classification" is not the mechanism. The mechanism is decision-tree gate ordering + a build-time compatibility-matrix invariant: incompatible family pairs (e.g. MeanderReach↔Volcanic) cannot be adjacent classifier outputs (a build-time test, never a runtime override). Valid geomorphic seams (cliff↔fjord at the glaciation threshold, lithology faults) are permitted sharp transitions — kept sharp (real geology is sharp), made non-grid/organic by the warp; no modulation feathering. The warp also prevents degenerate slivers at chunk scale.
(8) Believability laws (binding): drainage monotonicity (respect the D8 thalweg, D-208; tributaries join upstream; mouths at sea level); lithology→landform (Rock → vertical faces; Sand → ≤~32° angle of repose, dunes not cliffs; Gravel → braided channels/fans not single-thread meander; Soil → rolling/floodplain; Wetland → ≤5° flats; Lava → sheets/shield slopes + tubes, immature drainage); glaciation→form (fjord ≥2, U-valleys ≥1, moraines ≥1, cirques ≥2; grade 0 = V-ridges, never glacial U); climate→vegetation (treeline Forest→Scrub→Barren, no skip; riparian Thicket/Scrub 1–3 tiles along perennial waterways) — all flowing from the temperature primitive.
(9) Game-feel constraints: every chunk carries ≥1 tactical decision point; cover ≠ concealment; high-ground asymmetric-but-not-dominant; chokepoints narrow enough to matter (river crossings 3–15 m, gorge floors 2–8 m); seasonal/tidal state produces real passability changes, not cosmetic — BraidedDelta/MeanderReach
ElevationDeltacalibrated so channels fall below and levees above the Q-105 high-water threshold.(10) Mechanics: no per-tile
flow_direction[64×64]inChunkContext(D8 ≈ 152 m/cell, coarser than a chunk) → one basin-direction + global meander-curve params; features with wavelength > 64 m seed from Region-or-higher, not the chunk seed.RegionProfileis stored inBodyWorldState(D-203, ~6k/body, populated in the D-206 background pass) so the Atlas reads zone labels without triggering voxel derivation; voxel derivation is on-demand + cached, never persisted (D-227). Budget ~2.2–4.2 ms/chunk (validate per-family in Phase 4 — FjordWall/IncisedGorge are far costlier than the AlluvialPlain fallback). -
Rationale: D-227/D-228 fixed what a tile is (derive-don't-store voxels; orthogonal derived axes); D-239 fixes how the finished upper cascade (L0–L4) becomes actual tiles — the one unbuilt layer. The district-temperature primitive collapses the scattered climate inputs (precipitation, glaciation, season, snow/ice, vegetation) onto one derived scalar + moisture, keeping the whole climate branch deterministic and null-cutting airless bodies cleanly. Prevent-at-source seams + the warp give coherent, organic terrain without a runtime patch. The frozen vocabulary protects authored Atlas/wiki content while letting the classifier evolve.
-
Raised by: tile-derivation-contract workshop (Tyre — refinement chain, warp, determinism; Gestalt — 8 families, game-feel; Troblum — feasibility, warp precision, scale corrections; Miri — believability laws, vocabulary, lore reconciliation), lead-interviewed decisions + an adversarial verification pass, 2026-06-07.
-
Dissent: Tyre's initial cross-family elevation-blend was resolved against (prevent-at-source). Early-integer-truncation of the warp (raised against Gestalt's
ElevationDeltaranges and by Tyre) was resolved against in favour of f64-to-voxel. -
Implementation note (T-1024, 2026-06-07): §1 per-body
RIVER_THRESHOLDis now a derived field (derive_river_threshold) onRegionProfile. §2 district temperature is a nullablef32onRegionProfilesampled from a per-region climate derivation (derive_temperature_c); moisture is a separate integer primitive (derive_moisture_q, 0–100). Both populate theRegionProfilecarrier during the L4 cascade run. Climate inputs use a hybrid strategy: stellar luminosity and spectral class are sourced from the import pipeline (bodies.axial_tilt_deg,star_systems.spectral_class); greenhouse offsets and diurnal amplitudes are tunable at runtime viaserver/data/climate_constants.toml(source-canonical TOML, not hardcoded). -
Implementation note (T-1025/T-1027, 2026-06-08): §6's frozen 17-zone
MorphologyZoneenum is realised inserver/src/simulation/generator.rs(repr(u8), discriminant-pinned). The §5 8-family gated classifier + §7 compatibility-matrix invariants live inderive_morphology_zone(region_profile.rs). 16 of the 17 zones are reachable at RegionProfile scale;BraidedPlainis the exception — distinguishing it fromDeltaneeds a lithology signal (§8 Gravel→braided) thatRegionProfiledoes not carry, soBraidedPlainis deferred to ChunkContext sub-classification. The §7 compatibility invariant is enforced as a classifier-gate-ordering property (a build-time test that a single region cannot yield a forbidden pair), per §7's "build-time test" language; genuine cross-region sharp seams (cliff↔fjord, lithology faults) remain permitted. §2 climate-derived fields (precipitation_class,glaciation_grade,vegetation_class) all derive from the temperature(+moisture) primitive (T-1025). -
Amendment (T-1082, 2026-06-28): §5's family set grows from 8 to 9 — a
WaterBodygenerator family is added for the water zones OpenOcean / Lake / TidalFlat, which previously fell through to the dry-landAlluvialPlainfallback (the D-245 believability bug: oceans rendered as dry forested land —water=Dry+Foreston the sea).WaterBodylaysWater::Deep/Shallow(a Shallow shoal band on the district-anchored coast linecoast_anchor_m, Deep beyond; TidalFlat is all-Shallow), a seabedTerrainMaterial(Rockon steep districts /Sandon gentle /Wetlandfor tidal mud),Vegetation::Barren, the water surface atelevation_m = 0(the §8 mouths-at-sea-level convention), and seasonalIceviaderive_cover(frozen seas). This refines §6: open ocean / lake / tidal flat are still derived zone labels, but their generator is nowWaterBody, notAlluvialPlain— so §6's "tidal flat … not [a] distinct generator famil[y]" is superseded for the dispatch of those three zones.Wetlandstays a land zone onAlluvialPlain(a marsh — saturated ground, not open water). Lives inserver/src/atlas/voxel.rs(MorphologyFamily::WaterBody+generate_water_body, dispatched byzone_to_family); verified by the T-1083 believability harness (water-renders-wet flips PASS for Arbour + Edict). The §5 8-family decision-tree classifier (derive_morphology_zone) is unchanged — this is a voxel-tier generator family, selected by zone, not a new RegionProfile classifier branch. -
Amendment (T-1080, 2026-06-28): §2's moisture primitive is now a body ceiling × per-district spatial gradient, not a single body constant. The hydrosphere+atmosphere value (
derive_moisture_q) is the wettest a district on the body can be; per-district latitude (equator wet → pole dry), elevation (orographic / rain-shadow), and continentality (100 − ocean_fraction_q, coast wet → interior dry) subtract from it. Integer (D-010); coefficients inClimateConstants/climate_constants.toml[moisture_gradient](provisional, Q-123 calibrates). This mirrors D-240's latitude-graded temperature model and fixes the T-1080 believability bug (moisture_q = 80for all 2 048 districts → uniform vegetation/terrain — "nothing to fuzz"). It propagates:precipitation_class,vegetation_class, and themorphology_zoneWetland gate all readmoisture_q, so those diversify for free. Lives indistrict_profile.rs::derive_moisture_q; verified by the T-1083 believability harness (moisture-gradient flips PASS,distinct1 → ~49 on Arbour). The body-scale ceiling keeps each world's character; the gradient varies it within. -
Amendment (T-1081, 2026-06-28): the voxel tier now carries mid-scale relief (D-243 §2's invented terrain). Before, the family generators set
elevation_mfromelev_qat a compressed scale plus only ±4 m per-voxel micro-scatter — the walkable surface read flat (the D-245 "0–3 m, no hills to navigate by" bug). Avoxel_reliefpass (detail_scatter.rs, theterrain_detailfBm at a 0.13–1 km sub-district octave band — all finer than the 2 km district, so it never competes withelev_q's district-scale role — rather than the district 4–40 km band; body-globalSeedDomain::VoxelReliefseed, position-keyed; thef64perturbation truncated to integer metres before assignment — D-010) is added post-dispatch inderive_voxel_column. Two load-bearing choices: (a) the relief envelope isslope_q·3 + elev_q, not slope alone — the coarse heightmap (~40–78 km/px) yieldsslope_q ≈ 0even on high ground (observed max 13 on Arbour), so gating on slope would invent nothing; folding elevation in makes high terrain read rugged and coastal flats stay gentle. (b) Only the flat families (AlluvialPlain / LavaField / BraidedDelta / DuneStrand / MeanderReach) take it — CliffCoast / FjordWall / IncisedGorge already generate strong internal relief and a position-varying field would warp those features (e.g. drown a gorge's wall-to-floor drop); WaterBody stays at sea level. The relief span (VOXEL_RELIEF_SPAN_M = 100) and the believability voxel-relief threshold (≥ 8 m) are provisional (Q-123 calibrates, like the moisture coefficients). The span is held modest deliberately: relief sits on the compressedelev_q/Nbase (max ~50 m) andelevation_mclamps at 0 (sea level), so an oversized span clamps away on low ground (drowning the relief, biasing it positive) — a bigger span belongs with the deferred absolute-elevation model. Verified by the T-1083 harness — a newvoxel_reliefcontrast metric (mean within-district elevation range across a district-spanning transect, since a single 64 m sample chunk is narrower than the relief band) flips PASS (≈22 m on Arbour + Edict). Deferred: the absolute elevation span is still the compressedelev_q/Nbase — a per-body hypsometric relief model (so a body's true max relief sets the ceiling AND gives the relief headroom to grow without clamping) is a later refinement; the mid-scale relief gives navigable hills now without it. -
Cross-reference: D-227 (derive-don't-store voxel model), D-228 (composite tile axes / cohesion / seasonal state), D-210 (temperature proxy — formalised), D-203 (BodyWorldState cache), D-206 (background analysis pass), D-208 (drainage / D8), D-010 (determinism), D-234 (street/footprint geometry — consumes morphology), D-142 (zone types), D-217 (tile condition), Q-102 (cohesion = the warp), Q-103 (mutator schema — open), Q-105 (seasonal/clock state — temperature/ElevationDelta forward contract)
D-240: Registry orbit/star data is non-canonical — climate derives from a planet_class temperature envelope
- Decision: A body's registry orbital position and stellar assignment (
orbital_period_days,spectral_class,star_type, and any deriveddistance_au/luminosity) are non-canonical placeholder data — bodies were dropped into orbit and stars taken as they sat in the registry, with no authored physical intent. No derivation layer may consume them as physical inputs. Climate derives only from the authored, intent-bearing fields:planet_class(sets the temperature envelope),atmosphere,hydrosphere, plus the intrinsic latitude/elevation of the generated surface — with per-world variety from the deterministic body seed (D-010). Temperature is aplanet_class→ (min, max) °C envelope (source-canonical inserver/data/climate_constants.toml, tunable): latitude maps the region across the envelope (pole → cold end, equator → warm end); atmosphere / hydrosphere / elevation modulate within the bounds; the seed adds a small bounded nudge for inter-world character. A body can never derive outside its class band by construction. This supersedes the sun-driven (Stefan-Boltzmann luminosity + distance) portion of D-239 §2. - Rationale: Deriving physics from arbitrary registry values dresses placeholder data as authoritative — worse than an obvious error, because it looks real. The sun-driven path produced physically impossible results (a
temperate/liquid-water world deriving to +356 °C, because a NULLspectral_classdefaulted to a G-type Sun on an M-dwarf-dominated Gliese catalog — a 25× luminosity error). The 2026-06-08 audit found the blast radius is exactly one function (derive_temperature_c); every other reference merely plumbs the columns, so once temperature is re-based the orbit/star columns can be dropped from the derivation inputs entirely. Anchoring toplanet_class— which already encodes the authored intent (frozen,tropical,hot_arid, …) — keeps every world inside its band and makes the T-1031 believability harness's class-consistency check true by construction. - Raised by: Jeroen (2026-06-08), reviewing T-1024 climate output across 25 non-Kallast bodies.
- Cross-reference: D-239 §2 (temperature primitive — the sun-driven portion is superseded here), D-010 (determinism — seed-based variety), D-227 (derive-don't-store), D-228 (tile axes).
D-242: Corp HQ settlement model — HQs are not cities; separate from the name pool, specialization-keyed, install-baked
- Date: 2026-06-14
- Decision: A corporate headquarters is not a city and does not belong in the
atlas_city_namesflavored name pool.- Remove the corp-HQ cross-reference into the city pool. The D-207/D-223
reserved=1step (populate_atlas_city_names_corps,tooling/economy-db/economy_import/atlas.py) inserts oneatlas_city_namesrow per corp HQ whose name isn't already pooled. With noUNIQUE(body_id, name)it produces duplicate co-named "cities" — e.g. 10Groombridgerows on GJ380c, one per corp — which every downstream spatial layer (placement, roads, hinterland) then renders as separate settlements scattered across the map. This insert path is removed; the HQ relationship lives with the corp (corporations.headquarters_system, plus a resolvedheadquarters_body), not in the city pool. This supersedes D-223's "thereserved=1corp-HQ cross-reference stays" note. - HQ placement is a baked, specialization-keyed preference. Each corp's HQ is
CityTenantorStandalone, chosen by a source-canonicalspecialization → HQ-placementmap (corp_typeis degenerate — onlycorporation/combine, 158/7 — so the key iscorporations.specialization, optionallysupply_chain_role):- CityTenant (finance, media, retail, services …): the HQ is a tenant of an existing city — a corp→city link, no new settlement. Many corps may tenant one city (supplemental, non-competing HQs realistically cluster — a bank and a newspaper share a downtown).
- Standalone (extraction/mining, heavy industry, shipyards …): the HQ is its own settlement, emitted into the cascade settlement list with its own body + position, so the road graph and downstream layers treat it as a place.
- The preference depends only on corp specialization — not on seed or install — so it is authored once and baked into
systems.dbat import (a difference of corp type, not of run).
populationandsettlement_classare install-baked, not seed-derived. Body-levelpopulationis already baked (273/3 240 bodies > 0); the per-settlement spread is carried down into the city/settlement source data, so every settlement starts with a fixedpopulation+settlement_class. Only position remains seed-derived (the D-211 placement pipeline). This supersedes D-223's "population/settlement_classdeferred to placement (T-955)" and retires the never-built "assign population from the seed" step —match_citiesonly ever read those fields (all0/NULL) and nothing assigned them, so faking population in the seed layer buys nothing over baking it.- The cascade settlement input = the city name pool (cities, baked pop/class) + standalone-HQ settlements, merged. The Layer-2 road graph (D-211, T-1038) routes over that merged list unchanged; "hubs" are the significant cities, standalone HQs are typically minor nodes.
- Remove the corp-HQ cross-reference into the city pool. The D-207/D-223
- Rationale: Modelling HQs as city rows conflated two different things and made corp-dense worlds nonsensical — ten "Groombridge" dots, ten trunk roads, where there is one city hosting ten tenants. HQs are sometimes their own settlement (a mine or shipyard sits alone) and sometimes a tenant (a bank wants a downtown); that split is intrinsic to the corp's specialization and never varies by run, so it is authored, not derived. Baking
population/settlement_class(rather than deriving them from the seed) keeps generation deterministic with only position variable, matches the already-baked body population, and deletes an unbuilt seed step instead of building it. The names pool (D-223) returns to being cities only. - Raised by: Jeroen, 2026-06-14 — reviewing the T-1038 road-graph hub model; the corp-HQ-as-city rows surfaced as duplicate map settlements.
- Implementation: corp-HQ model + specialization map + standalone-HQ settlements, remove the cross-ref insert, resolve
headquarters_body(T-1074); per-settlement population/settlement_class baking (T-1075); and the gated road-graph hub refinement — scaled-cap hubs + co-location collapse + hybrid minor-settlement attach + the deferredRailHeadFacingjunction pass (which itself needs a D-213 amendment) — (T-1076, blocked by T-1074 + T-1075). All under Phase 4 (epic T-750). The T-1038 road-graph core (MST + A* + MaintenanceAuthority + waypoints + junction detection) ships independently of this record — it routes over whatever settlement list it is handed. - Implementation status (T-1074/T-1075, 2026-07-16): done. Both stories landed together, in order.
corp_typewas already known degenerate (158corporation/ 7combine) — confirmed the key iscorporations.specialization, notcorp_type. That free-textspecializationcolumn is legacy prose (only 20/165 rows ever populated, by the orphanedtooling/populate-corporations.sh— retired/deleted, T-1074 point 6) and is left alone (generate_corporations's fuzzy brand-relevance match still reads it). The HQ-placement key is a new, separate column:corporations.corp_specialization.- Vocabulary choice — reused AND extended, per this record's own instruction to prefer reuse.
corp_specializationreuses the D-237specialization_vocabularyid-space directly rather than a parallel corp-only taxonomy — every value already carriesbulk_class_projected(NonPhysicalvs everything else), which is the CityTenant/Standalone signal this record calls for. Backfilling all 165 corps (155 with a wiki page; the categorization pass read every one) found the 27 system-authored values genuinely under-cover corp-shaped identities: ~10% of corps do trade/logistics distribution, hospitality/tourism, general professional/advisory, or licensed multi-site clinical work — moving or selling what someone else made, or delivering certified services, not producing goods — which none of the 27 values name. Four values were added tospecialization_vocabulary.toml(allNonPhysical→CityTenant):trade_distribution,hospitality_hub,professional_services, andlicensed_clinical_services(the fourth via PR #177 review M1 — Somatic Futures is a 40+-clinic licensed network, one of several certified operators: neither generic consulting norlongevity_monopoly, which is Prometheus, THE sole facility). The vocabulary is now 31 values, shared by two authored axes:system_economy.economic_specialization(D-237, system-scale) andcorporations.corp_specialization(this record, corp-scale) — nothing stops a future system from adopting the 4 new values too. Per-corp authoring lives in wiki frontmatter (wiki/corporations/*.md, keycorp_specialization) — the natural per-corp source, parsed by_parse_corp_frontmatter/load_wiki_corps(extended) — not a per-corp TOML stanza file (unlike D-237's per-system TOML, 165 corps would make one file unwieldy; one wiki page per corp already exists and is the authoritative source for everything else about it). The corp pages are part of theimport_economicsmeta-stamp source set (PR #177 T1): a frontmatter edit without a regen trips the stamp check like any TOML edit. - The placement map is authored explicitly, not computed from
bulk_class_projectedat import time —wiki/economics/corp_hq_placement.toml, onehq_placementstanza per vocabulary value (all 31), defaulting to theNonPhysicalrule but reviewable/overridable per value without a code change. Standalone stanzas also carrystandalone_economic_role, mapping the 31-value specialization onto the pre-existing 10-valueeconomic_rolevocabulary (D-195/D-197) the emitted settlement row needs (match_cities/CompatibilityMatrixindex by the 10-value set, not the 31-value one). headquarters_bodyis recomputed from source on every run — never kept from prior DB state (PR #177 H1/H2). The importer-ownedcorporationscolumns (corp_specialization,hq_placement,headquarters_body) are NULL-reset and re-derived each run:corporationsis append-only, so a keep-if-set guard would freeze the first run's output forever and silently promote DB state to source (exactly the asset-pipeline golden-rule inversion — the review caught the original one-shot backfill doing precisely that). An authoredheadquarters_body:frontmatter override wins where present (hard-validated: body must exist and sit in the corp's HQ system; none authored yet); otherwisemost_populated_body_in_system(atlas.py, descended from the retired cross-reference function's heuristic) derives it with a four-stage deterministic tiebreak: population DESC → city-bearing body (hasatlas_city_namesrows) → body-type rank (planet<moon<gas_giant< belt/oort) →body_id. The city-presence tier is what the review's H2 added — the first draft ranked body-type directly and rested on a false premise ("belts can't host settlements"): belts in this world do host them (GJ845-belt: Orkney Ceramics; GJ268-belt: Jeju Lattice; GJ222A-belt: two Standalone HQs), so presence keeps settlements where they are, and the type rank only fires in fully cityless, fully unpopulated systems (GJ 702B — 12 bodies, zero population, zero city rows → planetGJ702Bbinstead of the belt the old alphabetical sort picked). The presence set is read at step 7b, before the run's pool rebuild — i.e. the previous run's settled state, a deliberate, documented hysteresis (settlement continuity across regens) whose fixpoint is stable: each run's output reproduces the presence distribution it read.- CityTenant is a new schema link, not a repurposed one.
corporations.headquarters_city_id(nullable FK →atlas_city_names.id) is the "corp→city link" this record calls for — the OLDatlas_city_names.corp_idcolumn is left in place (additive-safe, noDROP COLUMN) but is now permanentlyNULL: nothing writes it once the cross-reference insert is gone, and nothing ever read it downstream (confirmed: zero references incity_context_reader.rs/attractor_matching.rs— the Rust consumption side never depended on it). - The merge is free. Standalone-HQ settlements are inserted as ordinary
atlas_city_namesrows (populate_standalone_hq_settlements, Phase B) — same schema, no corp-linkage marker — soread_body_settlements→match_cities(the "natural seam") needed zero Rust changes; a Standalone HQ competes for attractors purely on population/settlement_class, exactly like any pooled city (verified live:cascade_for_body("GJ251c")places "The Gate Corporation" alongside "Tributarium"/"Ruhr" through the unmodified pipeline). Two new Rust unit tests lock this invariant in (city_context_reader.rs,attractor_matching.rs). - Verified against real, repeated
make regen-dbruns — each run a full recompute, not a no-op (wording corrected per PR #177 H3; the pre-review text described the one-shot backfill being stably inert): 0 duplicate(body_id, name)groups (was the whole bug); 155/155 wiki corps getcorp_specialization/hq_placement; 154/155headquarters_bodyre-derived on every run (the 1 gap is a pre-existing brokenheadquarters systemreference onsova-station-works.md, unrelated to this record), with two consecutive runs producing zero differences across all 165 corps' placement columns and tenancy targets; against the prior committed snapshot the recompute changes exactly 2 placements —prometheus-labs/kovalev-freight,GJ702B-belt→GJ702Bb(the intended H2 outcome) — and zero tenancy targets (calluna-wellness→ (GJ845-belt, Orkney Ceramics) andnamsan-collective→ (GJ268-belt, Jeju Lattice) preserved via the city-presence tier). 95Standalonecorps → 94 settlements emitted (same 1sovagap) + 60CityTenantcorps → 58 links (2 unmatched: the GJ 702B pair — now correctly on planetGJ702Bb, which still has zero authored city names, like every body in that system: a wiki content gap [T-1115], logged, not an import error). A poisoned-copy test confirmed no DB state survives the reset: hand-written garbage in all three columns is wiped and re-derived (wiki corp) or reverted to NULL (legacy corp with no authored source — the removed-key case). The 10 legacy DB-only corps seeded by the now-deletedpopulate-corporations.sh(mvg,adams-ford,dsmc, and the 7 Cygni-B "combines") have no wiki page to authorcorp_specializationfrom and are reset to NULL across the board each run — a logged, non-fatal gap (they are load-bearing for 2,468brand_productsrows viagenerate_brands, which readscorporationsdirectly and doesn't care about wiki-page existence, so their rows are kept, not deleted).
- Amendment (T-1075, 2026-07-16 — population/class derivation clarified): the phrase above, "the per-settlement spread is carried down into the city/settlement source data," is superseded by derivation-at-import, not hand-authoring:
import_economicscomputes the per-city population spread from the authoredbodies.populationon everyregen-dbrun, via a documented integer rank-size (Zipf, exponent 1) curve (atlas.py::_zipf_population_spread, constants named + commented — tuning the curve is a deliberate code change, not a data edit). There is no per-city population source file — onlybodies.population(already-baked) is authored; the per-settlement split is 100% derived. The spread runs over the corrected, merged pool (post-T-1074: Standalone-HQ settlements are ordinary rows by the time this runs, so they get a real share of their body's population like any city, not a bolted-on afterthought).settlement_classdefaults every pooled row toPopulationBudget(D-196); a small authored override TOML (wiki/economics/settlement_name_locked.toml,[[hero]]stanzas keyed on(body_id, name)— not the volatile numeric id) pins 8 hero settlements toNameLocked. EconomicTriggered and OrganicGrowth stay simulation-time (D-196) — out of bake scope; so is the D-196 generation-time consumer that reads the baked population against the >=50k-active/<5k-ghost thresholds to decide skeleton depth (confirmed live: no such consumer exists yet anywhere inserver/src/atlas— this record's bake only writes the values a future ticket will read). - Amendment (T-1076 item 0, 2026-07-16 — the Standalone/CityTenant boundary made explicit; Jeroen's ruling): the original examples ("extraction/mining, heavy industry, shipyards …" → Standalone) were operationalised at T-1074 as
bulk_class_projected ≠ NonPhysical→ Standalone — an over-generalisation that made every physical-goods specialization a standalone settlement (95/60 split) and produced a winery and a leatherworks as standalone asteroid-belt settlements. The boundary is now explicit, so it cannot be re-coarsened: Standalone applies ONLY where the HQ is itself an industrial complex / company town — (1) extraction/mining/quarrying, (2) heavy vehicle + shipyard manufacturing, (3) heavy energy (refinery/extraction complexes), (4) gate/orbital infrastructure fabrication. Everything else is CityTenant, explicitly including craft/consumer manufacturing (distilleries, breweries, wineries, ceramics, textiles, furniture), agriculture of every scale (the estate/agribusiness office sits in a market town; the fields are not the HQ), and high-tech fabs. Applied per-value incorp_hq_placement.toml(one-line justification on every flipped stanza): 9 values flipped Standalone→CityTenant (estate_farming,breadbasket,terroir_agriculture,terroir_spirits,terroir_organics,general_industrial,consumer_goods_bazaar,precision_tech,marine_farming); 11 stay Standalone (ore_extraction,company_mining,marble_monopoly,rare_mineral_extraction,lattice_material_source,fuel_production,geothermal_hub,shipbuilding,vehicle_production,gate_fabrication,military_industrial). Corp split moves 95/60 → 11 Standalone / 144 CityTenant;atlas_city_names423 → 340 rows (329 pool + 11 HQ settlements); tenant links 58 → 129 (15 unmatched: 14 on bodies with no city to tenant +sova-station-works, whose broken HQ-system ref predates this record). The vanished belt settlements (Orkney Ceramics, Jeju Lattice, Groot Karoo Cellars, Kalahari Leatherworks) took their two belt tenancies with them — expected, correct data change under the boundary; the H2 placement hysteresis converged in one transitional run (fixpoint verified: two subsequent regens byte-identical across all 165 corps, 0 duplicate(body_id, name)groups, 0 import errors). Cygni B'sNameLockedhero pin moved from the retired "Cygni Combines" HQ settlement to its pure-pool city "Metropolis". Jeroen's rendering note, recorded here so it is not re-litigated as placement: CityTenant HQs often sit on a settlement's edge or in industrial clusters — that is CityTenant rendering/district-placement color for later phases (building placement inside the settlement), not a third placement class. - Amendment (PR #178 M1 + T1, 2026-07-16 — five steel-complex corps re-tagged; hub-cap scale note): Miri's content pass verified 19/20 of the item-0 boundary judgments but found five already-authored corps that ARE the steel-complex identity the
general_industrialflip comment reserved for its own value — re-tagged in wiki frontmatter (one-line justification on each page):cygni-combines→shipbuilding(the Cygni B yards' hull-structure consortium — the D-237 hero identity, fits directly);sova-station-works,stalownia-kowalski,westphalia-heavy-works→military_industrial(habitat/orbital-module and heavy-equipment works — theheavy_equipment-anchored yard/complex value; note: it now carries civilian heavy works, not only military-administered industry — if that reads wrong later, the clean split is a newheavy_worksvocabulary value, not re-coarseninggeneral_industrial);sede-chemical-works→fuel_production(feedstock refining/chemical synthesis — the refinery-complex shape; the closest call, per Miri).shipbuildingandmilitary_industrialpreviously carried zero corps, so this also closes a content hole. Post-M1 counts: 16 Standalone / 139 CityTenant, 15 HQ settlements emitted (sova-station-worksemits nothing — its broken HQ-system ref is T-1054),atlas_city_names344 rows (329 pool + 15), tenant links 125 (14 unmatched); fixpoint immediate — two consecutive regens identical (every re-tagged corp's HQ body already carried pool cities, so the presence tier saw no transition). The Cygni Combines settlement returns on GJ820Bc; the Cygni B hero pin deliberately stays on "Metropolis" (Miri: the pool city is the canon-clean hero anchor — the combines' yard is a place beside the city, not the city itself). Scale note (T1): the road-graph hub cap (HUB_SPACING_DIAG_PX, T-1076 §1) keys off the working-grid diagonal, a fixed 512×256 for every planet today — revisit that premise when D-243's elastic planetary seam gives bodies genuinely varying grid sizes. - Cross-reference: D-207 (corp-HQ cross-ref — the insert path is removed here), D-223 (names-only pool — the corp-HQ-stays + pop/class-deferred notes are superseded here), D-211 (placement — consumes the merged list; only position is seed-derived), D-213 (
FoundingOrientation— aRailHeadFacingvariant is the gated follow-on), D-237 (corpspecializationvocabulary — the HQ-placement key, extended here from 27 to 31 values), D-195/D-197 (the 10-valueeconomic_rolevocabularystandalone_economic_rolemaps onto), D-196 (SettlementClass— the bake target), D-010 (determinism — only position is seed-derived). - Dissent: None
D-243: Spatial scale ladder — nested absolute containment with a single elastic planetary seam (resolves Q-110)
-
Date: 2026-06-14
-
Resolves: Q-110.
-
Decision: The world's spatial structure is a fully nested, absolute-metre containment ladder with exactly one elastic seam — the jump to planetary scale. Below the seam every level is a fixed integer multiple of the level below, in real metres, identical on every body; only the planetary seam floats per body. This resolves Q-110's three-way contradiction (region documented as ~1 km but implemented as 8 heightmap cells ≈ 624 km, with
chunk_contextassuming a third value) by fixing one ladder and one anchoring rule.The ladder (side length; the 2-D child count is the square of the linear ratio):
Level Side Linear ratio Role voxel 1 m — the tile (D-228/D-220) chunk 64 m ×64 voxels stream / derive unit (D-222, D-239) block 128 m ×2 chunks generator planning unit (D-222) quarter 512 m ×4 blocks settlement footprint cell (D-222) district 2,048 m ×4 quarters urban division and local-climate cell (D-222, D-239 §2) region 204.8 km (~205 km) ×100 districts top hard block — climate/weather/season lockdown + planetary grid (elastic seam) planet per body round(2πR / 204.8 km)regionsthe only floating quantity (1) The elastic seam — region ↔ planet. A body holds
round(2πR / 204.8 km)regions around the equator (and half that pole-to-pole),R=body_radius_km(D-204) — the single per-body quantity. The fixed 1024×512 heightmap (D-202) is the coarse elevation field, finer than the region grid (≈ 5 heightmap pixels per region on an Earth-sized body) and consumed per district, not per region. Everything below the region is fixed integer math;ChunkPos → … → RegionPosis body-independent.(2) Detail-scatter synthesis — the heightmap is the data ceiling. The heightmap carries continental shape only (one elevation sample per ~40–78 km). All sub-heightmap detail (region → voxel) is invented deterministically, not stored (D-227): the heightmap is interpolated and a simple, spatially-coherent detail-scatter field (a few octaves of seed+position-keyed value/fractal noise, shaped by local morphology + slope, continuous — never per-tile dice) is composed on top, alongside the D-239 §4 domain warp. It is calibrated to read as plausible terrain at a glance — visible, but never measured against a ground truth that does not exist (the player never traverses planetary scale continuously, which is why the top jump can be elastic). The containment ladder is for addressing/streaming, not for holding terrain features: large-wavelength landforms come from the low-frequency octaves of this field keyed to absolute coordinates, not from a container that size — which is why the ladder stops at the region and needs nothing coarser.
(3) Region is the climate/weather lockdown scale (refines D-239 §2; feeds Q-105). Weather, the seasonal clock, and the temperature baseline are region-scale phenomena (~hundreds of km), resolved once per region — the cheap shared-dynamism source Q-105 sought; every district and tile inside inherits it. Climate is a three-level stack: region = the climate context (latitude-driven temperature baseline, weather state, season); district (2 km) = local temperature = that baseline + elevation lapse + slope aspect (this is D-239 §2's 2 km climate district, now derived as a modulation of the region rather than from scratch); chunk/voxel = the D-239 §3 freeze/snow scatter on the local temperature. "Region temperature" is the context, not a uniform 205 km slab — intra-region latitude (~1.8°) and elevation land in the district/tile modulation. "Locked down" = defined at this scale, not frozen in time (the weather/season state still ticks; the region is the unit it ticks at).
(4) Edge fuzz — climate does not change on a line. The region (and district) is where climate is computed, not where it steps. A tile's climate value is a continuous, warp-perturbed blend of the surrounding regions' baselines (bilinear across region centres, displaced by a noise field so the blend boundary is ragged, not a straight gradient); the same applies at district edges for the local modulation. The ~205 km and 2 km grids are therefore invisible in the output — temperature, weather, and season grade smoothly and raggedly across boundaries the way a real frontal gradient does. This is the climate counterpart to D-239 §4's domain warp (same meta-rule: the addressing grid must never be visible in the result), with one distinction from D-239 §7: morphology seams stay sharp-but-organic (hard gates, warp-displaced — real geology is sharp), whereas climate is a continuous scalar field and is feathered/blended, not gated.
(5) Vocabulary — fixed, and recorded in CLAUDE.md. "Region" now means only the ~205 km top hard block. The old 1 km "RegionProfile" scale is removed — its terrain/climate carrier role moves onto the district (2 km), aligning it with D-239 §2's existing climate district. D-201's tier-4 "Region" (50–500 km watershed/political) is renamed Province (its actual content — drainage basins, D-205, territory), an overlay painted across regions, not a containment rung. Region (a fixed metric grid cell) and Province (an irregular lore-bearing boundary) sit at overlapping scales but are different kinds and must stay distinct in the docs. No new words are invented (
sectoris reserved byperception::VisibilitySector;tractand the rest were rejected). -
Rationale: The cascade glued absolute voxel/chunk scales to the body-relative heightmap with three contradictory assumptions and no record fixing metres-per-pixel, so nothing downstream could be metre-precise (Q-110). A rigid absolute ladder with a single elastic seam removes the contradiction at the source: the only body-specific function in the whole chain is region↔planet via
body_radius_km; everything below is fixed integer math, unblocking the production wiring (T-1046). Every rung earns a job — the region in particular is not mere addressing: it is the natural lockdown scale for weather/season/climate (the Q-105 dynamism source) and the sane planetary grid (~195×98 regions on an Earth-sized body, vs an unusable ~19,500 districts). Confining invented terrain detail to a deterministic scatter layer, and climate variation to an edge-fuzzed continuous blend, honours derive-don't-store (D-227) while keeping both believable — the grid never shows and the planetary seam is never walked. -
Raised by: Jeroen, 2026-06-14 — driving Q-110 during the cascade-spine sequencing (next: wire the tier into production, T-1046).
-
Implementation: unblocks T-1046 (
ChunkPos → … → RegionPosis now defined); the region↔heightmap interpolation + detail-scatter synthesis + climate edge-fuzz is the elastic-seam stage (new ticket). D-201 amended (Region→Province). The canonical ladder is recorded in CLAUDE.md. Refines D-239 §2 (district temperature now modulates a region baseline) and contributes the region-clock answer toward Q-105. -
Cross-reference: D-201 (spatial hierarchy — tier-4 Region→Province, amended here), D-202 (heightmap 1024×512), D-204 (
body_radius_km), D-220/D-222 (locked sub-settlement dims — chunk/block/quarter/district), D-205 (province boundaries — the overlay), D-225/D-227 (derive-on-demand / don't-store), D-239 §2/§3/§4/§7/§10 (climate primitive refined; warp; seams; budget reframes to per-active-derivation), Q-105 (region seasonal/clock state — answered at the region). -
Dissent: None
D-244: Asset rendering — 3D objects in-world; 2D limited to textures + flat artwork
- Date: 2026-06-16
- Decision: The in-world view renders 3D objects directly. Environment and props come from the Trellis image→3D
.glbpipeline (/glb-gen, promoted per D-241); characters come from the Quaternius 3D-mesh pipeline composited at runtime viaCharacterVisualDescriptor(D-159–D-164: 11 body types, segmented regions, separate head mesh, slot system, shape keys, region tints). The only flattened 2D content is (a) textures (PBR maps on 3D surfaces) and (b) flat 2D artwork — paintings, flags, billboards, signage, screen content — images that live as textures/decals on flat surfaces within the 3D world. There is no per-object sprite layer: game objects are not pre-rendered to 2D sprites for display. This completes the 3D pivot already ratified for the camera (D-148, supersedes D-019) and live 3D characters (D-149), extending it to all objects and the asset pipeline. - Rationale: The character system is irreducibly 3D — runtime slot compositing across body × head × hair × clothing × skin-tone × colour-overrides × shape-keys × directions × animation-frames cannot be expressed as pre-rendered sprites without combinatorial explosion. The 3D direction was already implicit in the character architecture (the 2026-03-19 character-asset doc states "no mesh work from the spikes carries over") and the Trellis environment pipeline, but was never ratified — so the retired 2D-sprite assumption drifted on in tooling and docs.
- What this retires / repurposes: the early-spike 3D→2D-sprite render pipeline (
renderer/, the/sprite-genskill, added in #541) is not the in-world format. It is repurposed as the 2D-artwork generator — producing the flat paintings/flags/billboards/signage assets in (b). The sprite-centric asset catalog (docs/assets/visual/sprites/tilesets framing,_templates/sprite.md) is re-scoped to a 3D-model + texture + artwork catalog. - Root-cause note: T-1049/T-1050 (the 2026-06-12
fable-ous.mdaudit) re-injected the dead sprite model because the retired spike scaffolding sat in the repo as live ground truth with no decision marking it dead. This record is that marker; T-961/T-1049/T-1050 are held in backlog pending re-scope. - Cross-reference: Builds on D-148 (3D camera, supersedes D-019) and D-149 (live 3D characters, not sprites) — D-244 extends both to all objects + the asset pipeline. D-159–D-164 (character 3D pipeline), D-241 (asset promotion), D-227 (derive/synthesise, don't store). Reconciles the sprite-era decisions (all amended 2026-06-17): D-043 ("not 3D"/Light2D scoped to overlay layers), D-044 (sprite footprint retired; sim occupancy + hierarchy survive), D-049 (z-stack logical model survives; sprite implementation language retired), D-066 (coordinate model + 2×2 geometry minimum survive; sprite-footprint language legacy); [D-019] is already superseded by D-148. Phase 5 (in-world rendering) builds on this.
- Dissent: None
D-245: Nature-layer believability acceptance gate
- Date: 2026-06-28
- Decision: The cascade's nature layers (terrain, hydrology, climate, vegetation, sub-biome — everything that produces the natural environment, as distinct from the built layers: settlements/roads/buildings) are "done" only when the believability litmus passes at randomly-sampled locations across all habitable bodies and world seeds. The deliverable of the nature half of the cascade is "the world reads as a living, real place anywhere," not "the layers are implemented." This is the current holy-grail objective for the generation cascade's nature half (the counterpart to "walkable exteriors" for the built half, D-166).
- "Anywhere" = held-out random sampling. The gate draws seeded-random locations across the full surface of multiple habitable bodies and multiple world seeds — never curated spots. A body passes only when (essentially) all land probes pass; water is a separate coherence check. This is what stops "looks alive in the demo, dead everywhere else."
- The litmus is multi-scale and never-repeating (the Netherlands principle): one macro identity (e.g. "delta") that is locally non-stationary — the sub-biome distribution drifts every km², and no two same-class km² are identical. Macro coherence and local non-repetition are both required.
- Automated necessary-conditions (the screen, per probe): (1) Coherence — water zones render wet; vegetation is conditioned on moisture/water-distance/slope/aspect (not random); drainage monotonic; no impossible combos. (2) Non-stationarity — same-class km² measurably differ; sub-biome mix drifts; no detectable tile-period repeat. (3) Intra-class variety — a patch shows ≥K distinct micro-features, never 100% one material. (4) Relief/landmark — relief variance above a floor where the macro terrain warrants it. (5) Climate-appropriateness — ecology matches the climate envelope; structured-sparse passes, blank fails.
- Irreducible human gate — the automated checks are necessary, not sufficient. Final sign-off is a person reading rendered layer-maps of random probes and answering the narrate-the-natural-history litmus. Locked by process (periodic sampled review + recorded sign-off), not by code.
- Strictness ramp — starts budgeted + advisory (≥X% of land probes pass + human sign-off) and ratchets to strict (every land probe; hard push-gate block) once the enforcer's first baselines calibrate the thresholds — so all of Phase 4 is not blocked on an uncalibrated metric.
- Rationale: "Implemented" is a false summit. The aliveness probe (T-1083) showed the nature layers can pass every binding law (D-239 §8) and still render a dead, uniform world (
Wetland 100%, dry ocean, no gradient). The deliverable that actually matters is believability, and the only way to make a gestalt judgment lockable is a random-sample gate where automated necessary-conditions screen a human sufficiency sign-off. Naming this the acceptance gate turns "feels alive" from an aspiration into a measurable, regressable target. - Implementation: Enforced by T-1083 (the believability sampling protocol — automated screen + per-layer map export for the human gate; snapshots baselines and runs in the push gate so nature cannot silently rot). Becomes the definition-of-done for epic T-1079 and the needle-movers under it (T-1080 meso non-stationarity, T-1081 relief, T-1082 water-bodies, T-1084 intra-class micro-mosaic). Seeded by the probe
server/src/bin/aliveness_probe.rs. Concrete thresholds are TBD — calibrated from T-1083's first baselines (a calibration Q-record may follow). - Cross-reference: D-239 (binding laws — necessary but not sufficient; this is the sufficiency gate above them), D-227 (derive-don't-store — the tier the non-stationary variation lives in), D-243 (the scale ladder the multi-scale litmus spans: macro region / meso district / micro chunk), D-210 (SubBiomeVariant — amended in spirit: sub-biome must be a spatially-varying distribution, not a single tag, T-948), D-166 (generation-before-player — this defines "done" for the nature half). Tickets: T-1079 (epic), T-1080/T-1081/T-1082/T-1084 (findings), T-1083 (enforcer).
- Dissent: None
D-246: Intra-class micro-habitat mosaic — sub-chunk palette modulation of the D-228 axes
- Date: 2026-07-02
- Decision: Within a single terrain class the sub-chunk seed paints a spatially-coherent mosaic of micro-habitats — the third believability lever (after T-1080 meso non-stationarity and T-1081 voxel relief), and the mechanism that resolves the aliveness probe's "Wetland 100%" into clearings, copses, creeks, meadows, bogs and marshes (and the equivalent for every other class). The mosaic is a derive-don't-store (D-227) modulation of the existing D-228 axes — never a new axis. Five load-bearing choices, resolved with Jeroen (2026-07-02, via the
/whats-nextrefinement pass on T-1084):- Mechanism — a new sub-chunk noise band + relief-conditioned selection. A
voxel_mosaic()octave band at ≈8–64 m wavelengths ([64, 32, 16, 8]m — finer than T-1081'svoxel_relief[1024…128]m sub-district band, so patches read distinct inside a single 64 m chunk), keyed by a new body-globalSeedDomain::VoxelMosaic = 12(append-only; the pinned-discriminant guard inseed.rsgains a line). The field value indexes the class palette's cumulative weights to pick a micro-habitat, and the pick is causally conditioned, not random — the localvoxel_reliefmicro-thalweg (lowest-relief band → the palette's wet entry: creek/bog/marsh/brook/active-channel; drier rises → meadow/glade/clearing) plusmoisture_q. Creeks are lines following the relief gradient, not blobs; blobs are smooth (low-frequency value noise), never per-tile salt-and-pepper. This replaces the currentscatter_vegetation()per-voxel seed-bit scatter (voxel.rs:1907) — which is exactly the salt-and-pepper the mosaic exists to kill. - Vocabulary front-loaded — all shipped micro-habitats mapped to D-228 axis values now. Every micro-habitat is a
(TerrainMaterial, Vegetation, Water)combination plus derived micro-relief/name. Where no honest existing value exists, a new value is added to an existing axis (never a new axis), and all of them land now rather than incrementally — because each value is a distinct selectable outcome in the weighted palette, so adding one later re-rolls the deterministic realization of every affected voxel. New values (amended into D-228):Vegetation::{Meadow, Deadfall, Lichen},TerrainMaterial::{Hardpan, Scree}. The table below is the temperate/baseline register; the biome variants (savanna gallery-copse, tundra thermokarst / lichen-heath, boreal muskeg / deadfall, …) fill the same niche spine per the Palette-key rule and are enumerated in T-1084 before first generation — so the table grows past these baseline rows, which is expected (and cheap — see Palette key). - Palette weights live in
server/data/mosaic_constants.toml, runtime-loaded likeclimate_constants.toml(the T-1080 precedent). The TOML is part of the deterministic input surface — weight edits change realizations, so they are expected only during Q-123 calibration and frozen after. - The believability metric is in scope for T-1084.
ContrastMetrics(believability.rs) gains a within-patchmicro_habitat_distinctfield (min distinct axis-triples across sampled same-class patches), andevaluate_criteriagains a provisional≥ 3criterion (K), calibrated by Q-123 item 3. Mechanic and metric land together (the T-1081 precedent — it shipped bothvoxel_reliefandcontrast.voxel_relief_m). - Family gating mirrors
voxel_relief. The flat families (AlluvialPlain, LavaField, BraidedDelta, DuneStrand, MeanderReach) take the full mosaic — vegetation/material and a micro-relief nudge (dune crest/slack, bog lows) and Shallow-water creek carving. The dramatic families (CliffCoast, FjordWall, IncisedGorge) take a non-structural pass — vegetation/material only, no elevation change (a relief nudge would warp their carved geometry) — conditioned on their own generator geometry. WaterBody takes none. The<5 ms/chunk budget (D-239 §10) is verified in the T-1081 believability/budget harness as part of implementation (voxel_mosaicadds ≈4value_noisecalls on top ofvoxel_relief's 4).
- Mechanism — a new sub-chunk noise band + relief-conditioned selection. A
- Palette key — a lookup table on
(surface-class × climate-biome × biosphere-register)(reworked 2026-07-02; the original surface-class-only key was biome-blind). The mosaic selects a weighted micro-habitat palette from three deterministic inputs, no stored tag: (1) surface-class from the family generator's base output (terrain == Wetland|Sand|Rock|Gravel|Lava→ that class;terrain == Soilsplit by dominant vegetation → Forest vs Grassland); (2) climate-biome from the district'stemperature_c/moisture_q/precipitation_class, so one Grassland surface reads savanna vs temperate meadow vs cold steppe vs tundra — D-210 sub-biome realized as a spatially-varying distribution, and the fix for the biome-blindness a surface-class-only key leaves; (3) biosphere-register from D-247 — native-mirror-wild vs introduced-Earth-managed (the D-228 wild→managed override precedence), which skins a niche (an alien mirror clade vs an Earth crop) without changing its gameplay semantics. A fixed niche spine (canopy / understory / wet-hollow / pioneer / bare) is the authoring scaffold climate + register fill differently — the vocabulary is authored per-niche, not as a free biome×class cross-product. Table size is not a performance or storage constraint (a text lookup stays flat well past ~100k entries and is negligible beside textures/models), so author generously and expressively where a biome earns distinct fills — though ~100k is headroom, not a target; the aim is coherent believability, bounded by the niche spine and taste. The only binding rule is determinism: the full vocabulary is fixed before the mosaic generates its first world (a later addition re-rolls every affected voxel), so T-1084 completes the table before generation, never after. - Vocab → D-228 axis table (the load-bearing content — items sharing a triple are separated by derived micro-relief/shape or derived name, per D-228's "names are derived, never stored"; bold = new axis value):
| Class (palette key) | Micro-habitat | TerrainMaterial | Vegetation | Water | Separated by |
|---|---|---|---|---|---|
| Wetland | clearing | Wetland | Barren | Dry | drier hummock (relief +) |
| Wetland | copse | Wetland | Forest | Dry | — |
| Wetland | creek | Wetland | Barren | Shallow | linear (micro-thalweg) |
| Wetland | meadow | Wetland | Meadow | Dry | — |
| Wetland | bog | Wetland | Scrub | Shallow | lowest relief |
| Wetland | marsh | Wetland | Grass | Shallow | reed graminoid |
| Forest | dense stand | Soil | Forest | Dry | base |
| Forest | glade | Soil | Grass | Dry | open gap |
| Forest | deadfall | Soil | Deadfall | Dry | — |
| Forest | brook | Soil | Thicket | Shallow | linear riparian |
| Forest | fern undergrowth | Soil | Thicket | Dry | renderer fern texture (cosmetic) |
| Grassland | tussock | Soil | Grass | Dry | base |
| Grassland | scrub island | Soil | Scrub | Dry | — |
| Grassland | wildflower | Soil | Meadow | Dry | shares Meadow |
| Grassland | dry wash | Gravel | Barren | Dry→Shallow | linear ephemeral channel |
| Sand | dune crest | Sand | Barren | Dry | high relief |
| Sand | slack | Sand | Grass | Dry | interdune low (relief −) |
| Sand | hardpan | Hardpan | Barren | Dry | — |
| Sand | oasis | Sand | Forest | Shallow | — |
| Sand | scrub clump | Sand | Scrub | Dry | — |
| Rock | outcrop | Rock | Barren | Dry | protruding (relief +) |
| Rock | scree | Scree | Barren | Dry | open slope |
| Rock | talus | Scree | Barren | Dry | derived name (cliff foot) |
| Rock | ledge | Rock | Barren | Dry | flat step (relief) |
| Rock | lichen | Rock | Lichen | Dry | — |
| Gravel | bar | Gravel | Barren | Dry | base |
| Gravel | active channel | Gravel | Barren | Shallow/Deep | flowing water |
| Gravel | vegetated island | Gravel | Scrub | Dry | stabilized |
| Lava | fresh sheet | Lava | Barren | Dry | base |
| Lava | weathered crust | Lava | Lichen | Dry | shares Lichen |
| Lava | tube collapse | Lava | Barren | Dry | depression (relief −); reconcile with existing tube_depression, voxel.rs:710 |
| Lava | pioneer scrub | Lava | Scrub | Dry | — |
- New D-228 axis values (append-only discriminants, D-010):
Vegetation::Meadow = 7(forb-rich flowering herbaceous — the wetland meadow and grassland wildflower),Vegetation::Deadfall = 8(dead woody debris — obstruction + fire fuel, no canopy concealment),Vegetation::Lichen = 9(crustose/pioneer biological crust — the rock lichen and lava weathered crust),TerrainMaterial::Hardpan = 6(compacted flat crust — hard fast footing, does not slump like sand),TerrainMaterial::Scree = 7(loose angular rock debris — unstable footing; scree/talus differ only by derived name).Wateris unchanged (Dry/Shallow/Deep suffice). Adding these lights up exhaustivematcharms acrossvoxel.rs,from_vegetation_class, the renderer, and any serializer — an intentional compile-time checklist. - Rationale: T-1084's finding was that a class can pass every binding law (D-239 §8) and still render as one uniform material ("Wetland 100%"); D-245 makes believability the gate, and intra-class variety is one of its five necessary conditions. Reusing the T-1081
detail_scattermachinery (same enveloped-fBm core, one more octave band, one moreSeedDomain) keeps the mechanism inside the derive-don't-store tier with no new architecture. Front-loading the whole vocabulary — rather than growing it as needs appear — is forced by determinism: the palette is a weighted selection the seed indexes, so every added value shifts the realization of already-generated worlds. Expressing micro-habitats as combinations of orthogonal axes (plus derived name/relief) rather than a flatMicroHabitatenum is the same anti-combinatorial-explosion logic as D-228 itself. - Implementation: T-1084 (story under epic T-1079). Touchpoints:
detail_scatter.rs(voxel_mosaic()+ the[64,32,16,8]m band);seed.rs(SeedDomain::VoxelMosaic = 12+ the pinned-discriminant guard);voxel.rs(new per-family-gated mosaic pass after the relief block ~:495; replacesscatter_vegetation()~:1907; new axis-valuematcharms);believability.rs(micro_habitat_distinct+ criterion); newserver/data/mosaic_constants.toml. Budget verified in the T-1081 harness. - Amendment (T-1084, 2026-07-05 — v1 implementation scope, PR #172 H2/C4): the first implementation lands a narrower pass than item 5 specifies, recorded here so the record and code agree (the D-239 T-1080/81/82 postscript pattern):
- Only the Soil-derived vegetated/wet classes take the mosaic in v1 — Wetland, Forest, Grassland (the
applygate invoxel.rs). The material-driven families (Sand, Rock, Gravel, Lava) are gated_ => false, so the material-variant rows of the table above — dune crest/hardpan, scree/talus, dry-wash Gravel, and every Lava row — are authored but not yet constructed on any reachable path, and the two newTerrainMaterial::{Hardpan, Scree}values are consequently unreachable in v1. A material change on those families must respect the family's own lithology and carved geometry (shape-aware §8 reconciliation), so they are the deferred follow-up. Item 5's promise that the flat families take the full (material + micro-relief) mosaic is deferred, not delivered at v1. - No elevation change in v1. The item-5 micro-relief nudge (dune crest/slack, bog lows) and the non-structural dramatic-family pass are both deferred — v1 modulates vegetation + water only, never
elevation_m. The D-239 §8 lithology law therefore holds trivially: material and shape are left exactly as the family generator set them. - For the three classes it does touch, the mosaic OWNS the §8 climate→vegetation law (C4), not merely stays inside it. The no-skip half holds — no palette emits
Barrenin a Forest / Wetland / Grassland zone. The climate-appropriateness half is enforced by a unified!Barrenapply-gate spanning all three classes (C1 + N1): a climatically-barren district (frozen< -50 °C= surface ice / geology per D-239 §2, or hyper-aridmoisture_q < 5— both resolved toVegetationClass::Barrenupstream byderive_vegetation) is skipped, so the mosaic never re-grants cover the climate withheld. The guard must span Wetland as well as Grassland:derive_morphology_zonehas no temperature gate, so a frozen + wet + flat district still yields Wetland material and would otherwise be painted with wetland copse/bog — the same C1 symptom via the material path (a frozen ice world must read as barren ground, not as vegetated tundra or marsh — D-245 §5). Forest isBarren-free by construction (SurfaceClass::Forestrequiresdominant_veg == Forest), so the guard is a no-op there. The trade: the family generators' guaranteed riparian Thicket band is softened to a probabilistic wet-habitat clustered nearhas_active_channelchunks (T-1040-gated) — acceptable at v1.
- Only the Soil-derived vegetated/wet classes take the mosaic in v1 — Wetland, Forest, Grassland (the
- Cross-reference: D-228 (the axes this modulates — amended 2026-07-02 to add the five values), D-227 (derive-don't-store — the tier the mosaic lives in), D-245 (believability gate — this is condition 3, intra-class variety), D-239 §8 (binding laws — the mosaic stays inside them) / §10 (sub-chunk budget), D-243 §2 (the detail-scatter tier T-1081 and this extend), D-247 (native-mirror vs introduced-Earth biosphere register — palette-key input 3; the mosaic is chirality-blind, but its register labels each niche native or introduced), D-210 / T-948 (SubBiomeVariant — now palette-key input 2: the macro biome realized as the finer intra-class distribution, not a single tag). Tickets: T-1084 (this), T-1079 (epic), T-1081 (relief — a conditioning input + the reused machinery), T-1083 (enforcer — measures the mosaic), Q-123 item 3 (calibrates K).
- Dissent: None
D-248: 3D locomotion presentation — per-leg constant-velocity interpolation keyed to the stance throttle
- Decision: The 3D client rig turns discrete server tile steps into motion with per-leg constant velocity: on each confirmed step, leg speed =
distance / stance interval, the interval read live fromInputMapper.MOVE_INTERVAL_MS(Sprint 200 / Walk 400 / Careful 600 / Crouch 800 ms — never copied). Multi-tile deltas (latest-wins snapshots drop intermediate steps) close under a 3× catch-up clamp; ≥ 2.5 m (5 subtiles) snaps all channels (position, yaw, camera, animation hard-cut). Diagonal legs run 1.41× (no √2 on the wire, D-053) — accepted as sim truth so held diagonals arrive on time. Gait clips are cadence-synced (speed_scale = clamp(leg_speed / NATIVE_MPS[clip], 0.6–1.8)— constant per leg, killing foot-slide); Careful usesWalk_Formal; idle enters after 0.18 s hysteresis (snapshot-jitter guard). Exactly one smoothing layer per channel: position constant-velocity (never eased), yaw eased under per-stance deg/s budgets, camera exponential (rate 6.0 — converges to a constant offset against constant velocity). Purchased UAL tiers (orual_extended) wire via explicit AnimationLibrary names +"lib/Clip"addressing (the imported default library name is""— a naive second copy collides). Stepped player camera yaw remains out (not canon; the T-key tilt cycle stays a dev affordance, D-148/D-158). - Rationale: resolves the D-054 ↔ D-053 cadence conflict: a fixed 100–150 ms tween dashes-then-stands at Walk/Careful/Crouch cadences, and exponential position lerp (the 2D renderer's model) produces a per-step velocity sawtooth foot cadence can never sync to. Constant-velocity legs sized to the throttle make held-key motion seamless and make cadence sync possible at all. Annotates D-054: its "client-side Tween interpolation (100–150 ms)" sentence is scoped to the 2D renderer henceforth.
- Implementation: T-1088 (
client/scripts/sandbox/locomotion_rig.gd,locomotion_anim.gd,sandbox_constants.gd— constants are initial values pending the live tuning pass). Strictly interpolate-only: a silently rejected move needs zero client handling by construction (Q-020 annotation) — facing updates, position doesn't, gait keys off render velocity ("bump-to-turn"). - Cross-reference: D-054, D-053, D-055, D-066, D-149, D-249. Tickets: T-1088.
- Dissent: None
D-249: 3D facing presentation authority — server feet, client eyes
- Decision: Which source rotates the 3D character model: moving (incl. the idle-hysteresis window) → the snapshot
player_facingoctant (the server overwrites Facing from the move delta in the same tick — it is the motion direction); idle → the client-local aim octant, i.e. the same snapped octant that rides theSetFacingwire (D-054: only octants cross the wire, so mouse-responsive idle facing is client-local by construction, and the model never shows an octant the server wasn't told). Facing freezes under input suppression (dialogue_active/free_camera_mode). The idle source is an injected provider — NPCs leave it unset and collapse to pure-wire, one code path. The verified octant→yaw table isyaw = π/2 − θ(South 0°, East +90°, West −90°, North 180°), applied on ModelRoot in WorldRoot-local space so octant→yaw composes with the D-148 45° map rotation exactly once (confirms compositor-api-spec §2). Trap: the rig must never callCharacterVisual.set_facing()— its internal table is E/W-mirrored relative to this convention (character_visual.gd:184-193). - Rationale: single-source alternatives are strictly worse — pure-snapshot idle facing is ~250–300 ms of laggy 45° pops (the 2D client already draws its indicator from client-local aim,
entity_renderer.gd:186-193); pure-client facing breaks server-authoritative display while moving. The residual artifact is bounded: idle, the model leads the server's vision cone by ≤ 1 RTT, and the disagreement is exactly the in-flight SetFacing. - Cross-reference: D-054, D-151, D-248, Q-084 (presentation half settled here; the walk-vs-aim animation split stays open — the purchased 8-direction walk/jog/crouch clip sets now make a direction-matched-clip solution tractable). Tickets: T-1088.
- Dissent: None
D-252: Facing is view-only — movement no longer writes Facing; NPC gaze is intent
- Decision: The
Facingcomponent (and the wire'splayer_facing/entity facing octants) is the view direction only — the vision-cone heading. Accepted moves no longer overwrite Facing (facing_from_deltais removed from the movement path): the player's Facing changes only via explicitSetFacing(the mouse octant, D-054's change-gated send). Movement direction is not a wire concern — the client derives body heading from position deltas (the 3D rig's per-leg velocity, D-248). NPC gaze becomes intent: the NPC path-follow system sets Facing to the step direction explicitly, relocating look-where-you-walk into AI intent — later behaviors (a patrolling guard glancing sideways, an NPC backing away while watching the player) become Facing writes by the AI, no special cases. Implementation correction (2026-07-06): the old coupling was player-only — NPCs never received Facing from movement; their cones sat at spawn direction while walking. Path-follow gaze is therefore a strict improvement, not preservation. - Motivation (play evidence, T-1088 live session 2026-07-06): during client path-follows the vision cone flapped to path-forward on every accepted step and stuck there until the mouse crossed an octant boundary — the movement overwrite fought the explicit aim, and the best client-side mitigation (post-step re-assert, ~2-tick delay) still left a ~100 ms flap per step because movement wins within a tick. Splitting the semantics is the correct fix; the walk-one-way-look-another model this enables is exactly the split [Q-084] parked.
- Consequences: "bump-to-turn" (blocked moves updated facing) is retired — correct under a mouse-view model. The client's post-step
reassert_facingmitigation is removed. D-249 is amended by this record: while moving, the body yaw source is the leg-velocity direction (wire facing while moving is now the view, which must never rotate the body); idle behavior is unchanged; the layered head/torso look-at ([T-1088] follow-facing) may run during any movement, not only path-follows. The 2D renderer's player sprite direction follows the view octant in all states (was: move direction while moving) — accepted drift on D-166-frozen code. Amends D-054 (its facing sentence: octant-only wire + explicit sends stand; the movement overwrite is struck). - Cross-reference: D-054, D-248, D-249, D-015 (vision cone), Q-084 (walk-vs-aim split — resolved by this record for semantics; camera rotation option stays parked). Tickets: T-1093 (implementation), T-1088.
- Raised by: Jeroen ("we need to split movement direction and viewing direction in the server protocol", live session 2026-07-06, after the T-1093 mitigation demonstrated the residual flap) with Claude (mitigation evidence, NPC-intent relocation, consequence audit).
- Dissent: None
D-253: Region transient state model — seasonal/tidal/weather/snow phase functions (resolves Q-105)
-
Date: 2026-07-08
-
Resolves: Q-105.
-
Decision: The region (D-243) is the single scale at which transient (clock-bound) surface state is resolved — "computed once per region per phase, inherited by every district and tile inside" (D-243 §3, the cheap-dynamism source). This record pins that model: what transient state a region carries, how its clock advances and recomputes, how tiles inherit it, and its determinism — but explicitly not the gameplay/rendering that later consumes it.
(a) The transient bundle — four phase terms + their derived surface scalars. A region carries a memoized
RegionPhase(proposed carrier name) built from four clock terms, one per natural clock rate (fastest → slowest), each a pure function of the in-game clock (D-031) and the region's static params:- Diurnal — day/night, from the D-031 day-phase clock; amplitude = the atmosphere heat-retention already defined in D-239 §2 (thick air → small swing). This term is the one D-239 §2 already owns for temperature; D-253 folds it into the same region phase model as the other three, rather than leaving it a separate mechanism.
- Tidal — from the lunar/day clock; amplitude from the body's satellite config; absent (zero) when the body has no moon. Drives the tidal component of water-height.
- Weather — a coarse, seeded, deterministic precipitation/condition term (a bounded pseudo-sequence keyed on
(region, weather-bucket)— not a simulated advecting weather system; deliberately minimal to stay "nearly free"). Drives puddles + general conditions. - Seasonal — from the year clock, phased continuously by latitude (amplitude → 0 at the equator and sign inverting across it — a continuous function of latitude, not a binary hemisphere flag, so the equatorial seam never steps). The slow temperature term + the seasonal component of water-height + the snow/crop drivers.
- From these terms the region derives its transient surface scalars (region-level, not per-tile): temperature(time) = the T-1078 / D-240 static baseline + seasonal offset + diurnal offset (this makes D-239 §2's "temperature(time)" concrete — the static baseline T-1078 ships is the mean-phase value); water-height(time) = mean water level + seasonal term + tidal term; snow/ice depth =
f(temperature(time), moisture)feeding the existing T-1030 scattered band (D-239 §3) — D-253 supplies only the depth over time, T-1030 owns where the band scatters; weather intensity/category; and the crop-cycle phase (sown → growing → ripe → harvested → fallow) =f(seasonal), the cadence clock for D-228's managed-Vegetationfarmland override (D-253 owns the cadence; the settlement/economic layer owns crop type and farmland placement).
(b) Phase-stepping — recompute on bucket rollover, never per tick, never integrated. Each term declares a clock-bucket size (diurnal ≈ day-phase; tidal ≈ day-phase; weather ≈ a multi-phase block; seasonal ≈ a season step).
RegionPhaseis memoized with the clock-buckets it was computed for; a query recomputes a term only when its bucket rolls over — the "nearly free" requirement, and D-226's dynamic-state path. Crucially, every scalar is evaluated from the absolute clock value (state = f(clock)), never integrated step-by-step (state += Δ): absolute evaluation is drift-free, reproducible, and hands D-226 its frozen-phase inspection for free (freeze the clock → the whole world's transient state is stable and re-derivable). Default bucket sizes are source-canonical + tunable (theclimate_constants.tomlprecedent), provisional pending calibration.(c) Inheritance — region computes the scalar, the tile realizes it locally, edge-fuzzed. Districts and tiles never compute their own phase — they read the region scalars, blended across region neighbours by the same D-243 §4 edge-fuzz the static climate baseline uses (warp-perturbed bilinear across region centres), so a weather front / seasonal gradient / thaw line never steps on the ~205 km grid (and the equatorial seasonal seam is continuous by construction, per (a)). The per-tile realization is a local comparison, not an inherited value: a tile is flooded iff
region-water-height(time) > tile.elevation; snowed to a depth capped by the region snow-depth, scattered by the T-1030 band and gated on its own material/moisture; puddled iff the weather term is wet and the tile sits in a micro-relief low (the T-1081voxel_reliefhollows) that diurnal evaporation has not burned off; its farmland shows the crop stage the region phase dictates. So floodplain / tidal-flat / seasonal-river emerge (D-228) — the static world stays static; only the region scalars carry the clock. This directly discharges T-1082's deferred TidalFlat wet/dry alternation (Shallow/Dry= tidal water-height vs local elevation).(d) Determinism (D-010). Every term and scalar is integer fixed-point (phase positions as basis-points of their cycle; depths/heights in integer mm/cm; temperature offsets on the existing integer scale) and a seeded pure function of
(absolute clock, region params, body seed). No per-tick float accumulation exists anywhere in the model — see (b). Weather's pseudo-sequence is a seeded hash of(region, weather-bucket), so it replays identically. Frozen clock → byte-identical state on every platform and every reload; this is what lets the transient overlay live in the D-227 derive-don't-store tier (the memo is not persisted — a save records only the clock and the state re-derives).(e) Scope boundary — this is the STATE MODEL, not its consumers. D-253 defines derivable transient state and the functions that produce it — Phase-4-appropriate, viewable as an Atlas layer like every other cascade output, and completing the forward-contracts left open by T-1030 / T-1078 / T-1082. It does NOT build: weather/flood/snow gameplay (movement, perception, passability effects — D-239 §9 names the passability hook but its wiring is later), the farming/crop simulation, snow/water rendering (Phase 5), or any verb that reads this state. Those are later phases and inherit a fixed contract here. Adding a fifth term or a consumer is a future amendment, not a reinterpretation of this record.
-
Rationale: The model is cheap because the four transient terms line up with the four natural clock rates and reuse the existing temperature primitive — temperature(time) and water-height(time) are just a static baseline plus the same seasonal/diurnal/tidal offsets, so one clock model drives temperature, flooding, tides, snow, puddles, weather, and crops with no per-system simulation. The single load-bearing choice — absolute-clock evaluation rather than step integration — satisfies three requirements at once: determinism (D-010, no drift), cheapness (recompute only on bucket rollover, and per-region not per-tile → O(regions)), and inspectability (D-226 frozen phase). Computing at the region and realizing per-tile by comparison is what keeps the dynamism O(regions) while still producing per-tile flood/snow/puddle/crop extent — the "computed once, inherited" contract D-243 §3 promised.
-
Raised by: Jeroen (Q-105 model constraints — clock + hemisphere bound, moon-gated tides, computed-once-per-region, "nearly free", 2026-05-25) + Tyre (T-1057 triage — the four-term phase-function model, the absolute-clock / memoized-by-bucket / edge-fuzzed-inheritance pins, 2026-07-08). Authored as a proposed design for PR review — the flagged judgment calls (below) are open for the lead/user to adjust.
-
Design choices flagged for review: (1) diurnal promoted to a first-class term — Q-105 enumerated three terms (seasonal/tidal/weather); D-253 adds diurnal as the fourth, since D-239 §2 already owns it for temperature and T-1030's dawn-frost already depends on it (a coherence unification, but it extends Q-105's framing). (2) weather kept deliberately minimal (a seeded coarse precipitation/condition term, no advecting fronts) — the depth is a taste call. (3) recompute-bucket granularities (diurnal/tidal ≈ day-phase, weather ≈ multi-phase, seasonal ≈ season-step) are provisional/tunable, not hard-pinned. (4) memoized-not-persisted reconciles D-228's "region property computed once per phase" with D-227 derive-don't-store (the region caches the bundle keyed by bucket; it is never durable state). (5) "phased by hemisphere" read as continuous-in-latitude (amplitude→0 at equator) rather than a north/south sign flag, so the seam edge-fuzzes cleanly.
-
Implementation: Phase 4+ (epic T-750), the forward contract from T-1030 (transient freeze/snow depth), T-1078 (region climate stack — shipped the static baseline + region-clock structure; D-253 adds the transient terms), and T-1082 (tidal-flat wet/dry). Proposed home: a
RegionPhasecarrier alongsideRegionProfileinBodyWorldState(D-203 / D-239 §10 region storage), memoized by clock-bucket; proposed derivationsderive_region_phase(clock, region)+ per-tile realization helpers (flood / snow / puddle / crop) — names non-binding, the impl ticket fixes them. Constants in a source-canonical TOML (climate_constants.tomlprecedent). A new implementation ticket lands under T-750. -
Cross-reference: D-243 (region = climate/weather/season lockdown scale; §3 computed-once-inherited; §4 edge-fuzz — the inheritance contract), D-228 (the seasonal water-height / snow-cover overlay / crop cycle this model drives — its open water-height sub-question resolved here), D-239 §2 (temperature(time) primitive — made concrete) / §3 (T-1030 scattered freeze-snow — given its clock) / §9 (seasonal passability — a later consumer), D-240 (
planet_classtemperature envelope — the baseline the seasonal/diurnal terms offset), D-226 (dynamic-state frozen-phase inspection), D-031 (game clock / day phases — the clock source), D-010 (determinism), D-203 (BodyWorldState region cache — the memo home), D-227 (derive-don't-store — the memo is not persisted). Tickets: T-1057 (this triage), T-1030 / T-1078 / T-1082 (the discharged forward-contracts), T-1081 (voxel_reliefmicro-lows — puddle/flood sites). -
Dissent: None
D-254: Standalone Atlas companion app — make atlas, dual-connection reader
-
Date: 2026-07-17
-
Decision: The implant Atlas (D-169/D-170's
implant/mapapp) ships as a second, independent Godot entry point —client/scenes/atlas_standalone.tscn, launched via a newmake atlastarget — that boots the SAME implant scene tree used in-game but skips the player entirely: no character, nomain.tscn, no gameplay HUD. It connects to the simulation server either by attaching to an already-running game (inheriting that world's state read-only) or by spawning its own server process (offering seed selection now; save selection is a recorded, unbuilt hook — saves are Phase 5+). The server enforces read-only server-side via a distinctConnectionRoleon the handshake (Player | Reader, Reader spawns no character and receives noObserverSnapshotat all) — the Atlas app itself gains no new client capability, it is the existing Atlas UI pointed at a bridge connection the server structurally refuses inputs from. A future market-trading widening (§6) adds aTradingReaderrole as a strict superset ofReader(never a replacement) rather than inventing a second connection type.(1) CONNECTION MODEL — attach vs. spawn, discovery.
Today's reality, confirmed in code:
main.rsbinds aTcpListener, printsLISTENING:{port}, then callslistener.accept()exactly once — blocking, no loop. A second TCP client completes its TCP-level handshake (kernel backlog accepts it) but never gets an application-level accept — it hangs forever waiting forHandshakeMessage. Not refused, not replaced — silently starved. This is the actual failure mode the reader connection must design against; zero multi-connection plumbing exists anywhere inserver/src/bridge/today, confirming the ticket's own framing ("almost certainly single-connection").Default port
9876(sim_bridge.gd:26, matchesmain.rsfallback), overridable via positional addr /--port/ (client-side)SR_PORT.SR_PORTis already the env var two Godot scripts read today for "which port do I dial" (visual_capture.gd:99,locomotion_sandbox.gd:67) — the discovery mechanism reuses that existing convention rather than inventing a third (SR_ADDRis a server-side bind override and is not load-bearing for either connection mode below).- Attach-mode discovery: fixed default port 9876 +
SR_PORToverride — the same two-tier scheme the game client already uses to find its own server. Raw TCP connect with a ~500ms timeout (localhost, not WAN — no reason to wait longer).ECONNREFUSEDis a real, unambiguous signal ("no server listening") and falls through to spawn-mode. A connection succeeding does not yet mean attach is safe — that gate is the Reader-role handshake in §2, not the TCP connect itself. - Spawn-mode lifecycle: reuse the
tests/run-visualprecedent exactly —--port 0(OS-assigned), parseLISTENING:{port}from stdout — but without--test-mode: the companion needs the realsystems.dbworld, not Gauntlet test fixtures. Ownership: the companion app owns the child process it spawns, the same patternserver_process.gdalready implements (OS.create_process/OS.kill/NOTIFICATION_PREDELETEsafety net) — reused directly, not reimplemented. World seed is passed via the companion's ownStartupMessage.world_seedpost-handshake (not a--seedCLI flag) — this keeps the save/load seam (§5) as the single source of truth for how a spawned world gets populated, rather than splitting seed-selection across a CLI flag and a wire message. - Mode selection UX: auto-attach-else-spawn — try attach for ~500ms, fall through silently to spawn on refusal. Zero friction for the common case ("inspect the world I already have running"), and the fallback is never wrong (spawn always works). An explicit Attach/Spawn chooser is deferred — only justified if reader-mode failures turn out confusing enough in practice that users need visibility into why attach didn't happen; not assumed necessary at design time.
(2) READER CONNECTION CLASS — handshake variant, server-side enforcement.
Enforcement is server-side at the protocol layer, never client politeness — a hostile or buggy companion client is exactly D-010's adversarial case, and the read-only guarantee has to hold against that, not just against a well-behaved reference client. The one seam that matters: before the server unconditionally spawns a
PlayerCharacter(main.rs, today unconditional on every accepted connection).Handshake extension: add
role: ConnectionRoletoStartupMessage— enumPlayer | Reader(widened by §6 toPlayer | Reader | TradingReader) — with#[serde(default = "ConnectionRole::player")]for back-compat, rather than a separate pre-startup negotiation message. This follows D-192's existing "no lockstep negotiation" precedent (protocol_version field dropped for the same reason): role is data on the existing message, not a new protocol gate. Critical determinism guard: a Reader'sworld_seedfield is ignored server-side and never re-seedsSimRng— a second StartupMessage touchingSimRngafter tick 0 would break determinism for whatever Player is already in session (spawn-mode readers get their seed from the world THEY spawned, at genuine tick 0; attach-mode readers must never be able to perturb an already-running world's RNG state via their own handshake).Server enforcement — structural, not filtered: Reader role skips the character-spawn path entirely and receives no
ObserverSnapshotat all — not a stripped/redacted one, none. This is the load-bearing point:ObserverSnapshotis a per-character observation record (facing, inventory, visible_tiles are all meaningless without a character), so forwarding the Player's own snapshot to a Reader — even filtered — would be a direct D-010 boundary violation (a second observer silently granted the first observer's fog-cleared view). What a Reader can legitimately receive is proven by the existing handler signatures:handle_star_map_request,handle_city_names_request,handle_atlas_request(and this record's new browse-request handlers, §4) all take no observer/character/query parameter whatsoever — justbody_id/world_seed/path— which is the independent proof that this data was already install-static/world-public before D-254, not a new carve-out invented for readers.Message Player Reader Vec<PlayerInput>(inputs)yes no ObserverSnapshot(outbound)yes no — not even filtered Atlas/StarMap/CityNames/Browse request+response yes yes HandshakeMessageyes yes Violation handling: a Reader sending
Vec<PlayerInput>is syntactically valid (the existingdecode_inbounddemux parses it fine) but role-disallowed. Log + drop on first offense, mirroring the existing recoverableDeserializationWithDumppattern; escalate to disconnect only on repeated violations — a natural fit for the already-flagged N-consecutive-errors handling in the bridge module, made per-connection once multiple connections exist.Multi-connection architecture — scoped honestly as 0-1 Player + 0-N Readers, explicitly NOT general N-player (that is D-009's separate, larger, and currently out-of-scope ambition — this record does not reopen it).
BridgeResource(today a singleBox<dyn SimBridge>) becomes a collection; the single blockingaccept()becomes a non-blocking accept-loop polled per-tick, so a Reader connecting mid-session never stalls the Player. The inbound drain loop routesVec<PlayerInput>only from the Player-role connection; atlas/starmap/citynames/browse requests are accepted from any connection, but responses need a connection-id tag (today's response buffers have no "whose request was this" notion, because there has only ever been one connection). OutboundObserverSnapshotsends target the Player connection only — this is a structural enforcement of the boundary above, not merely a convention that could be gotten wrong by a future edit.Back-pressure/lifecycle — the sharp existing edge: today
BridgeError::DisconnectedsetsServerRunning = falseand kills the whole server process, because currently one connection's disconnect is the session ending. That behavior must NOT fire on a Reader's disconnect once roles exist — only a Player disconnect should flipServerRunning; a companion app closing its window must never kill the game it's attached to. Determinism holds by construction as long as reader frames never reach the InputQueue/SimRng path (guaranteed by the enforcement above, not by a separate check). Recommend a lower per-reader inbound frame cap (e.g. 8/tick vs. the existing Player cap of 64/tick) — a reader has no legitimate reason to send that volume of requests per tick, and the cap is cheap insurance against a runaway/misbehaving companion client.(3) APP SHELL — how
make atlaslaunches the Atlas standalone.Decision: a dedicated entry scene, not a feature flag on
main.tscn.client/scenes/atlas_standalone.tscnis a bare root (Node2DorControl) with a script (atlas_standalone.gd) following the exact boot shapeclient/tests/visual_capture.gdalready establishes for minimal Godot entry points (_init() -> _run.call_deferred(), connect, wait for handshake, open UI) — exceptatlas_standalone.gdis a real scene script (extends Node2D, normal_ready()), not aSceneTree-extending test harness; theSceneTreepattern is for offscreen capture tooling, the standalone app needs a visible window.Why not a flag on
main.tscn/main.gd:main.gdis saturated with player-only wiring that a "headless" branch would have to route around at every touch point, not bypass cleanly — 18@onreadygameplay HUD nodes (minimap, stance indicator, inventory grid, dialogue box, interaction list, gauntlet HUD…), aSnapshotEventRouterwith a dozen player-centricregister_always/registerhandlers (update_zone,play_recognition_chimes,consume_dialogue…), free-camera WASD panning tied toGameState.free_camera_mode, and a_process()loop whose entire second half is input-queue flushing (InputMapper.flush_queue()→SimBridge.send_input()). None of that exists to serve the Atlas — it exists to serve a playing character, which a reader connection never has (and, per §2, structurally cannot send inputs for even if it tried). A flag would mean auditing and branching every one of those systems to no-op correctly; a dedicated scene means writing on the order of 100 lines that do only what the Atlas needs, with zero risk of a reader session accidentally exercising player-only code paths (interaction prompts, dialogue, bug report capture) that assume a character exists.Boot sequence (
atlas_standalone.gd, modeled directly onvisual_capture.gd's live-mode wait blocks):_ready(): run §1's auto-attach-else-spawn discovery (trySR_PORT-or-default-9876 connect, ~500ms timeout; on refusal, spawn a server child via theserver_process.gdpattern with--port 0and parseLISTENING:{port}). ConfigureSimBridgeaccordingly (server_pathset for spawn, unset + resolved attach port for attach).- Call
SimBridge.connect_to_sim()using the Reader handshake variant (§2'srole: ConnectionRole = ReaderonStartupMessage), not the character-startup pathmain.gduses. This is the one placeatlas_standalone.gd's connect call diverges frommain.gd's. - Poll
SimBridge.stateuntilCONNECTED— sameConnectionStateenum, same polling shape asvisual_capture.gd's live-mode wait, minus the fixed-frame-count settle (a real window can justawaitthe signal instead of budgeting frames for a screenshot). - On connect:
HudGroups.open_app("implant/map"). There is no gameplay group ever registered in this scene, so D-170's gameplay/implant mutual-exclusivity degenerates harmlessly to "implant is always the sole active exclusive group" — noHudGroupscode changes needed; the invariant it enforces (only one of gameplay/implant visible) is trivially satisfied when gameplay never registers anything. ImplantRegistry.instantiate_all(self)— the same callhud.gd._ready()makes in the normal game — populates every installed implant app (Atlas + Economics both come along for free; Economics degrades gracefully since it's reachable but not the entry point, and read-only holds for it too automatically, since it rides the same Reader connection).- The Atlas's own
KEY_M/KEY_ESCAPEhandling (atlas_app.gd) currently callsHudGroups.close_app()on M/Escape from the top-level "reach" screen, which would leave the standalone window showing a blank Control with nothing to fall back to (there is no gameplay layer). Two options, left for the implementation ticket to pick: (a)atlas_standalone.gdintercepts the close and either quits the app or re-opensimplant/mapinstead of demoting to a nonexistent gameplay layer, or (b)atlas_app.gdgains astandalone_modeflag that no-ops the close-to-gameplay branch. (a) is recommended — it does not touchatlas_app.gdat all, keeping the in-game and standalone Atlas byte-identical.
Window title/branding:
atlas_standalone.tscnsets its own window title viaDisplayServer.window_set_title()in_ready()(e.g. "The Settled Reach — Atlas"), sinceproject.godot's sharedconfig/namewould otherwise make the standalone window read identically to the main game window in the taskbar/alt-tab — a second-monitor companion needs to be visually distinguishable at a glance. This is the only project-level Godot config touched; norun/main_sceneoverride, no export preset changes in this ticket.Dev launch (un-exported):
make atlasruns$(GODOT) --path client client/scenes/atlas_standalone.tscn— Godot accepts an explicit scene path as a positional argument, overridingrun/main_scenefor that invocation only (the same mechanismgodot --path client -s res://tests/visual_capture.gdalready uses to run a non-default entry script). Noproject.godotedit needed;run/main_scenestaysmain_menu.tscnfor the normal game. Since discovery (§1) is auto-attach-else-spawn at runtime,make atlasitself stays a single simple target — it does not needmake game's explicit background-cargo run+sleep+ launch +make stopchoreography, becauseatlas_standalone.gdowns its own spawn decision and child-process lifecycle internally (§1/§2).make atlasis just: build client, launch it.Exportable later: because this is a genuine second scene (not a runtime-detected mode), it is also a legitimate Godot export preset target down the line —
godot --export-release "Atlas" build/atlas/...withatlas_standalone.tscnas that preset's main scene. Nothing in this design blocks that; it is out of scope for this ticket (no export preset is added now) but the architecture does not need to change to support it later. This directly serves purpose (2) in the epic: "remains available as a LEGITIMATE player-facing pattern post-release." One caveat inherited from §2/§6, flagged here because it bears on export/distribution specifically: the default bind (127.0.0.1:9876) is loopback-only, and loopback is the entire security boundary the read-only guarantee currently leans on. A same-machine export is safe as designed. A LAN companion (a genuinely different second monitor — a different physical machine on the same network) is a different, larger feature: it requires the non-default-bind + real-auth work §6 already flags as a prerequisite forTradingReader, and arguably forReadertoo once "same machine" stops holding. Not built now; recorded so nobody exports this to a non-loopback bind by default.(4) DATA BROWSER — "scan ALL database data."
Browse surface. A new implant app,
implant/browser(or folded into the Atlas as a new top-level screen reachable from "reach" — the implementation ticket picks the exact navigation entry point; recorded here as its own app since the entity set is broader than geography and doesn't naturally nest under the Atlas's reach→system→planet→regional drill-down), composed entirely from the existing D-169 component library (ImplantPanel/ImplantHeader/ImplantDataRow/ImplantTextBlock/ImplantSeparator) — no new UI primitives needed, this is exactly the list+detail pattern the library was built for. Two screen shapes, reused per entity kind:- Index screen — a scrollable
ImplantDataRowlist (name + one or two summary columns), filterable/searchable by name, one per entity kind. - Detail screen — an
ImplantPanelofImplantDataRows (andImplantTextBlockfor free text / descriptions) showing every column the wire response carries for that one entity,nav.push()-reachable from the index row.
v1 entity scope (deliberately narrow, honest about phase).
systems-schema.sql's table set spans registry data (systems, bodies, stations, corporations, commodities, trait templates) and cascade-derived atlas geometry (atlas_cities,atlas_roads,atlas_rivers,atlas_province_boundaries…) that is Phase-4-in-progress and per-body-optional (populated only once a body's generation cascade has run — the sameAtlasLayerStatus::Ready-vs-Pendinggating the Atlas's regional screen already handles). v1 ships registry-tier screens only — tables that exist, are fully populated, and are stable regardless of cascade progress:- Star systems (
star_systems+system_economy/system_factions/system_culturefolded into one detail screen — small tables, natural 1:1 join) - Bodies (
bodies, filterable by system — the existingSystemScreen's body list is the UI precedent) - Stations (
stations) - Corporations (
corporations+corp_presence/corp_financial_statefolded in) - Commodities (
commodities+production_chains/chain_inputs) - Trait catalog (
trait_templates) — Jeroen's brief names this explicitly
Deliberately excluded from v1, left for a follow-up ticket once Phase 4 cascade tables stabilize:
atlas_cities/atlas_roads/atlas_railroads/atlas_pois/atlas_rivers/atlas_oceans/atlas_mountain_ranges/atlas_province_boundaries(cascade-derived, per-body, partially populated mid-Phase-4 — a browser screen over a table that's empty for most bodies today is not a useful v1 screen) andcorp_lifecycle_events/system_history/historical_events(event-log tables, better served by a future timeline/log UI shape than list+detail). The six-entity v1 list above is the full set of "always fully populated, one row = one interesting thing" registry tables; everything else waits.Data path — wire-only, extending the existing proxy pattern (no local SQLite read). Two options exist in principle: (a) the client opens
server/data/systems.dbdirectly (it already ships in the client build — instant, complete, works even with no server running), or (b) every browser screen is a wire request/response pair through the bridge, exactly likeStarMapRequest/CityNamesRequesttoday. This record picks (b), unambiguously, for two independent reasons:- Pragmatic: Godot has no built-in SQLite.
client/addons/holds exactly two addons today (gdUnit4,messagepack) — no SQLite driver exists anywhere in the client. Readingsystems.dblocally would mean adding a third-party GDExtension (e.g.godot-sqlite) as a new dependency. D-020 explicitly rejected GDExtension for the core client-server bridge specifically to avoid "gdext pre-1.0 API churn, Godot version ABI breakage, FFI thread safety" — introducing a GDExtension now, for a companion-app convenience, reopens exactly the risk category D-020 spent effort closing. This is not a hard architectural violation (D-020 scoped its GDExtension rejection to the simulation bridge, not "any GDExtension ever") but it is the wrong trade for a feature whose entire value proposition is "lightweight." - Architectural: the codebase already made this call, recently, on purpose. T-949 migrated the star map — 100% static, authored, non-per-body data — off a direct client-side
FileAccessread ofstar_map_data.jsonand onto a wire request, specifically because "the client never reads game data files directly" (D-010 boundary framing). That decision already resolved the "but this data is static, why not read it locally" question a companion-app data browser would otherwise re-litigate — T-949 answered it for star-map data, and there is no principled reasonstar_systems/bodies/corporationsare different in kind. One data-access rule for the whole client (server-authoritative reads, always through the bridge) is simpler to reason about and extend than "static tables read locally, dynamic tables read over the wire, judgment call per table" — especially since today's "fully static" table can grow a cascade-dynamic column later (corp_financial_statealready looks time-varying).
So: static registry data is NOT read locally — it goes through the SAME wire path as everything else, because the server is already the sole owner of
systems.dbaccess and that ownership is a feature (single source of truth, single enforcement point for D-010 boundaries), not a latency cost worth working around. The "instant, complete, offline-capable" properties Jeroen's brief names as motivations are achieved a different way: attach-mode's "instant" comes from a fast local TCP round-trip (sub-millisecond on loopback — the ~1-5ms serialization cost D-020 already accepted is not the bottleneck for a data browser that isn't rendering 60fps), and "complete" comes from reading the same open handle the running server already has, with no second file-format copy to keep in sync.Server-side extension (the actual new work). One new proxy, following
atlas_data_proxy.rs's established shape: per-entity-kind request types (mirroringStarMapRequest's "thin, one dataset" shape), not a generic SQL-ish query surface — a generic query API is a much bigger security/complexity surface for a v1 feature that only needs six fixed table shapes, and is explicitly rejected for that reason. Each handler is arusqliteread againstsystems.dbusing the exactCityContextReader::open()-style pattern already proven server-side — the server already has this dependency and this pattern; this ticket is "write five more read functions," not "introduce a new capability."D-010 boundary note — the "no character" framing, reinforced by §2. A reader connection has no character (§2: it receives no
ObserverSnapshotat all), so there is no per-character knowledge/fog to bound against — this is a SIMPLER boundary case than the normal player observation, not a harder one. What the Reader class is allowed to see is bounded by connection class, not character knowledge state, and §2 already proved the six v1 entities pass that bar independently (their handlers take no observer/character parameter — they were install-static/world-public before this record, not a carve-out invented for readers). The one thing explicitly ruled OUT of v1 scope: browsing a specific save's diverged dynamic state (an economy snapshot that has drifted from the shared baseline via play, one corp's post-game-start financial trajectory) is information a Reader attached to someone else's playthrough should not casually have. v1's six entities are registry-tier (identical across all saves, cascade-independent), so this doesn't bite yet — it becomes live the moment a market-state screen is added (§6) or a cascade-tier table (the excluded list above) is browsed against an attach-mode connection to someone else's running game. Flagged here so whichever follow-up ticket adds those screens re-reads this paragraph first.(5) SAVE/LOAD SEAM — recorded hook, not built.
Saves are Phase 5+ (per the cascade);
meta.schema_version(T-888) already carries the lineage-migration seam on the DB side, but no save file format or save/load UI exists yet anywhere in the client. This record fixes WHERE the Atlas's save/load interaction slots in, once it exists, without building any of it:- Attach-mode has no save/load UI at all — it inherits whatever world the attached game session is running, save/load included; the Atlas is a read-only window onto a live session, and "loading a different save" from inside an attached reader is a contradiction (that's just attaching elsewhere, not loading). No hook needed here.
- Spawn-mode v1 (this ticket's actual scope) offers seed selection only at launch — the standalone app's own minimal startup screen (part of
atlas_standalone.tscn, shown before theHudGroups.open_app("implant/map")call in the boot sequence above) asks for a world seed the same waycharacter_creation.tscn/GameState.world_seeddoes today for a normal new game, then spawns a Reader-role server against that seed via §1'sStartupMessage.world_seed(not a CLI flag — §1 already fixed this as the single source of truth for spawn-mode seeding). - Spawn-mode's future save picker slots into that SAME pre-Atlas startup screen, as a second choice alongside "new seed": once a save file format exists, the startup screen gains a "load existing save" option that spawns the server and immediately issues whatever the (then-existing)
LoadGameflow is — the exact wire actionmain.gd's_dispatch_pending_load()already sends today (InputMapper.Action.LOAD_GAME→SimBridge.send_input()), reused verbatim. The Reader-role server applies the load exactly as a normal server does, then simply never accepts player inputs afterward (§2's enforcement doesn't care how the world was populated — it gates on connection role, not on world provenance). No new save/load mechanism is invented for the Atlas — it is a consumer of whatever Phase 5+ builds, hooked in at exactly one point (the pre-launch startup screen), recorded now so future work knows the seam exists and where.
(6) FUTURE TRADING — what changes when the app gains write verbs.
Designing the seam now, not implementing it.
Per-verb allowlist via a widened role, not a new connection type. The
ConnectionRoleenum from §2 extends toPlayer | Reader | TradingReader.TradingReaderis strictly additive toReader— everything aReadergets, plus a narrow, explicitly-enumeratedPlayerActionallowlist for trade verbs — never a replacement. This keepsPlayer ⊇ TradingReader ⊇ Readera strict superset relationship, so widening later only adds match arms at the same enforcement point (§2's role-gated input handling) and never touches theReaderpath at all — the base read-only guarantee this whole record establishes is structurally unaffected by trading being added later.Idempotency/ordering. Trade commands travel through the existing
tick-stampedPlayerInput{tick, action}envelope (not a bespoke unstamped request), so ordering against the Player's own concurrent actions falls out of the existingInputQueueordering for free — no new sequencing mechanism needed. Unlike movement (visibly-wrong-but-harmless if accidentally duplicated), a duplicated trade command is a real bug class (a double-sell). Recommend a client-generated idempotency token + a short server-side dedup window — cheap and bounded for a localhost, single-user, low-frequency command class. Rejected alternative: relying on TCP's delivery guarantee alone — that only catches transport-level duplication, not the actual threat (a user double-clicking through a UI hiccup and generating two distinct, both-valid application-level messages).Identity/auth — the assumption that must stay visible. Same machine, same user, no auth — loopback-only IS the security boundary (the server already effectively enforces this via the
127.0.0.1:9876default bind). This reasoning breaks the instantSR_ADDRor any non-default bind lets aTradingReaderconnect from a different machine — which is exactly D-009's actual multiplayer future, or even this record's own §3 export-later note about a genuinely-remote second-monitor companion. The moment loopback-only stops holding, real auth (at minimum a session-minted token) is required beforeTradingReaderwidens beyond it. This assumption is recorded here explicitly so it is visible to whoever eventually picks up a LAN-companion or remote-trading idea, rather than being silently inherited as "it already works, why would auth be needed." - Attach-mode discovery: fixed default port 9876 +
-
Rationale: Three independent product goals (Jeroen's brief) converge on one architecture cleanly: a dev data-inspection surface (purpose 1), a legitimate post-release second-monitor pattern (purpose 2), and an attach-or-spawn reader with a save seam (purpose 3) all want the SAME thing underneath — an implant UI that can run without a player. Building that once (dedicated entry scene +
ConnectionRole-gated reader connection + wire-only data access) serves all three simultaneously; there is no version of this where the dev tool and the shipped companion app are different pieces of software. The wire-only data path is the one design choice that could have gone either way and didn't — it is deliberately consistent with T-949's precedent rather than reopening it, and it avoids a new GDExtension dependency for a "lightweight" feature. The app-shell choice (dedicated scene over amain.tscnflag) keeps blast radius smallest: the standalone Atlas cannot regress player-only code paths because it never touches them. TheReader/TradingReadersuperset relationship (§2/§6) means the read-only guarantee this record exists to make is never at risk from the later trading feature — it can only be extended, never weakened, by construction. -
Implementation: New ticket tree under T-1128 (epic) — proposed tree delivered in the T-1129 design-pass report, not filed here (tree ownership: team lead). Client:
client/scenes/atlas_standalone.tscn+atlas_standalone.gd, a newimplant/browserapp (or Atlas-nested screen) underclient/ui/implant/apps/,Makefileatlastarget. Server:ConnectionRoleonStartupMessage, the accept-loop +BridgeResourcemulti-connection change, per-connection response tagging, and the Player-onlyServerRunning/snapshot-targeting fixes (§2) inserver/src/bridge/; a newBrowseRequest/BrowseResponseproxy inserver/src/atlas/(sibling toatlas_data_proxy.rs, §4). Nosystems-schema.sqlchanges required — v1's six entity screens read existing tables as-is. -
Cross-reference: D-010 (client-server boundary — the wire-only data-path rationale; "no character" reader framing; the adversarial-client enforcement stance), D-009 (multiplayer design-for-it baseline — this record's 0-1 Player + 0-N Reader model is explicitly NOT that larger ambition), D-020 (subprocess/IPC over GDExtension — why local SQLite is rejected;
SimBridge/bridge trait extension point), D-169 (implant component library — the data browser is composed entirely from existing components), D-170 (HudGroups — the standalone scene's degenerate single-group case), D-192 (no lockstep negotiation precedent — whyConnectionRoleis aStartupMessagefield, not a new pre-handshake message). T-949 (star map wire-migration precedent this record extends rather than re-litigates), T-888 (schema_version save lineage — the seam §5 hooks into once it exists). Tickets: T-1128 (epic), T-1129 (this design pass). -
Raised by: Jeroen (2026-07-17, brief: standalone Atlas via
make atlas, read-only reader against the running game or its own spawned server, save/load interaction seam, future trading). Designed by Tyre (architecture lead, §3–§5, integration, record author) + Oscar (§1, §2, §6 — connection model, reader protocol, trading seam). -
Dissent: None recorded at design time. Two judgment calls flagged for confirmation rather than dissent, both revisable by the implementation ticket without touching the rest of this record: (a) §4's choice to make the data browser a separate
implant/browserapp rather than a new top-level screen nested inside the existing Atlas — the six v1 entity kinds don't share the Atlas's geographic drill-down shape, so a separate app was chosen for navigational clarity (the Atlas stays "the map," the browser is "the database") but this is a naming/IA call, not architecture; (b) §3'satlas_app.gd-unmodified close-handling option (a) vs. astandalone_modeflag option (b) — recommended but not forced.
108 decisions (D-001 through D-254, excluding gaps). Last updated: 2026-07-17 (D-254 — standalone Atlas companion app: dedicated entry scene, ConnectionRole-gated reader connection (0-1 Player + 0-N Reader), wire-only data browser extending the T-949 proxy pattern, save/load and trading seams recorded not built).