Planet-down cascade workshop (3 rounds, 5 agents): layer-by-layer generation from empty world through population overlay, city planning, and street rendering. Includes consultant review by Troblum. Also commits: pre-Sprint-35 DB backup, Claude Code team-mode tmux test log (team-test.md). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
739 lines
33 KiB
Markdown
739 lines
33 KiB
Markdown
---
|
||
title: "Tyre Round 3 — Planet-Down Cascade Workshop"
|
||
author: tyre
|
||
workshop: planet-down-cascade
|
||
round: 3
|
||
created: 2026-05-01
|
||
---
|
||
|
||
# Tyre Round 3 — Planet-Down Cascade Workshop
|
||
|
||
## Summary
|
||
|
||
Round 3 convergence document. Contains D-record candidates for all locked technical decisions, resolution of four open questions from Round 2, finalized Rust types and SQL DDL, and the implementation ticket dependency chain.
|
||
|
||
Lead-resolved before Round 3: two-tier mismatch flagging (`score < 0.35` = warning, `score < 0.15` = error/blocks).
|
||
|
||
---
|
||
|
||
## 1. Lead-Resolved Items
|
||
|
||
### Q1 — Mismatch Flag Threshold (LOCKED)
|
||
|
||
Two-tier system:
|
||
|
||
- `score < 0.35`: `MismatchSeverity::Soft` — `flagged_for_review = true`; placement proceeds with warning in generation log
|
||
- `score < 0.15`: `MismatchSeverity::Hard` — assignment overridden to `Synthetic { reason: PoliticalDecision }`; city placed at Province centroid; `FoundingOrientation = AdminFacing`; generation proceeds without panic
|
||
|
||
```rust
|
||
enum MismatchSeverity {
|
||
None, // best attractor score >= 0.35
|
||
Soft, // 0.15 <= score < 0.35; placement proceeds with flag
|
||
Hard, // score < 0.15; overridden to Synthetic
|
||
}
|
||
```
|
||
|
||
`Hard` mismatch is not a generation failure — it's a policy decision. The city exists but was placed politically, not geographically. The flag is surfaced in developer tooling and the eventual city history log.
|
||
|
||
---
|
||
|
||
## 2. Convergence Items — Round 3 Resolutions
|
||
|
||
### Q2 — founding_age → layout_mode (Ozzie's Proposal)
|
||
|
||
**Proposal:** founding age should influence block geometry, making old cities irregular and young cities grid-like.
|
||
|
||
**Technical evaluation:**
|
||
|
||
The five spatial archetypes (CompanyTown, AdminCapital, FreePort, Contested, OrganicGrowth) determine *macro geometry* — spine, radial, multi-node. These reflect how the city was *designed* and should not be modified by age.
|
||
|
||
Block geometry is orthogonal: it reflects how the city *evolved after* being designed. A CompanyTown laid out as a rigid grid in Year 1 may have irregular blocks in Year 200 as organic infill accumulated around the original plan.
|
||
|
||
**Resolution: ADOPT via `BlockIrregularity` field — do NOT modify `layout_mode`.**
|
||
|
||
`layout_mode` (archetype) stays fixed. A new `block_irregularity: BlockIrregularity` field is added to `DistrictSkeleton`, derived from `founding_age_years` and `SpatialArchetype`. This is orthogonal to the archetype — CompanyTown can be Grid or Organic depending on age.
|
||
|
||
```rust
|
||
enum BlockIrregularity {
|
||
Grid, // Regular 64m × 64m blocks; planned geometry
|
||
SlightlyWorn, // ~10% deviation from grid; some organic infill
|
||
Irregular, // ~25% deviation; multiple planning generations visible
|
||
Organic, // No regular grid; emerged rather than planned
|
||
}
|
||
|
||
impl BlockIrregularity {
|
||
fn from_age_and_archetype(founding_age_years: u32, archetype: SpatialArchetype) -> Self {
|
||
// Reference age (years to reach next irregularity tier)
|
||
let step: u32 = match archetype {
|
||
SpatialArchetype::CompanyTown => 40, // Company towns evolve quickly
|
||
SpatialArchetype::AdminCapital => 100, // State capitals resist change
|
||
SpatialArchetype::FreePort => 60,
|
||
SpatialArchetype::Contested => 35, // Conflict accelerates irregularity
|
||
SpatialArchetype::OrganicGrowth => 20, // Always evolving; reaches Organic fastest
|
||
};
|
||
match founding_age_years / step.max(1) {
|
||
0 => Self::Grid,
|
||
1 => Self::SlightlyWorn,
|
||
2 => Self::Irregular,
|
||
_ => Self::Organic,
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`block_irregularity` is a `DistrictSkeleton` field, not `CityGenerationContext` — it varies per district within a city. Derivation is fully local; no cross-city queries.
|
||
|
||
**Phase implication:** This field is set at Layer 3 and consumed at Layer 4 (street rendering). It has no effect on Phase 2 or 3 deliverables. No rework to the existing convergence.
|
||
|
||
---
|
||
|
||
### Q3 — Province Boundary Legibility
|
||
|
||
**Requirement (Ozzie, non-negotiable):** Province boundaries must render as natural watershed lines on the planetary map, not arbitrary grid edges.
|
||
|
||
**Technical resolution:**
|
||
|
||
Province boundaries are ridgelines between drainage basins. The D8 drainage simulation computes flow direction per cell; ridgelines are cells where no adjacent cell drains into the current cell from the "wrong side." Extracting boundary polylines from a D8 result is O(grid_size) — standard watershed delineation.
|
||
|
||
**Two options evaluated:**
|
||
|
||
Option A: Rust runtime computes drainage → extracts boundaries → stores in savegame DB.
|
||
Option B: Python pipeline computes drainage at build time → extracts boundaries → stores in systems.db.
|
||
|
||
**Resolution: Option B.** The planetary map must render Province boundaries immediately when the player opens it, before any Rust generation has run. Option A introduces a generation-status dependency in the renderer. Option B eliminates it: boundaries are pre-computed at `make regen-db` time and available on first map open.
|
||
|
||
`planet_simulation.py` already computes a DEM per body. Watershed boundary extraction is a post-processing step over the same data. The Python implementation does not need the full D8 precision of the Rust runtime — it's a rendering hint, not game logic.
|
||
|
||
**New schema:**
|
||
|
||
```sql
|
||
CREATE TABLE atlas_province_boundaries (
|
||
body_id INTEGER NOT NULL REFERENCES bodies(id),
|
||
province_x INTEGER NOT NULL,
|
||
province_y INTEGER NOT NULL,
|
||
boundary BLOB NOT NULL,
|
||
-- float32 pairs [u0,v0, u1,v1, ...] in atlas UV space (0.0..1.0)
|
||
-- Boundary polyline tracing the natural watershed outline of this province
|
||
PRIMARY KEY (body_id, province_x, province_y)
|
||
);
|
||
CREATE INDEX idx_province_bounds_body ON atlas_province_boundaries(body_id);
|
||
```
|
||
|
||
Storage estimate: ~20 float32 pairs per province boundary segment × ~64 segments per body → ~82KB/body → ~32MB for 400 bodies. Acceptable.
|
||
|
||
**Renderer contract:**
|
||
- On map open: load all `boundary` BLOBs for `body_id`; render each as a polyline in atlas UV space
|
||
- No dependency on `BodyWorldState` generation status
|
||
- The Rust L1 drainage computes the same basin topology independently for generation purposes; renderer never waits for it
|
||
|
||
**Pipeline change:** `generate_atlas.py` adds a watershed extraction step after heightmap storage. The DDL addition goes into `import_economics.py`'s `MIGRATION_SQL` block (runs first; creates the table); `generate_atlas.py` populates it.
|
||
|
||
---
|
||
|
||
### Q5 — atlas_city_names Population Path
|
||
|
||
**Question:** Who writes source rows to `atlas_city_names` before Stage 0 fulfillment?
|
||
|
||
**Analysis:** The schema has `corp_id` and `tier_hint` — deliberate authoring fields. But with ~400 bodies and 10–30 cities each, hand-authoring all names is not tractable.
|
||
|
||
**Resolution: Two-tier authorship.**
|
||
|
||
**Tier 1 — Authored (`reserved = true`):** World designers write named cities for canon locations in `wiki/worlds/{body_slug}.toml`. These are named places with canonical identities — they appear in lore, faction text, and player-facing narrative. Imported by `import_economics.py`. All `corp_id` and `tier_hint` populated by the author.
|
||
|
||
```toml
|
||
# wiki/worlds/nova-kassel.toml
|
||
[[cities]]
|
||
name = "Port Cassidy"
|
||
corp_slug = "meridian-transit" # nullable
|
||
tier_hint = 3 # nullable; expected WorldTier integer
|
||
reserved = true
|
||
```
|
||
|
||
**Tier 2 — Generated (`reserved = false`):** `import_economics.py` generates additional name rows from the corporation brand files. Corp HQ worlds receive at least one corp-affiliated city name derived from brand name + city-name templates from the `generate_brands` output. These are placeholders — no specific lore attachment; placed by attractor-matching opportunistically.
|
||
|
||
Source population order in `import_economics.py`:
|
||
1. Preserve existing `reserved = true` rows for the body across reruns
|
||
2. Delete existing `reserved = false` rows (regenerated fresh each run)
|
||
3. Import authored rows from `wiki/worlds/*.toml` → `reserved = true`
|
||
4. Generate corp-derived name rows for bodies below `corp_city_quota` → `reserved = false`
|
||
|
||
Paula's Stage 0–3 fulfillment pipeline (runtime) operates on whatever rows exist at generation time, regardless of source.
|
||
|
||
---
|
||
|
||
### Q6 — Full Multiplier Table (Locked)
|
||
|
||
Adding `Logistics` as the 8th district type (warehousing, distribution, freight staging). The 7-column table from Round 2 omits this type; it's essential for Transit/Port and Manufacturing cities and has minimum 3 weight across all rows.
|
||
|
||
**Final locked table** (all rows sum to 100; minimum value across all cells: 3):
|
||
|
||
| Economic Role | Res | Com | Ind | Adm | Ent | Civ | Mix | Log |
|
||
|-------------------|-----|-----|-----|-----|-----|-----|-----|-----|
|
||
| Mining/Extraction | 33 | 11 | 26 | 8 | 3 | 5 | 9 | 5 |
|
||
| Manufacturing | 27 | 13 | 23 | 8 | 5 | 7 | 10 | 7 |
|
||
| Research Hub | 26 | 17 | 9 | 15 | 8 | 12 | 9 | 4 |
|
||
| Commercial Hub | 20 | 28 | 7 | 10 | 12 | 8 | 10 | 5 |
|
||
| Administrative | 18 | 14 | 5 | 28 | 8 | 15 | 7 | 5 |
|
||
| Transit/Port | 20 | 16 | 13 | 7 | 5 | 5 | 16 | 18 |
|
||
| Energy | 30 | 9 | 27 | 8 | 3 | 6 | 10 | 7 |
|
||
| Agricultural | 32 | 16 | 9 | 5 | 7 | 8 | 16 | 7 |
|
||
|
||
Row sums: 100, 100, 100, 100, 100, 100, 100, 100. Minimum cell value: 3 (Mining/Ent and Energy/Ent). Floor invariant holds.
|
||
|
||
**Political archetype modifiers (applied after role table; floor at 3):**
|
||
|
||
All five archetypes need modifiers. Round 2 specified three (CompanyTown, AdminCapital, FreePort). The two missing ones are proposed here and require lead confirmation before locking.
|
||
|
||
| Archetype | Adjustment |
|
||
|-----------------|------------|
|
||
| CompanyTown | Administrative −10, Industrial +10 |
|
||
| AdminCapital | Administrative +15, Commercial −8, Entertainment −7 |
|
||
| FreePort | Commercial +12, Mixed +8, Administrative −20 |
|
||
| Contested | Mixed +10, Civic −5, Administrative −5 *(proposed — needs lead confirmation)* |
|
||
| OrganicGrowth | Mixed +15, Commercial +5, Administrative −10, Industrial −10 *(proposed — needs lead confirmation)* |
|
||
|
||
Note: modifier application must re-check the floor — if any cell drops below 3 after modifier, clamp to 3 and redistribute the deficit proportionally across the row.
|
||
|
||
---
|
||
|
||
## 3. D-Record Candidates
|
||
|
||
These are proposed D-records. IDs must be claimed via `tooling/db/decision claim D <domain> "title"` before writing to `decisions/` domain files. All items require corresponding implementation tickets.
|
||
|
||
---
|
||
|
||
### D-candidate: Heightmap Storage Schema (ARCH-1)
|
||
|
||
**Domain:** architecture
|
||
**Decision:** Planetary heightmaps are stored as float32 LE BLOBs in `atlas_body_heightmaps` in systems.db; written by `generate_atlas.py`; loaded into `BodyWorldState` via `bytemuck::cast_slice`.
|
||
|
||
```sql
|
||
CREATE TABLE atlas_body_heightmaps (
|
||
body_id INTEGER NOT NULL REFERENCES bodies(id),
|
||
data BLOB NOT NULL, -- float32 LE, 512×256 = 524,288 bytes
|
||
PRIMARY KEY (body_id)
|
||
);
|
||
```
|
||
|
||
```rust
|
||
fn load_heightmap(conn: &Connection, body_id: i64) -> Result<Vec<f32>> {
|
||
let data: Vec<u8> = conn.query_row(
|
||
"SELECT data FROM atlas_body_heightmaps WHERE body_id = ?1",
|
||
[body_id], |row| row.get(0),
|
||
)?;
|
||
Ok(bytemuck::cast_slice(&data).to_vec())
|
||
}
|
||
```
|
||
|
||
Storage: ~512KB/body × 400 bodies = ~200MB. Coordinate system: row 0 = north pole, 512 columns (longitude), 256 rows (latitude). `bytemuck::cast_slice` is zero-copy on native endian architectures.
|
||
|
||
---
|
||
|
||
### D-candidate: BodyWorldState as Bevy Resource (ARCH-2)
|
||
|
||
**Domain:** architecture
|
||
**Decision:** Session-level generation state lives in a Bevy `Resource` (`GenerationCache`). Never serialized. Fully reproducible from `seed` + systems.db. LRU cap: 50 bodies.
|
||
|
||
```rust
|
||
#[derive(Resource)]
|
||
struct GenerationCache {
|
||
entries: LruCache<i64, Arc<BodyWorldState>>,
|
||
}
|
||
|
||
struct BodyWorldState {
|
||
body_id: i64,
|
||
seed: u64,
|
||
heightmap: Vec<f32>, // 512×256
|
||
river_network: RiverNetwork, // D8 drainage output (regional summary, not full grid)
|
||
attractors: Vec<GeoAttractor>,
|
||
settlements: Vec<GeneratedSettlement>,
|
||
provinces: Vec<ProvinceWorldState>,
|
||
generated_at: std::time::Instant,
|
||
}
|
||
```
|
||
|
||
Memory budget: ~5MB for 50 bodies (full 512×256 accumulation grid discarded post-extraction; only 64×32 regional summary retained). `Arc<BodyWorldState>` for cheap cross-system sharing without cache lock contention. LRU eviction is safe — evicted bodies re-generate deterministically on next access.
|
||
|
||
---
|
||
|
||
### D-candidate: City Name Reservation Schema and Population Path (ARCH-3)
|
||
|
||
**Domain:** architecture
|
||
**Decision:** `atlas_city_names` stores name reservations, not positions. Rows come from two sources: authored TOML files (`reserved = true`) and generated corp-derived names (`reserved = false`). Both flow through `import_economics.py`. Runtime Stage 0–3 fulfillment assigns names to generated settlement positions.
|
||
|
||
```sql
|
||
CREATE TABLE atlas_city_names (
|
||
id INTEGER PRIMARY KEY,
|
||
body_id INTEGER NOT NULL REFERENCES bodies(id),
|
||
name TEXT NOT NULL,
|
||
corp_id INTEGER REFERENCES corporations(id),
|
||
tier_hint INTEGER,
|
||
reserved BOOLEAN NOT NULL DEFAULT 0
|
||
);
|
||
CREATE INDEX idx_city_names_body ON atlas_city_names(body_id);
|
||
```
|
||
|
||
Source TOML: `wiki/worlds/{body_slug}.toml` → `[[cities]]` arrays. Authoring format documented in Q5 resolution above.
|
||
|
||
---
|
||
|
||
### D-candidate: Body Radius Column (ARCH-4)
|
||
|
||
**Domain:** architecture
|
||
**Decision:** `bodies.body_radius_km` nullable REAL column; Rust reads with `planet_class` fallback.
|
||
|
||
```sql
|
||
ALTER TABLE bodies ADD COLUMN body_radius_km REAL;
|
||
```
|
||
|
||
```rust
|
||
fn body_radius_km(row: &Row) -> f64 {
|
||
row.get::<_, Option<f64>>("body_radius_km")
|
||
.unwrap_or(None)
|
||
.unwrap_or_else(|| default_radius_for_class(
|
||
row.get("planet_class").unwrap_or("")
|
||
))
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### D-candidate: Province Boundary Pre-Computation (ARCH-5)
|
||
|
||
**Domain:** architecture
|
||
**Decision:** Province boundary polylines are computed at build time by Python (watershed extraction from DEM) and stored in `atlas_province_boundaries` in systems.db. The planetary map renderer loads these directly; no dependency on Rust generation status.
|
||
|
||
```sql
|
||
CREATE TABLE atlas_province_boundaries (
|
||
body_id INTEGER NOT NULL REFERENCES bodies(id),
|
||
province_x INTEGER NOT NULL,
|
||
province_y INTEGER NOT NULL,
|
||
boundary BLOB NOT NULL,
|
||
-- float32 pairs [u0,v0, u1,v1, ...] in atlas UV space (0.0..1.0)
|
||
PRIMARY KEY (body_id, province_x, province_y)
|
||
);
|
||
CREATE INDEX idx_province_bounds_body ON atlas_province_boundaries(body_id);
|
||
```
|
||
|
||
Schema added to `import_economics.py` MIGRATION_SQL (creates table). `generate_atlas.py` populates it after heightmap storage. Renderer loads by `body_id` and renders as polylines.
|
||
|
||
---
|
||
|
||
### D-candidate: D8 Priority-Flood Drainage — Layer 1 (GEN-1)
|
||
|
||
**Domain:** architecture (generation)
|
||
**Decision:** Layer 1 uses D8 priority-flood drainage in Rust to derive river networks and geographic attractors from heightmaps. Target: ~50ms at 512×256.
|
||
|
||
Algorithm:
|
||
1. Load heightmap from `BodyWorldState` (ARCH-2)
|
||
2. D8 single-direction flow: each cell drains to lowest adjacent neighbor (8 directions)
|
||
3. Priority-flood fills sinks: `BinaryHeap<(Reverse<f32>, (usize, usize))>` processes in elevation order
|
||
4. Accumulate drainage area per cell
|
||
5. High-accumulation cells → `RiverNetwork` segments
|
||
6. Extract geographic attractors: `CoastalHarbor` (coastline × high drainage), `MountainPass` (low-elevation saddles), `ResourceConcentration` (tagged from systems.db), `ArablePlain` (low slope × high moisture)
|
||
|
||
Seed: `drainage_seed = child_seed(body_seed, "drainage")` — tie-breaking in priority-flood.
|
||
|
||
---
|
||
|
||
### D-candidate: Five-Phase Attractor Assignment — Layer 2 (GEN-2)
|
||
|
||
**Domain:** architecture (generation)
|
||
**Decision:** City-to-attractor assignment: score matrix + hard zeros → tier sort → Tier A greedy → Tier B/C Hungarian → synthetic overflow.
|
||
|
||
Key parameters:
|
||
- Hard-zero filter: `is_physically_possible()` (H1–H4) runs before scoring; zeros are structural, not low scores
|
||
- Minimum non-zero score: 0.10 (no compatible attractor scores below this floor)
|
||
- Tier A: `MiningExtraction | ResourceExtraction` economic roles (highest geographic constraint)
|
||
- Hungarian: O(N³) maximum-weight bipartite matching; N ≤ 30 cities; defensive fallback at N > 40 (greedy for Tier C cities with score > 0.5)
|
||
- Synthetic overflow: `SyntheticPlacementReason`: `PopulationOverflow | PoliticalDecision | CorpExpansion`
|
||
- Mismatch thresholds (lead-resolved): Soft at 0.35; Hard (→ Synthetic override) at 0.15
|
||
|
||
---
|
||
|
||
### D-candidate: Three-Component District Mix — Layer 3 (GEN-3)
|
||
|
||
**Domain:** architecture (generation)
|
||
**Decision:** District type distribution uses three orthogonal components. Self-contained (no cross-city queries). Full locked multiplier table in Q6 resolution above.
|
||
|
||
Components:
|
||
1. Population tier guarantees — minimum required district counts by population band
|
||
2. Economic role multiplier table — integer weights (sum 100, min 3) × 8 economic roles × 8 district types
|
||
3. Founding age character modifier — affects `prosperity_baseline` and character tags at Backwater+ WorldTier; does NOT change weights
|
||
|
||
Political archetype modifiers applied post-table with floor at 3.
|
||
|
||
---
|
||
|
||
### D-candidate: BlockIrregularity from founding_age — Layer 3 (GEN-4)
|
||
|
||
**Domain:** architecture (generation)
|
||
**Decision:** Block geometry irregularity is a `DistrictSkeleton` field derived from `founding_age_years` × `SpatialArchetype`. Orthogonal to spatial arrangement archetype. Consumed at Layer 4.
|
||
|
||
Full Rust type and derivation function: see Q2 resolution above.
|
||
|
||
Phase 4 usage: Layer 4 tile placement uses `block_irregularity` to vary street width, corner treatments, and block subdivision patterns. `Grid` produces regular tile-aligned 64m blocks. `Organic` produces irregular boundaries with no aligned corners.
|
||
|
||
---
|
||
|
||
### D-candidate: TerritorialStatus Priority-Ordered Derivation (GEN-5)
|
||
|
||
**Domain:** architecture (generation)
|
||
**Decision:** `TerritorialStatus` derives from `ProvinceWorldState` via priority-ordered algorithm. `placed_at_generation: bool` is the sole differentiator between `AbandonedZone` and `WildernessBuffer`.
|
||
|
||
Algorithm and enum values: locked in Round 2; see round-2-notes.md §3.
|
||
|
||
---
|
||
|
||
### D-candidate: SettlementClass Enum (GEN-6)
|
||
|
||
**Domain:** architecture (generation)
|
||
**Decision:** `SettlementClass` generalizes the latent/active distinction. `active: bool` is derived from class conditions at runtime. `placed_at_generation: bool` is set at Layer 2 and immutable.
|
||
|
||
```rust
|
||
enum SettlementClass {
|
||
NameLocked, // Has a name in atlas_city_names; always active
|
||
PopulationBudget, // Active if body_population_density > threshold
|
||
EconomicTriggered, // Active if route_traffic_score > threshold
|
||
OrganicGrowth, // Placed by geographic probability; geographically_triggered = false
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### D-candidate: Background Generation Queue (GEN-7)
|
||
|
||
**Domain:** architecture
|
||
**Decision:** Background body generation uses a rayon thread pool (not async Tokio). Priority ordering based on player position, travel routes, and body name mentions in dialogue text.
|
||
|
||
```rust
|
||
struct GenerationQueue {
|
||
pending: BinaryHeap<Reverse<GenerationRequest>>,
|
||
in_flight: HashSet<i64>,
|
||
completed: HashSet<String>,
|
||
}
|
||
```
|
||
|
||
Priority ordering: player-targeted body → adjacent bodies in travel route → bodies mentioned in dialogue → active corp supply chains → all others.
|
||
|
||
Aho-Corasick `SystemNameIndex`: pattern-matches body/system names in player-facing text (news ticker, NPC dialogue, documents) to trigger pre-generation before the player travels there.
|
||
|
||
---
|
||
|
||
### D-candidate: WorldTier Enum Bug Fix (GEN-8)
|
||
|
||
**Domain:** architecture (bug)
|
||
**Decision:** `WorldTier` in `server/src/simulation/generator.rs` must be `{ Epicenter, Regional, Backwater, Passage, Waypoint }`. Current code `{ Peripheral, Connected, Core }` is wrong. This is a code bug, not a design question.
|
||
|
||
Affected file: `server/src/simulation/generator.rs`, `enum WorldTier` and all match arms.
|
||
Prerequisite for: every generation implementation ticket.
|
||
|
||
---
|
||
|
||
### D-candidate: atlas_feature_names Schema (GEN-9)
|
||
|
||
**Domain:** architecture
|
||
**Decision:** Geographic feature names (rivers, mountain passes, bays) are stored in `atlas_feature_names`, distinct from `atlas_city_names`. Assigned at Layer 1 name fulfillment (Stage 1 of Paula's four-stage pipeline).
|
||
|
||
```sql
|
||
CREATE TABLE atlas_feature_names (
|
||
id INTEGER PRIMARY KEY,
|
||
body_id INTEGER NOT NULL REFERENCES bodies(id),
|
||
name TEXT NOT NULL,
|
||
tag_hint TEXT -- nullable; expected FeatureTag for this name
|
||
);
|
||
CREATE INDEX idx_feature_names_body ON atlas_feature_names(body_id);
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Final Type Specifications
|
||
|
||
### CityGenerationContext — Final Fields
|
||
|
||
```rust
|
||
struct CityGenerationContext {
|
||
// Core identity
|
||
name: String,
|
||
body_id: i64,
|
||
seed: u64,
|
||
|
||
// Location
|
||
location: (u8, u8), // atlas grid cell
|
||
region_size: (u8, u8), // cells the city region occupies
|
||
|
||
// Population and economics
|
||
population: u64,
|
||
economic_role: EconomicRole,
|
||
corp_presence: Vec<CorpPresence>,
|
||
|
||
// Tier (enum values require GEN-8 bug fix)
|
||
world_tier: WorldTier, // Epicenter|Regional|Backwater|Passage|Waypoint
|
||
|
||
// Age and character
|
||
founding_age_years: u32,
|
||
spatial_archetype: SpatialArchetype,
|
||
|
||
// Layer 2 outputs — attractor assignment
|
||
attractor_assignment: AttractorAssignment,
|
||
mismatch_severity: MismatchSeverity,
|
||
|
||
// Layer 2 outputs — settlement class
|
||
settlement_class: SettlementClass,
|
||
active: bool, // derived from SettlementClass conditions
|
||
|
||
// Layer 2 outputs — founding orientation
|
||
geographically_triggered: bool, // false → FoundingOrientation::AdminFacing
|
||
|
||
// Layer 3 inputs/outputs — prosperity
|
||
prosperity_baseline: f32, // set at generation; founding_age modulated
|
||
prosperity_current: f32, // economics variable; tile conditions derived from this
|
||
|
||
// Layer 3 context
|
||
territorial_context: TerritorialStatus,
|
||
}
|
||
// Note: prosperity_delta = prosperity_current - prosperity_baseline; derived, never stored
|
||
// Note: block_irregularity lives on DistrictSkeleton (per-district), not here (per-city)
|
||
```
|
||
|
||
### DistrictSkeleton — New Field
|
||
|
||
Add to existing `DistrictSkeleton` in `generator.rs`:
|
||
|
||
```rust
|
||
// New field — derived at Layer 3 from founding_age_years + spatial_archetype
|
||
block_irregularity: BlockIrregularity,
|
||
```
|
||
|
||
### New Enum Types
|
||
|
||
```rust
|
||
enum SpatialArchetype {
|
||
CompanyTown, // Spine pattern
|
||
AdminCapital, // Radial pattern
|
||
FreePort, // Multi-node pattern (3–5 nodes)
|
||
Contested, // Dual-center overlay pattern
|
||
OrganicGrowth, // Irregular local density pattern
|
||
}
|
||
|
||
enum EconomicRole {
|
||
MiningExtraction,
|
||
Manufacturing,
|
||
ResearchHub,
|
||
CommercialHub,
|
||
Administrative,
|
||
TransitPort,
|
||
Energy,
|
||
Agricultural,
|
||
}
|
||
|
||
enum DistrictType {
|
||
Residential,
|
||
Commercial,
|
||
Industrial,
|
||
Administrative,
|
||
Entertainment,
|
||
Civic,
|
||
MixedUse,
|
||
Logistics, // NEW: warehousing, distribution, freight staging
|
||
}
|
||
|
||
enum MismatchSeverity {
|
||
None, // score >= 0.35
|
||
Soft, // 0.15 <= score < 0.35
|
||
Hard, // score < 0.15; overridden to Synthetic
|
||
}
|
||
```
|
||
|
||
### `GenerateChunkData` Upgrade (Prior Workshop Item, Still Required)
|
||
|
||
`GeneratorChunkData = Vec<bool>` must become `Vec<TileEntry>`. `TileEntry` needs at minimum:
|
||
|
||
```rust
|
||
struct TileEntry {
|
||
tile_type: TileType,
|
||
walkable: bool,
|
||
spawn_category: Option<PropCategory>, // scatter hook; None until Phase 6
|
||
// tile condition derived at render time from prosperity_current thresholds
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. SeedChain Usage Per Layer
|
||
|
||
All seeds: FNV-1a `child_seed(parent, discriminant)` per D-010.
|
||
|
||
```
|
||
system_seed = child_seed(world_seed, system_id)
|
||
body_seed = child_seed(system_seed, body_id)
|
||
|
||
// Layer 1
|
||
drainage_seed = child_seed(body_seed, "drainage")
|
||
attractor_seed = child_seed(body_seed, "attractors")
|
||
boundary_seed = child_seed(body_seed, "province_bounds")
|
||
|
||
// Layer 2 — iterate cities in atlas_city_names id order
|
||
placement_seed = child_seed(body_seed, "placement")
|
||
for city_index in sorted order:
|
||
city_seed = child_seed(placement_seed, city_index as u64)
|
||
// Used for: synthetic attractor offsets; tie-breaking in assignment
|
||
|
||
// Layer 3 — per city
|
||
district_seed = child_seed(city_seed, "districts")
|
||
founding_seed = child_seed(city_seed, "founding")
|
||
prosperity_seed = child_seed(city_seed, "prosperity")
|
||
|
||
// Layer 4 — per district
|
||
tile_seed = child_seed(district_seed, tile_index as u64)
|
||
// Tile LAYOUT is seed-locked (deterministic from tile_seed)
|
||
// Tile CONDITIONS are economics-variable via threshold-crossing cache invalidation
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Performance Budget Per Layer
|
||
|
||
Target: < 700ms at session start (synchronous cold generation); < 143ms on-demand.
|
||
|
||
| Layer | Operation | Budget | Estimated |
|
||
|-------|-----------|--------|-----------|
|
||
| L1 | D8 drainage routing | 50ms | ~50ms |
|
||
| L1 | Attractor extraction | 10ms | ~8ms |
|
||
| L1 | Province boundary extraction | 5ms | ~5ms |
|
||
| L2 | Hard-zero filter + score matrix | 5ms | ~3ms |
|
||
| L2 | Tier A greedy assignment | 2ms | ~1ms |
|
||
| L2 | Hungarian (Tier B/C, N ≤ 30) | 10ms | ~15ms |
|
||
| L2 | Synthetic overflow | 2ms | ~1ms |
|
||
| L2 | Name fulfillment Stages 1–3 | 5ms | ~5ms |
|
||
| L3 | District mix (all cities) | 15ms | ~20ms |
|
||
| L3 | TerritorialStatus (all provinces) | 25ms | ~25ms |
|
||
| L3 | WorldTier assignment + prosperity | 5ms | ~10ms |
|
||
| **Total** | | **134ms** | **~143ms** |
|
||
| Session budget (cold start) | 700ms | | |
|
||
| Headroom | | | 4.9× |
|
||
|
||
**Defensive check:** Hungarian is O(N³). At N = 30: ~15ms. At N = 50: ~400ms (exceeds L2 budget alone). If any body has > 40 named cities: switch Tier C to greedy for cities where best-available-attractor score > 0.5, then run Hungarian only on the remainder. This bound has not been hit on any current body but needs the guard.
|
||
|
||
---
|
||
|
||
## 7. Implementation Ticket Dependency Chain
|
||
|
||
Effort in dev-days. All `SCHEMA-*` tickets can be grouped into a single migration PR.
|
||
|
||
### Tier 0 — Prerequisites (No Dependencies)
|
||
|
||
| Ticket | Work | Effort | File(s) |
|
||
|--------|------|--------|---------|
|
||
| BUG-WorldTier | Fix `WorldTier` enum; update all match arms | 0.5d | `server/src/simulation/generator.rs` |
|
||
| SCHEMA-bodies | `body_radius_km REAL` column | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
|
||
| SCHEMA-heightmaps | `atlas_body_heightmaps` DDL | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
|
||
| SCHEMA-city-names | `atlas_city_names` DDL + index | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
|
||
| SCHEMA-feature-names | `atlas_feature_names` DDL + index | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
|
||
| SCHEMA-province-bounds | `atlas_province_boundaries` DDL + index | 0.25d | `systems-schema.sql`, `import_economics.py` MIGRATION_SQL |
|
||
|
||
### Tier 1 — Python Pipeline (Depends on Tier 0 schemas)
|
||
|
||
| Ticket | Work | Effort | Depends on |
|
||
|--------|------|--------|------------|
|
||
| PY-heightmap-import | `generate_atlas.py`: BLOB-pack elevation float32; INSERT into `atlas_body_heightmaps` | 1d | SCHEMA-heightmaps |
|
||
| PY-province-bounds | `generate_atlas.py`: watershed extraction from DEM; store boundary polylines | 2d | SCHEMA-province-bounds, PY-heightmap-import |
|
||
| PY-city-names-authored | `import_economics.py`: read `wiki/worlds/*.toml` `[[cities]]`; INSERT with `reserved = true` | 1d | SCHEMA-city-names |
|
||
| PY-city-names-corp | `import_economics.py`: generate corp-derived name rows for under-quota bodies | 1d | PY-city-names-authored |
|
||
| PY-body-radius | `import_economics.py`: populate `body_radius_km` from planet_class defaults or authored values | 0.5d | SCHEMA-bodies |
|
||
|
||
### Tier 2 — Rust Type Definitions (Depends on Tier 0)
|
||
|
||
| Ticket | Work | Effort | Depends on |
|
||
|--------|------|--------|------------|
|
||
| RS-types-worldtier | Fix `WorldTier` enum in Rust; update all match arms | 0.5d | BUG-WorldTier |
|
||
| RS-types-settlement | `SettlementClass`, `MismatchSeverity`, `AttractorAssignment`, `SyntheticPlacementReason` | 0.5d | RS-types-worldtier |
|
||
| RS-types-district | Add `BlockIrregularity` to `DistrictSkeleton`; `SpatialArchetype`, `EconomicRole`, `DistrictType::Logistics` | 0.5d | — |
|
||
| RS-types-territorial | `TerritorialStatus` with correct variants | 0.5d | — |
|
||
| RS-types-city-ctx | Finalize `CityGenerationContext` with all new fields | 1d | RS-types-settlement, RS-types-territorial |
|
||
|
||
### Tier 3 — Rust Core (Depends on Tier 1 + Tier 2)
|
||
|
||
| Ticket | Work | Effort | Depends on |
|
||
|--------|------|--------|------------|
|
||
| RS-heightmap-load | `load_heightmap()` via bytemuck; integration into `BodyWorldState` init | 0.5d | PY-heightmap-import, RS-types-city-ctx |
|
||
| RS-body-state | `BodyWorldState` struct + `GenerationCache` Bevy Resource + LRU cache | 1.5d | RS-heightmap-load |
|
||
| RS-drainage | D8 priority-flood drainage; attractor extraction; RiverNetwork construction | 3d | RS-body-state |
|
||
| RS-attractor-types | `GeographicAttractor`, `AttractorType`, `CompatibilityMatrix` | 0.5d | RS-types-settlement |
|
||
|
||
### Tier 4 — Generation Algorithms (Depends on Tier 3)
|
||
|
||
| Ticket | Work | Effort | Depends on |
|
||
|--------|------|--------|------------|
|
||
| RS-attractor-assign | Five-phase attractor assignment + Hungarian + synthetic overflow + mismatch flags | 4d | RS-drainage, RS-attractor-types, PY-city-names-authored |
|
||
| RS-district-mix | Three-component district mix + locked multiplier table + political archetype modifiers | 3d | RS-attractor-assign, RS-types-district |
|
||
| RS-territorial | TerritorialStatus priority-ordered derivation; ProvinceWorldState population | 2d | RS-attractor-assign |
|
||
| RS-block-irregularity | `BlockIrregularity::from_age_and_archetype()` + integration into district loop | 1d | RS-district-mix |
|
||
| RS-tile-conditions | Threshold cache with invalidation for tile conditions (L4-Q1) | 1.5d | RS-district-mix |
|
||
|
||
### Tier 5 — Background + UI (Depends on Tier 4)
|
||
|
||
| Ticket | Work | Effort | Depends on |
|
||
|--------|------|--------|------------|
|
||
| RS-bg-queue | `GenerationQueue` + rayon thread pool + priority ordering | 2d | RS-body-state |
|
||
| RS-aho-corasick | `SystemNameIndex` + text scanning → generation trigger | 1d | RS-bg-queue |
|
||
| UI-province-bounds | Godot planetary map: load `atlas_province_boundaries`; render as natural polylines | 2d | PY-province-bounds |
|
||
|
||
### Critical Path
|
||
|
||
```
|
||
SCHEMA-heightmaps
|
||
→ PY-heightmap-import
|
||
→ RS-heightmap-load
|
||
→ RS-body-state
|
||
→ RS-drainage
|
||
→ RS-attractor-assign
|
||
→ RS-district-mix
|
||
→ RS-tile-conditions
|
||
```
|
||
|
||
Critical path effort: 0.25 + 1 + 0.5 + 1.5 + 3 + 4 + 3 + 1.5 = **14.75 dev-days**
|
||
|
||
Total effort (all tiers, parallel where possible): **~33 dev-days**
|
||
|
||
Parallel acceleration: Tier 0 + Tier 1 + Tier 2 can all run simultaneously. On a two-agent split (Python team / Rust team), calendar time narrows to ~20 days.
|
||
|
||
---
|
||
|
||
## 8. Open Items Not Resolved in This Round
|
||
|
||
### Q4 — Port/Station as Special City Type
|
||
|
||
Not addressed. Orbital stations lack terrain; no geographic attractors; no drainage. Technical sketch: `SpatialArchetype::SpaceStation` as a sixth variant, bypassing Layer 1 entirely. `body_id` points to orbital body. District mix: no Agricultural; Logistics and Industrial dominant. Defer to lead for whether this needs a Round 4 or a separate ticket.
|
||
|
||
### Contested + OrganicGrowth Archetype Modifiers
|
||
|
||
My proposed modifiers (see Q6 above) are proposals, not locked. Lead or Burnelli-Sheldon should confirm or adjust before `RS-district-mix` is implemented.
|
||
|
||
### Province Watershed Algorithm Detail
|
||
|
||
I've specified that `generate_atlas.py` adds watershed extraction. The exact algorithm (D8 ridgeline detection, smoothing kernel, UV coordinate normalization) needs a concrete spec before `PY-province-bounds` is assigned. Recommend a brief technical sidequest with Tyre or a consultant review before that ticket starts.
|
||
|
||
---
|
||
|
||
## 9. Items Confirmed — No Further Discussion Required
|
||
|
||
Carried forward from Rounds 1–2, not reopened:
|
||
|
||
- **SeedChain (FNV-1a)** — D-010; unchanged
|
||
- **D8 drainage routing** — locked; ARCH-1 accepted by all agents
|
||
- **Scatter deferred** — lead decision; `spawn_category: Option<PropCategory>` hook preserved in `TileEntry`
|
||
- **District = 256m, Block = 64m** (4×4 blocks per district) — confirmed
|
||
- **Province = 1 regional grid cell (~540km×270km on reference body)** — confirmed
|
||
- **Tile condition thresholds** — `prosperity_current` > 0.63 = Intact, 0.43–0.63 = Worn, 0.23–0.43 = Cracked, < 0.23 = Broken; Paula's offsets prevent boundary oscillation
|
||
- **prosperity_delta = derived, never stored** — confirmed
|
||
- **Self-contained district generation** — no cross-city queries; lead requirement
|
||
- **L4-Q1: threshold-crossing cache invalidation** for tile conditions — ticket RS-tile-conditions
|
||
- **L4-Q4: pre-fetch two ring cells ahead** of player movement — confirmed; handled in chunk_streaming.rs after RS-body-state lands
|
||
- **Area = atlas layer, not navigation tier** — confirmed
|
||
- **Background generation budget** — 700ms session start; ~143ms on-demand; within budget at 4.9× headroom
|
||
|
||
---
|
||
|
||
*Round 3 complete from Tyre's side. Nine D-record candidates produced. Implementation dependency chain: 33 dev-days total, 14.75 critical path. WorldTier enum bug fix (BUG-WorldTier) is the hard prerequisite blocker — no generation ticket can land without it.*
|