Four-round workshop (Gestalt, Tyre, Paula, Burnelli-Sheldon, Miri) mapping the full generation pipeline from planetary heightmap to walkable tile. 25 D-records produced. Ticket dependency chain for Tier 0-4 implementation identified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
489 lines
22 KiB
Markdown
489 lines
22 KiB
Markdown
---
|
||
title: "Tyre — Round 2: Spatial Generation Architecture Proposal"
|
||
description: "Technical architecture for the spatial pipeline: coordinate translation, city-to-district decomposition, Phase 1/2 generation, chunk wiring, seed boundary, testing, and effort"
|
||
type: workshop
|
||
status: active
|
||
workshop: generation-cascade
|
||
agent: tyre
|
||
round: 2
|
||
created: 2026-04-30
|
||
---
|
||
|
||
# Generation Cascade — Round 2: Spatial Generation Architecture Proposal
|
||
|
||
**Tyre — Technical Architect**
|
||
|
||
---
|
||
|
||
## Scope Declaration
|
||
|
||
Per lead directive: this proposal covers the **spatial pipeline only** — from atlas city markers to walkable rendered tiles. Heritage grammar, NPC manifests, social sites, triangle wiring, culture overlays — all deferred. The remaining questions from Round 1 that fall outside spatial scope (OQ-R1-A heritage root count, OQ-R1-B SocietyProfileRef type) are not addressed here.
|
||
|
||
Spatial scope items I am addressing:
|
||
- OQ-R1-C: City marker schema approach
|
||
- OQ-R1-D: Atlas coordinate translation formula (resolved below)
|
||
- OQ-R1-E: Vertical slice effort estimate (revised)
|
||
- Gap B: Coordinate system design
|
||
- Gap C: Missing DistrictSkeleton fields
|
||
|
||
---
|
||
|
||
## The Core Architecture Insight: Two Separate Coordinate Systems
|
||
|
||
This resolves OQ-R1-D and underpins everything below.
|
||
|
||
The atlas grid (512×256 pixels) represents the **planetary surface** as a display coordinate system for the atlas UI. It shows continents, coastlines, city dots. It is not a world-space coordinate for the game simulation.
|
||
|
||
The walkable world uses a **city-local coordinate system**, measured in sim tiles (0.5m each, per D-066). A city does not "exist" at pixel (row, col) in the sim world — it exists in its own local space starting at (0, 0). The atlas pixel position is only used for:
|
||
1. Rendering the city dot on the planetary map UI
|
||
2. Determining relative positions *between* cities on the same body (future: overland travel)
|
||
|
||
**There is no pixel-to-sim-tile conversion formula** and none is needed. The translation is a lookup, not arithmetic:
|
||
|
||
```
|
||
atlas_city.center = [row, col] → planetary UI display position only
|
||
walkable world origin → always (0, 0) in city-local sim tile space
|
||
```
|
||
|
||
This is the correct architectural answer to OQ-R1-D. Attempting to produce a linear formula from 512×256 pixels to a planetary sim-tile grid would require deciding how large the planet is in sim tiles — a meaningless number for gameplay. Players don't walk between cities.
|
||
|
||
**What this means for district positioning:** Districts in a city are laid out in city-local sim tile space. District 0 is at world position (0, 0). District 1 is at (512, 0) (one district-width east). And so on. The atlas pixel coord stays in the atlas layer; the district layout algorithm works purely in sim tile space.
|
||
|
||
---
|
||
|
||
## Seed vs. Stored-Data Boundary
|
||
|
||
This is the most important architectural decision in the spatial pipeline. I'll be direct about where the line is.
|
||
|
||
### What is stored (authored or generated-then-committed)
|
||
|
||
| Data | Location | Why stored |
|
||
|------|----------|-----------|
|
||
| Star system and body definitions | `wiki/`, `systems.db` bodies table | Authored world content |
|
||
| Economic data, corporations | `systems.db` economics tables | Generated-then-committed via pipeline |
|
||
| City positions, populations, infrastructure | `markers.json`, `systems.db` atlas tables | Generated-then-committed; hand-edit possible |
|
||
| City names | `markers.json` (Gemma-filled) | Curated; not reproducible on demand |
|
||
| Chunk mutations (DamageOverlay, player changes) | Save file (future) | Playthrough-specific |
|
||
|
||
### What is NOT stored — always re-derived from seed
|
||
|
||
| Data | Why seed-derived |
|
||
|------|-----------------|
|
||
| District count and positions per city | Function of (city_data, world_seed) |
|
||
| DistrictSkeleton (all fields) | Function of (DistrictId, body_context, world_seed) |
|
||
| BlockSkeleton per district | Function of (district_seed) |
|
||
| ChunkData tile grid | Function of (block_skeleton, chunk_coord, district_seed) |
|
||
| Building footprints within chunks | Function of (zoning, density, chunk_seed) |
|
||
|
||
**The boundary is between Layer 4 and Layer 6:** everything from city markers downward is seed-derived and never persisted as generator output. The `markers.json` city entries are the stored anchor. Below that, the world is fully deterministic from `world_seed`.
|
||
|
||
### The SeedChain
|
||
|
||
Every child seed is derived from its parent using FNV-1a (per D-010's determinism mandate):
|
||
|
||
```rust
|
||
fn child_seed(parent: u64, discriminant: u64) -> u64 {
|
||
let mut h = parent ^ 0xcbf29ce484222325;
|
||
for byte in discriminant.to_le_bytes() {
|
||
h ^= byte as u64;
|
||
h = h.wrapping_mul(0x100000001b3);
|
||
}
|
||
h
|
||
}
|
||
|
||
district_seed = child_seed(world_seed, district_id)
|
||
chunk_seed = child_seed(district_seed, (cx as u64) << 32 | (cy as u64))
|
||
```
|
||
|
||
This means:
|
||
- Any district can be generated in isolation, without generating its neighbors first
|
||
- Any chunk can be generated in isolation, given its district_seed
|
||
- Generation is embarrassingly parallelizable
|
||
- No global state is required
|
||
|
||
### Implication for save files
|
||
|
||
Save files store:
|
||
- `world_seed: u64` (the single master seed)
|
||
- `markers_snapshot: HashMap<BodyId, MarkersJson>` (or a hash of it for validation)
|
||
- `chunk_mutations: BTreeMap<ChunkCoord, ChunkMutations>` (only player-modified chunks)
|
||
|
||
Everything else is re-derived on load. A save file for a large world stays small.
|
||
|
||
---
|
||
|
||
## Layer-by-Layer Architecture Proposal
|
||
|
||
### Layer 6: City → District Decomposition
|
||
|
||
**Inputs:**
|
||
- `body_id: &str`
|
||
- `city: &AtlasCity` (from systems.db atlas_cities table — `id`, `kind`, `population`)
|
||
- `body: &Body` (from systems.db bodies table — `settlement_pattern`, `economic_role`, `planet_class`)
|
||
- `world_seed: u64`
|
||
|
||
**Algorithm:**
|
||
|
||
```rust
|
||
fn decompose_city(city: &AtlasCity, body: &Body, world_seed: u64) -> Vec<DistrictPlacement> {
|
||
let n = district_count(city.population, city.kind, body.settlement_pattern);
|
||
let layout = city_layout_grid(n, city_seed(world_seed, &city.id));
|
||
layout.iter().enumerate().map(|(i, grid_pos)| DistrictPlacement {
|
||
district_id: district_id(world_seed, &body.body_id, &city.id, i as u32),
|
||
world_origin: grid_to_sim_tiles(grid_pos), // (col * 512, row * 512) in sim tiles
|
||
city_id: city.id.clone(),
|
||
}).collect()
|
||
}
|
||
```
|
||
|
||
**District count formula:**
|
||
|
||
```
|
||
population → n_districts:
|
||
0 → 0 (uninhabited)
|
||
1–50k → 1
|
||
50k–500k → 2–4 (log-scaled)
|
||
500k–5M → 4–9 (log-scaled)
|
||
5M+ → 9–16 (capped at 4×4)
|
||
capital → +1 bonus district
|
||
domed/cave → fixed 1 regardless of population
|
||
```
|
||
|
||
**District positioning:** A simple grid arrangement in city-local space. District at grid position (col, row) gets world origin `(col * 512, row * 512)` sim tiles. Grid dimensions chosen to be roughly square for the given count (e.g., 4 districts → 2×2; 6 districts → 3×2). Exact grid shape is seeded for slight variety (some cities are long and thin, some square).
|
||
|
||
**OQ-R1-C resolution:** `primary_function`, `planet_class`, and `settlement_pattern` are available in `systems.db` bodies table and do NOT need to be added to `markers.json`. The decomposition function reads them from systems.db directly, cross-referenced by body_id. `generate_atlas.py` does not need modification.
|
||
|
||
**Output:**
|
||
|
||
```rust
|
||
struct DistrictPlacement {
|
||
district_id: DistrictId, // u64, deterministic from world_seed + body + city + index
|
||
world_origin: (i64, i64), // sim tile position of district's (0,0) corner
|
||
city_id: String,
|
||
}
|
||
```
|
||
|
||
**Testing:** Pure function. Unit test: given (population, settlement_pattern), district count matches formula. Given same (city, body, seed), district positions are identical across runs. Given different seeds, positions vary. No ECS needed.
|
||
|
||
---
|
||
|
||
### Layer 7: Phase 1 — DistrictSkeleton (Spatial Structure Only)
|
||
|
||
**Scope for minimum vertical slice:** Stages 1 and 2 only. Stages 3 (reservations), 4 (social sites), and 5 (guarantee audit) are deferred — they produce spatial enrichment but are not required for a walkable tile.
|
||
|
||
**Stage 1: Classification**
|
||
|
||
```rust
|
||
fn classify_district(placement: &DistrictPlacement, body: &Body, city: &AtlasCity)
|
||
-> (WorldTier, ComplexityTier, SettingType, DistrictType)
|
||
```
|
||
|
||
- `WorldTier`: derived from system economic tier in systems.db (`economic_role`, system connectivity). Hub systems → `Epicenter`; standard → `Regional` or `Backwater`; transit stops → `Passage`.
|
||
- `ComplexityTier`: derived from WorldTier ceiling table (workshop-outcomes.md)
|
||
- `SettingType`: derived from `planet_class` + `economic_role` + city `kind`. Capital on a standard planet → `Urban`. Rural body → `Agricultural`. Orbital station → `Station`.
|
||
- `DistrictType`: derived from city `kind` + district index within city (center district → `Commercial` or `MixedUse`; outer districts → `Residential`, `Industrial`, `Transit`)
|
||
|
||
All deterministic from upstream data. No RNG needed at Stage 1 — classification is a pure lookup/mapping.
|
||
|
||
**Stage 2: Block grid**
|
||
|
||
```rust
|
||
fn generate_block_grid(district_seed: u64, district_type: &DistrictType,
|
||
complexity: &ComplexityTier) -> [[BlockSkeleton; 4]; 4]
|
||
```
|
||
|
||
- `DistrictLayoutMode`: seeded choice between `Grid` and `Organic`. Weight toward Grid for Commercial/Administrative, toward Organic for Residential/Mixed. Lead decision L-2: both modes must coexist.
|
||
- Per block: assign `ZoningType` from a seeded draw against the district's type-to-zoning probability table. Commercial district → mostly Commercial blocks with some Residential edges.
|
||
- Per block: assign `density_pct` seeded from district_seed + block position. Creates density gradient (center blocks denser than edge blocks for Urban districts).
|
||
- `Era`: stub string for now (`"standard"`). Real era differentiation is later work.
|
||
- Everything else on `BlockSkeleton` (`chunk_layout`, `hosted_sites`, `landmark`) → left as empty/stub.
|
||
|
||
**Key constraint:** The block grid is fully determined by `(district_seed, district_type, complexity)`. No external lookups at generation time. The entire `DistrictSkeleton` can be re-derived from those three inputs plus `world_seed`.
|
||
|
||
**Missing fields to add:** Per Gap C from Round 1, these three fields are required by the workshop spec and absent from the current struct:
|
||
- `vertical_structure: VerticalStructure` — add as stub enum `VerticalStructure::Flat` for now
|
||
- `breach_only_zones: Vec<ZoneId>` — add as `Vec<String>`, empty for minimum slice
|
||
- `derived_analysis: DerivedDistrictAnalysis` — add as stub struct, all fields `None`/default
|
||
|
||
These are 0.5 days of type work.
|
||
|
||
**WorldTier fix:** The enum must be corrected before any generation work starts. One-line change, 0.5 days including test update.
|
||
|
||
---
|
||
|
||
### Layer 8: Phase 2 — ChunkData Tile Generation (Spatial Only)
|
||
|
||
This layer converts a `BlockSkeleton` into a 64×64 sim-tile grid of walkable/non-walkable tiles with tile type assignments.
|
||
|
||
**Inputs:**
|
||
- `block: &BlockSkeleton` (zoning, density_pct, layout from Phase 1)
|
||
- `chunk_coord: ChunkCoord` (which 2×2 chunk-within-block this is)
|
||
- `chunk_seed: u64` (derived from district_seed + chunk_coord)
|
||
|
||
**Output:** A 64×64 tile grid (the raw `GeneratorChunkData`)
|
||
|
||
**Minimum viable generation algorithm:**
|
||
|
||
The key insight: for the minimum slice, we don't need heritage grammar, room templates, or furniture. We need:
|
||
1. Street tiles (walkable, visually distinct)
|
||
2. Building exterior tiles (wall, non-walkable)
|
||
3. Building interior floor tiles (walkable)
|
||
|
||
```rust
|
||
fn generate_chunk_tiles(block: &BlockSkeleton, within_block_pos: (u8, u8),
|
||
chunk_seed: u64) -> GeneratorChunkData {
|
||
let density = block.density_pct;
|
||
let mut rng = SimRng::from_seed(chunk_seed);
|
||
|
||
// Step 1: Fill with street tile
|
||
// Step 2: Place building footprints based on density
|
||
// - density 0-20%: 0-1 small buildings
|
||
// - density 20-60%: 2-4 medium buildings
|
||
// - density 60-100%: 4-8 buildings, larger, tighter
|
||
// Step 3: For each footprint: fill interior with floor tile, perimeter with wall tile
|
||
// Step 4: Ensure street connectivity (no building can fully block a chunk edge)
|
||
}
|
||
```
|
||
|
||
**Building footprint algorithm:** Simple non-overlapping rectangle placement using seeded random dimensions within density budget. Minimum building: 6×6 tiles. Minimum street gap between buildings: 4 tiles. This is not artistically sophisticated but produces walkable, navigable space.
|
||
|
||
**Tile vocabulary for minimum slice:**
|
||
|
||
```rust
|
||
// Minimum TileId set (just string constants to start)
|
||
const TILE_FLOOR_STREET: &str = "floor_street";
|
||
const TILE_FLOOR_INTERIOR: &str = "floor_interior";
|
||
const TILE_WALL: &str = "wall";
|
||
```
|
||
|
||
Three tile types. Everything needed for walkability and basic navigation. Visual polish is a later pass.
|
||
|
||
**`GeneratorChunkData` type change:** The current type alias `pub type GeneratorChunkData = Vec<bool>` must become a proper struct to carry tile IDs, not just walkability. Minimum:
|
||
|
||
```rust
|
||
pub struct GeneratorChunkData {
|
||
pub tiles: Vec<TileEntry>, // 64×64 = 4096 entries
|
||
}
|
||
|
||
pub struct TileEntry {
|
||
pub tile_id: TileId, // "floor_street", "wall", etc.
|
||
pub walkable: bool, // pre-computed for the movement system
|
||
}
|
||
```
|
||
|
||
This is a type-level change that breaks the placeholder `ChunkData::new_walkable()` usage cleanly — making the transition visible rather than silent.
|
||
|
||
---
|
||
|
||
### Layer 9: Chunk Streaming → Generator Wiring
|
||
|
||
The chunk streaming architecture is ready. The hookup requires two additions:
|
||
|
||
**Addition 1: `DistrictMap` resource**
|
||
|
||
```rust
|
||
#[derive(Resource)]
|
||
pub struct DistrictMap {
|
||
/// Maps a chunk coord to the district it belongs to.
|
||
/// Built at world load time from DistrictPlacement data.
|
||
chunks: BTreeMap<ChunkCoord, DistrictId>,
|
||
/// The Phase 1 skeleton for each loaded district.
|
||
skeletons: BTreeMap<DistrictId, DistrictSkeleton>,
|
||
}
|
||
```
|
||
|
||
**Addition 2: Replace `load_chunk()` placeholder**
|
||
|
||
```rust
|
||
pub fn load_chunk(&mut self, coord: ChunkCoord, district_map: &DistrictMap,
|
||
world_seed: u64) -> bool {
|
||
if self.chunks.contains_key(&coord) { return false; }
|
||
|
||
let chunk_data = if let Some(district_id) = district_map.chunks.get(&coord) {
|
||
let skeleton = &district_map.skeletons[district_id];
|
||
let block = block_for_chunk(skeleton, coord);
|
||
let chunk_seed = child_seed(child_seed(world_seed, *district_id),
|
||
coord_to_u64(coord));
|
||
generate_chunk_tiles(block, within_block_pos(coord), chunk_seed)
|
||
} else {
|
||
// Outside any district — wilderness, open terrain
|
||
GeneratorChunkData::all_walkable()
|
||
};
|
||
|
||
self.chunks.insert(coord, chunk_data);
|
||
true
|
||
}
|
||
```
|
||
|
||
This is the complete wiring. No other changes to `chunk_streaming.rs` are needed.
|
||
|
||
---
|
||
|
||
## Data Format Boundaries
|
||
|
||
The handoff between Python tooling and Rust runtime is clean and already exists:
|
||
|
||
```
|
||
PYTHON TOOLING (offline pipeline)
|
||
systems.db → bodies, star_systems, economics, atlas_cities tables
|
||
markers.json → city positions and populations (per body)
|
||
|
||
↓ [HANDOFF — server startup reads these two sources]
|
||
|
||
RUST SERVER (runtime)
|
||
At startup: read systems.db + markers.json → build DistrictMap in memory
|
||
On load_chunk(): derive ChunkData from district_seed + block_skeleton
|
||
On save: persist only ChunkMutations (player-modified chunks)
|
||
```
|
||
|
||
**The handoff is currently wired:** `server/src/main.rs` references the generator. Systems.db is read at startup. The gap is not the boundary mechanism — it's that no code reads atlas_cities to build a DistrictMap.
|
||
|
||
**No Python layer needed for Phase 1 or Phase 2.** The entire spatial generation below city markers is Rust code, working against data already in systems.db. This is clean.
|
||
|
||
---
|
||
|
||
## Testing Strategy Per Layer
|
||
|
||
Each layer produces deterministic output and can be validated independently.
|
||
|
||
### City decomposition
|
||
```rust
|
||
#[test]
|
||
fn decompose_city_deterministic() {
|
||
let result1 = decompose_city(&city, &body, 42);
|
||
let result2 = decompose_city(&city, &body, 42);
|
||
assert_eq!(result1, result2);
|
||
}
|
||
|
||
#[test]
|
||
fn district_count_formula() {
|
||
assert_eq!(district_count(0, "city", "standard"), 0);
|
||
assert_eq!(district_count(10_000, "city", "standard"), 1);
|
||
assert_eq!(district_count(1_000_000, "capital", "standard"), 5); // 4 + 1 capital bonus
|
||
}
|
||
```
|
||
|
||
Fully unit-testable. No ECS, no file I/O.
|
||
|
||
### Phase 1 DistrictSkeleton
|
||
```rust
|
||
#[test]
|
||
fn skeleton_same_seed_identical() {
|
||
let s1 = generate_skeleton(placement, body, city, 42);
|
||
let s2 = generate_skeleton(placement, body, city, 42);
|
||
assert_eq!(s1, s2); // requires PartialEq on DistrictSkeleton — needs real types, not String stubs
|
||
}
|
||
|
||
#[test]
|
||
fn block_grid_coverage() {
|
||
let s = generate_skeleton(placement, body, city, 42);
|
||
// Every block has a valid ZoningType
|
||
for row in &s.blocks {
|
||
for block in row {
|
||
assert!(block.zoning != ZoningType::Mixed || s.district_type == DistrictType::MixedUse);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Requires replacing String stub types with real enums/structs (which is also needed to implement generation). Tests become the specification.
|
||
|
||
### Phase 2 ChunkData
|
||
```rust
|
||
#[test]
|
||
fn chunk_has_walkable_tiles() {
|
||
let data = generate_chunk_tiles(&block, (0, 0), 99);
|
||
let walkable_count = data.tiles.iter().filter(|t| t.walkable).count();
|
||
assert!(walkable_count >= 100, "chunk must have ≥100 walkable tiles for NPC routing");
|
||
}
|
||
|
||
#[test]
|
||
fn chunk_edges_walkable() {
|
||
// Ensure no chunk boundary is fully blocked (prevents dead zones at chunk seams)
|
||
let data = generate_chunk_tiles(&block, (0, 0), 99);
|
||
// at least 4 walkable tiles on each edge
|
||
}
|
||
|
||
#[test]
|
||
fn chunk_deterministic() {
|
||
let d1 = generate_chunk_tiles(&block, (0, 0), 42);
|
||
let d2 = generate_chunk_tiles(&block, (0, 0), 42);
|
||
assert_eq!(d1.tiles, d2.tiles);
|
||
}
|
||
```
|
||
|
||
For visual/rendering validation: a Gauntlet room with a generated district. Run `--test-mode SR_LIVE=1` and walk the tiles manually. Per CLAUDE.md, new room added — existing Gauntlet rooms are not modified.
|
||
|
||
### End-to-end integration
|
||
Spawn a player in a generated city. Verify:
|
||
1. All chunks within `ChunkLoadRadius` load without panic
|
||
2. Player can move (walkability map is non-empty)
|
||
3. Second load produces identical tile layout (determinism check)
|
||
|
||
This is a live server test using existing `--test-mode` infrastructure.
|
||
|
||
---
|
||
|
||
## Effort Estimates — Revised
|
||
|
||
My Round 1 estimate (9 days) was contaminated by heritage grammar work that is now out of scope. Revised for spatial-only:
|
||
|
||
| Item | Effort | Notes |
|
||
|------|--------|-------|
|
||
| Fix WorldTier enum + test updates | 0.5d | One enum change, propagate through compilation errors |
|
||
| Add missing DistrictSkeleton fields as stubs | 0.5d | Type-only change; no logic |
|
||
| Replace String stubs with real types (Stages 1-2 scope) | 1d | `DistrictContext`, `DistrictBoundaries`, etc. — enough to implement Stages 1-2 |
|
||
| City-to-district decomposition | 1d | Pure function, well-defined algorithm, unit-testable |
|
||
| Server startup: read atlas data → build DistrictMap | 0.5d | Reading systems.db + markers.json; already have DB access patterns |
|
||
| Phase 1 generation (Stages 1-2 only) | 1.5d | Classification lookup + seeded block grid |
|
||
| `GeneratorChunkData` type upgrade (Vec<bool> → TileEntry grid) | 0.5d | Type change + update `new_walkable()` caller |
|
||
| Phase 2 minimal tile generation | 1.5d | Floor/wall placement from density, seeded rectangles |
|
||
| Chunk streaming wiring | 0.5d | `DistrictMap` resource + dispatch in `load_chunk()` |
|
||
| Integration test (walkable generated city) | 0.5d | Gauntlet room + live test |
|
||
|
||
**Total: ~8 days** — but only ~6 of those are truly sequential (critical path). The type changes and test writing can overlap with algorithm work.
|
||
|
||
**Minimum parallelizable scope:** If two developers were available:
|
||
- Dev A: Layers 6-7 (city decomp + Phase 1)
|
||
- Dev B: Layers 8-9 (Phase 2 + streaming wiring, stubbing Phase 1 output)
|
||
|
||
Parallelized: ~4 days wall-clock.
|
||
|
||
### Reconciling with Gestalt's estimate
|
||
|
||
Gestalt's 5-7 days appears to assume the String stub replacements are free (fold into Phase 1 work) and the startup/DistrictMap wiring is minimal. Both are reasonable simplifications. The key actual divergence: I have explicit line items for the type upgrade (`GeneratorChunkData`) and the server startup path that Gestalt may have assumed as already wired. Neither of us included heritage grammar — that was the bulk of the Round 1 overestimate.
|
||
|
||
Gestalt's floor estimate (~5 days) represents the fastest possible path with no unexpected compilation surprises from the String-to-real-type migrations. My 8 days is the conservative estimate including that friction. True answer is somewhere between: **~6-7 days** under reasonable conditions.
|
||
|
||
---
|
||
|
||
## Open Question Resolutions
|
||
|
||
### OQ-R1-D (Atlas coordinate translation)
|
||
Resolved: there is no pixel-to-sim-tile formula. The atlas coordinates are UI-only. The walkable world uses city-local coordinates starting at (0, 0). See "Core Architecture Insight" section above.
|
||
|
||
### OQ-R1-C (City marker schema)
|
||
Resolved: `primary_function`, `planet_class`, `settlement_pattern` are read from `systems.db` bodies table by cross-reference on `body_id`. `generate_atlas.py` does not need modification.
|
||
|
||
### OQ-R1-E (Effort estimate)
|
||
Resolved: ~6-7 days for minimum spatial vertical slice (spatial only, no heritage grammar, no social sites). Conservative estimate is 8 days including type migration friction.
|
||
|
||
### OQ-R1-F (#615 cascade position)
|
||
Per lead directive: #615 is NPC territory and out of scope for this workshop. No position taken.
|
||
|
||
---
|
||
|
||
## What This Proposal Defers
|
||
|
||
To be explicit about what is NOT in scope for the vertical slice:
|
||
|
||
- Heritage grammar (Phase 2 tile aesthetics beyond floor/wall)
|
||
- Room templates (rectangular buildings replace proper room grammar)
|
||
- DistrictSkeleton Stages 3-5 (reservations, social sites, guarantee audit)
|
||
- Building interiors with furniture or objects
|
||
- Vertical structure / z-levels beyond ground floor
|
||
- MobileChunk
|
||
- NPC placement
|
||
|
||
None of these are needed for the question "can the player walk through a generated district?" They can be layered in after the spatial plumbing exists.
|