Files
settled-reach/server/src/simulation/generator.rs
T
jpmschweitzerandClaude Fable 5 90655382eb feat(simulation): D-232 three-phase trait draw + deviation/swerve system (T-994, T-1003)
T-994 — three-phase draw replacing the flat splitmix64 flavor pick:
- Phase 1 body K-draw (trait_draw.rs): hard gates -> soft weights (two-part
  geographic_sector join, PR #148) -> SeedChain::for_body-seeded weighted
  draw of K (5/3/1/0 by ComplexityTier), pins count toward K, coverage-aware
  over a new body-level district-type-mix aggregate threaded into
  CityGenerationContext at L3->L4 dispatch.
- Phase 2 district-dominant pick keyed by the D-243 2048m District cell —
  settlements sharing a cell independently derive the identical template.
  Resolved at dispatch, never in FillChunk (T-987 stays pure).
- trait_catalog_reader.rs: systems.db reader for trait_templates +
  atlas_body_trait_bias (D-225 resource pattern), registered in main.rs
  with graceful degradation.
- BlockSkeleton carries district_type; assign_block_tags is a pure lookup.

T-1003 — deviation/swerve system (trait_swerve.rs):
- ArchitectureFlavorRef is now InVocabulary(u8) | Swerve(tag).
- Rare per-building wildcard: foreign-import pool (other corridors +
  cross_corridor) driven by Epicenter/Passage tier, mixed faction,
  road-graph degree; heritage-callback pool (own-corridor heritage) driven
  by isolation, remote tier, founding_age_years. Integer bps, base 100,
  cap 300 per driver — placeholder constants pending calibration.
- Sparsity escape hatch = same mechanism, necessity-triggered at the
  phase-2 pick (deterministic max-weight over the hard-gate-eligible pool).
- New SeedDomains: TraitVocabulary(13) / TraitDistrict(14) / TraitSwerve(15).

Governance: D-232 amended (district pinned to the D-243 2048m tier — the
D-222/D-243 redefinition was never captured; swerve driver mappings +
placeholder rates recorded), D-229 flavor_ref field updated to the enum
shape, D-235 amended (T-995 vocabulary ratification note; rides here due
to single-file entanglement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:16:47 +02:00

1589 lines
66 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! World generator data model — quarter skeleton and mobile chunk types.
//!
//! Phase 1 generator output: the `QuarterSkeleton` produced by the world generation
//! pipeline. Consumed by Phase 2 (chunk fill) and by the simulation startup path.
//!
//! **D-110 (signed z-levels):** All z-level *position* fields use `i8` (negative values
//! represent basements/sub-levels). Z-level *count* fields use `u8` (always ≥ 1).
//! The distinction:
//! - `base_z: i8` — where the bottom floor starts (can be negative)
//! - `z_levels: u8` — how many floors total (always positive)
//!
//! **D-108 (MobileChunk):** Vessel/vehicle interior chunks attached to mobile entities.
//! Uses the same chunk primitives as static districts in a simpler flat structure
//! (no Phase 1/Phase 2 split, template-stamped interiors).
//!
//! Sources: workshop-outcomes.md, tyre-round4.md
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
// ---------------------------------------------------------------------------
// Concept ID type aliases
// ---------------------------------------------------------------------------
/// Unique district identifier (content-addressable via hash of world position + seed).
pub type QuarterId = u64;
/// Unique multi-block reservation identifier within a district.
pub type ReservationId = u64;
/// Unique social site identifier within a district.
pub type SocialSiteId = u64;
/// Unique role slot identifier within a social site.
pub type RoleSlotId = u64;
// ---------------------------------------------------------------------------
// Stub types for concepts not yet implemented (defined here to allow
// the data model to compile; will be replaced with proper types when
// the respective systems are implemented)
// ---------------------------------------------------------------------------
/// Biome classification for wilderness districts. Stub — full taxonomy deferred.
pub type Biome = String;
/// Water body sub-classification. Stub — full taxonomy deferred.
pub type WaterType = String;
/// Specialized district function (e.g. military, medical, penal). Stub.
pub type SpecializedFunction = String;
/// Cultural / architectural era tag. Stub.
pub type Era = String;
/// Landmark slot description within a block. Stub.
pub type LandmarkSlot = String;
// `ChunkLayout`, `CorridorSpine`, `AccessPoint` are typed street-network structs
// (D-234, #957) — see the "Street-network types" section below.
/// District context — neighboring districts, world position, system. Stub.
pub type QuarterContext = String;
/// Zone definition — base palette + modifiers + zone name. Stub.
pub type ZoneDefinition = String;
/// District boundary system — edge descriptors with neighbors. Stub.
pub type QuarterBoundaries = String;
/// Guarantee audit result — tier-appropriate spatial invariant checks. Stub.
pub type GuaranteeAuditResult = String;
// ---------------------------------------------------------------------------
// Street-network types (D-234, #957)
// ---------------------------------------------------------------------------
/// Where a quarter's street network connects to the outside, to a reservation,
/// or to an interior junction (D-234). These are the nodes the arterial graph
/// spans and the guarantee audit (D-097) reads.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct AccessPoint {
/// Tile-space position within the 512-tile quarter (0–511).
pub position: (u16, u16),
pub kind: AccessKind,
}
/// Kind of [`AccessPoint`] (D-234).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum AccessKind {
/// Road enters the quarter from outside at a compass octant (0=N…7=NW),
/// from `CityGenerationContext.road_entry_directions`.
QuarterEdge { octant: u8 },
/// Gate into a multi-block reservation.
ReservationGate { reservation: ReservationId },
/// Internal junction between blocks.
BlockJunction,
}
/// One arterial spine — a ±45°-snapped edge of the quarter's trunk-road graph
/// (D-234 refined: arterials = least-cost graph over the access nodes). `from`/`to`
/// index into the quarter's `access_points`; `path` is the tile-space polyline
/// (segments axis- or 45°-aligned, D-096 ±45° cap). This node/edge graph is what
/// the guarantee audit (D-097) reads as the trunk topology.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct CorridorSpine {
pub from: u16,
pub to: u16,
pub path: Vec<(u16, u16)>,
}
/// Local street lattice descriptor for one block (D-234 refined: local streets =
/// a ±45° grid lattice within each block, modulated by the D-096 layout mode).
/// A compact parametric form the fill layer rasterizes — not the raster itself.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct ChunkLayout {
/// Street spacing in tiles (the block is divided into spacing-sized cells).
pub spacing: u8,
/// Lattice origin offset in tiles (organic jitter; `(0,0)` for grid).
pub offset: (u8, u8),
/// Rotation in 15° steps (0–3 ⇒ 0–45°, the D-096 cap; 0 for grid).
pub rotation_steps: u8,
}
/// Base visual palette for a zone. Stub.
pub type BasePalette = String;
/// Economic modifier on a zone palette. Stub.
pub type EconomicModifier = String;
/// Faction presence modifier on a zone palette. Stub.
pub type FactionModifier = String;
/// Condition modifier on a zone palette (worn, pristine, damaged). Stub.
pub type ConditionModifier = String;
/// Season modifier (affects palette and ambient conditions). Stub.
pub type Season = String;
/// Role slot within a social site template. Stub.
pub type RoleSlot = String;
/// Day phase (morning, afternoon, evening, night, late-night). Stub.
pub type DayPhase = String;
/// Triangle template reference (links to server/content/templates/). Stub.
pub type TriangleTemplate = String;
/// Raw chunk tile data for generator use (64×64 bool grid, true = walkable). Stub.
pub type GeneratorChunkData = Vec<bool>;
/// Tile type identifier (references visual tile set). Stub.
pub type TileId = String;
/// Entity identifier for mutation causality tracking. Stub.
pub type EntityId = u64;
/// Action identifier for player-caused mutations. Stub.
pub type ActionId = String;
/// Object instance identifier within a chunk. Stub.
pub type ObjectId = u64;
/// Placed object (item, furniture, fixture) within a chunk. Stub.
pub type PlacedObject = String;
// ---------------------------------------------------------------------------
// World and complexity tier enums
// ---------------------------------------------------------------------------
/// Network importance of a world in the galaxy.
/// Determines simulation fidelity budget and NPC complexity ceiling.
///
/// Source: D-218, workshop-outcomes.md
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum WorldTier {
/// Hub system. Full simulation, high faction pressure.
Epicenter,
/// Regional system. 1–4 districts, partial full-budget simulation.
Regional,
/// Small community. 1 district. Network-insignificant, NOT budget-capped.
Backwater,
/// Transit stop. Pass-through node. Moderate complexity ceiling.
Passage,
/// Not simulated until player approaches. Minimal complexity ceiling.
#[default]
Waypoint,
}
/// Generator content budget for a district.
/// Derived from WorldTier + SettingType at Phase 1.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum ComplexityTier {
/// Full social architecture. All Tier 1+2 guarantees. 20-80+ NPCs.
Full,
/// Moderate social architecture. Tier 1 + partial Tier 2. 8-20 NPCs.
Moderate,
/// Minimal social architecture. Tier 1 only. 1-8 NPCs.
Minimal,
/// No social architecture. Pure terrain. 0 NPCs. No guarantees.
#[default]
Empty,
}
/// Physical setting type for a district.
/// Merged from Gestalt's SettingGeometry and Tyre's TerrainType.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum SettingType {
Station,
#[default]
Urban,
Agricultural,
Maritime,
Wilderness {
biome: Biome,
},
Water {
water_type: WaterType,
},
Transitional,
Orbital,
Specialized {
function: SpecializedFunction,
},
}
/// Classification of a district (high-level function).
///
/// `Ord`/`PartialOrd` (T-994): lets callers dedup a body-wide coverage set into a
/// `BTreeSet`/`BTreeMap` key (D-010 determinism) — the D-232 trait-template draw's
/// `zone_affinity` lookup and the body-level district-type-mix coverage aggregate.
/// Declaration order is not a stability-pinned wire format (unlike `MorphologyZone`
/// or `SeedDomain`), so this is a safe additive derive.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum DistrictType {
LogisticsHub,
Residential,
Commercial,
Industrial,
Administrative,
Entertainment,
#[default]
MixedUse,
Transit,
Specialized,
}
/// How blocks are placed within the district's 512×512 footprint.
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub enum DistrictLayoutMode {
/// Standard Cartesian grid — perpendicular streets.
#[default]
Grid,
/// Organic placement with per-block offsets and rotations.
Organic {
placements: [[BlockPlacement; 4]; 4],
},
}
/// Zoning classification for a block or floor zone.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum ZoningType {
Commercial,
Residential,
Industrial,
Administrative,
Transit,
Recreational,
Restricted,
#[default]
Mixed,
}
/// Reservation function — what purpose a multi-block reservation serves.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ReservationFunction {
Skyscraper,
Park,
Terminal,
Plaza,
Monument,
IndustrialComplex,
MilitaryBase,
ResearchFacility,
UndergroundComplex,
}
/// Zone access tier — who is allowed into a spatial zone under normal circumstances.
///
/// Renamed from `AccessTier` to `ZoneAccessTier` to avoid collision with the
/// dialogue-layer `AccessTier` (D-028) in `simulation::line_pool`.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ZoneAccessTier {
/// Anyone can enter.
Public,
/// Requires employment or residence credential.
Credentialed,
/// Requires explicit invitation or escort.
Restricted,
/// Law enforcement / military only.
Secured,
/// Breach access only — normally sealed.
BreachOnly,
}
/// Vertical corridor type (how floors connect in a multi-level reservation).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum VerticalCorridorType {
Stairwell,
Elevator,
FreightLift,
AccessHatch,
EmergencyLadder,
}
/// Load state for a z-level within a reservation.
/// Controls lazy loading of floor data during play.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum ZLevelLoadState {
/// Floor is fully generated and in memory.
Loaded(GeneratorChunkData),
/// Floor skeleton only — full data not yet generated.
Skeleton(FloorZone),
/// Floor not yet generated (deferred to Phase 2 on first entry).
Ungenerated,
}
/// Cause of a chunk mutation (for save/load audit trail).
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum MutationCause {
Explosion { radius: u8, source: EntityId },
Fire { spread_from: Option<(u16, u16)> },
Construction { builder: EntityId },
Decay { time_since_maintenance: u32 },
PlayerAction { action: ActionId },
}
/// Type of structural change to a region of chunk tiles.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum StructuralChangeType {
WallDestroyed,
FloorCollapsed,
CeilingBreached,
AreaSealed,
WallConstructed,
}
/// Purpose classification for a triangle within a social site.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum TrianglePurpose {
Investigation,
Economic,
Social,
Political,
Tactical,
Mundane,
}
/// What exists on the far side of a wall tile (for destructible geometry).
/// Every wall tile in a generated chunk is tagged with one of these.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum WallBackside {
/// Another room/corridor exists (tiles already generated).
AdjacentSpace,
/// Solid structural material — 1-2 tiles of fill, then another wall.
StructuralFill,
/// Narrow utility gap (1-3 tiles): pipes, conduits, non-navigable.
ServiceVoid,
/// Edge of chunk — adjacent chunk's boundary tiles on the far side.
ChunkBoundary,
/// Faces outside (hull, exterior wall) — breach has catastrophic consequences.
Exterior,
}
/// Era cause — why a block has the era tag it has.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum EraCause {
Original,
CorporateMerger,
EmergencyExtension,
OrganicGrowth,
InstitutionalIncursion,
EconomicDisruption,
CulturalShift,
}
// ---------------------------------------------------------------------------
// Settlement classification enums (D-196, D-212, D-213, D-214, D-215)
// ---------------------------------------------------------------------------
/// How a settlement enters and exits active simulation.
/// Controls whether generation runs, and at what complexity level.
/// Source: D-196
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum SettlementClass {
/// Named in wiki; always active regardless of population threshold.
NameLocked,
/// Active if pop ≥ 50_000; ghost stub if pop < 5_000.
PopulationBudget,
/// Active only while the triggering economic condition holds.
EconomicTriggered,
/// Emergent settlement not in atlas at generation time; written during simulation.
OrganicGrowth,
}
/// Dominant power structure of a settlement and its physical spatial expression.
/// Derived from TerritorialStatus + economic_role at generation time.
/// Source: D-214
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub enum PoliticalArchetype {
/// Top-down Commission planning; rectilinear, institutional core, radial-core arrangement.
Commission,
/// Corp-dominated; commercial density, restricted campus blocks, restricted-perimeter adjacent.
Corporate,
/// Self-organized; organic growth, mixed use, ribbon arrangement.
Pioneer,
/// Garrison or fortification origin; defensible geometry, fortified-perimeter arrangement.
Military,
/// University or research origin; campus-quad structure, green space, radial-core arrangement.
Academic,
/// Factory-first; large-footprint industrial blocks, worker residential rings, ribbon arrangement.
Industrial,
}
/// Primary spatial axis of a city's original street grid.
/// Derived from the matched attractor type (D-211). Controls district grid rotation.
/// Source: D-213
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum FoundingOrientation {
/// Street grid perpendicular to coastline. `facing_degrees`: compass bearing toward water (0–359).
Coastal { facing_degrees: u16 },
/// Street grid parallel to founding river. `bearing_degrees`: river flow direction (0–359).
RiverAligned { bearing_degrees: u16 },
/// Grid rotated to follow local contours (valley floor settlements).
TerrainFollowing,
/// Grid aligned to cardinal N/S/E/W (Commission-planned settlements on flat terrain).
Cardinal,
/// Arbitrary bearing (pioneer settlements on open terrain). `bearing_degrees`: 0–359.
Free { bearing_degrees: u16 },
}
/// Who maintains an inter-settlement road/rail edge — readable in the road's
/// condition (Ozzie: "you can tell who controls territory by the state of the
/// roads"). Derived at Layer-2 road generation from the endpoint settlements'
/// political archetypes + the edge's haul length (no separate authoring).
/// Five-value vocabulary frozen by the cascade workshop (workshop-outcomes.md
/// "OQ-R3-4"); see [`crate::atlas::road_graph::maintenance_authority`].
/// Source: D-211 (Layer-2 transport), D-212 (territorial status input).
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MaintenanceAuthority {
/// Government-built/maintained — a Commission (or strategic Military) endpoint.
Administrative,
/// Corporation-maintained — a Corporate endpoint (e.g. road to a corp mine).
Corporate,
/// Locally maintained short links between neighbouring small settlements.
Communal,
/// Long-haul inter-regional routes between distant non-state settlements.
Trade,
/// Roads that exist but are no longer maintained (a Derelict-province
/// endpoint, or runtime abandonment). Not reachable from the current
/// authored data alone (D-212 Derelict is deferred) — wired for forward use.
Abandoned,
}
/// Territory control status for a province (drainage basin). Priority-ordered derivation.
/// Source: D-212 (amended 2026-06-05 — see `AutonomistHeld` + the dominant_faction
/// mapping note on the variant docs and in `attractor_matching::territorial_status_from_faction`).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum TerritorialStatus {
/// Established central governance — the Concord Assembly or its aligned
/// institutions (e.g. the Veil Institute). (Original D-212: Commission
/// faction_influence ≥ 0.6.)
CommissionControlled,
/// Commercial control — Syndic combines. (Original D-212: single corporation
/// faction_influence ≥ 0.5.)
CorpTerritory,
/// Two or more factions each ≥ 0.3; no dominant faction.
ContestedZone,
/// No faction with influence ≥ 0.2.
FrontierUnclaimed,
/// Self-governing autonomist bloc that rejects central (Assembly) authority —
/// the Compact of Westphalia and compact-sympathetic systems. Locally
/// governed, but outside the Assembly's reach (D-212 amendment 2026-06-05).
AutonomistHeld,
/// Cultural corridor has indigenous autonomy flag.
IndigenousHeld,
/// Population density < 0.01 AND no faction ≥ 0.1.
Derelict,
}
/// Spatial arrangement pattern governing district adjacency and landmark
/// placement within a settlement (D-215). Derived from `PoliticalArchetype`
/// (+ transit_hub economic role → `HubAndSpoke`). #956 derives and stores which
/// pattern applies; the block-adjacency *enforcement* is the Quarter-skeleton
/// generator's job (#957).
/// Source: D-215
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArrangementPattern {
/// Central landmark surrounded by mixed-use rings, transit spokes radiating
/// outward (Commission, Academic).
RadialCore,
/// Restricted campus block in the interior, commercial ring, peripheral
/// worker residential (Corporate).
CampusGrid,
/// Districts string along a linear feature; no dominant center (Pioneer,
/// Industrial).
RibbonDevelopment,
/// Restricted/secured districts at the footprint edge, open interior core,
/// single controlled access per edge (Military).
FortifiedPerimeter,
/// Transit district at center, all others reachable via direct corridors
/// (transit_hub economic role, any archetype).
HubAndSpoke,
}
// ---------------------------------------------------------------------------
// Attractor types for settlement placement (D-195, D-209, D-211)
// ---------------------------------------------------------------------------
/// The type of terrain feature that attracts settlement placement.
/// Source: D-195, D-209
// `#[repr(u8)]` with explicit discriminants: `AttractorType` is cast `as u8` as
// the primary sort key for the attractor list (`features.rs`), so the layout is
// load-bearing — reordering would change attractor ordering and the cascade
// golden. Append new variants; never renumber or reorder existing ones.
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum AttractorType {
/// Where a river meets sea level or coastline. Historically high-value.
RiverMouth = 0,
/// Proximity to coast without a river mouth. Port access.
CoastalAccess = 1,
/// Where a river crosses a topographic saddle or confluence point.
RiverCrossing = 2,
/// Local elevation minimum; flat, arable, sheltered.
ValleyFloor = 3,
/// Saddle point between adjacent drainage basins; controls a mountain pass.
PassEntrance = 4,
/// Adjacent to a lake polygon.
LakeShore = 5,
/// Flat terrain away from all other attractors; fallback for plains settlements.
PlainCenter = 6,
}
/// Fine-grained terrain classification carried by each `GeographicAttractor`.
/// Classifies the local terrain more finely than the top-level `SettingType`;
/// drives ZonePalette modifier selection (D-101) and `terrain_modification_cost`.
/// Source: D-210
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub enum SubBiomeVariant {
TropicalWet,
TemperateForest,
TemperateGrassland,
BorealForest,
Tundra,
Desert,
Savanna,
Alpine,
Wetland,
CoastalLowland,
/// No heightmap-derivable signal in the Layer-1 inputs (elevation, slope,
/// moisture, latitude); reserved for a future volcanic data source (D-210).
Volcanic,
}
/// A terrain feature at a specific map position that influences city placement scoring.
/// Source: D-195, D-209, D-210
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GeographicAttractor {
/// Pixel position in heightmap space [row, col].
pub position: (u16, u16),
pub attractor_type: AttractorType,
/// Strength on a 0–100 integer scale (100 = strongest). Quantized from flow
/// accumulation / habitability at Layer-1 output. Integer for D-010
/// determinism — every ranking/scoring decision is exact, no f32 (#955).
pub strength: i32,
/// Fine-grained terrain classification at this position (D-210).
pub sub_biome: SubBiomeVariant,
/// Infrastructure-build cost as a percent of baseline (100 = baseline
/// grassland; >100 = more expensive, e.g. 200 = 2× cost). Derived from
/// `sub_biome` + local slope; penalizes marginal cities in the matching
/// pipeline (D-211). Integer for D-010 determinism (#955). (D-210)
pub terrain_modification_cost: i32,
/// Compass bearing (degrees, quantized to 8 octants: 0=N,45=NE…315=NW)
/// toward the nearest water from this position, or 360 = "no water nearby".
/// Layer-1 terrain extraction feeding D-213 founding orientation (coastal
/// facing / river bearing) and the D-234 waterfront rule. Integer 8-octant
/// keeps it D-010-deterministic (no `atan2`).
pub water_bearing: u16,
}
/// Compatibility weights between economic roles and attractor types.
/// A 10×7 matrix (10 economic_role values × 7 AttractorType variants).
/// Each cell is an integer affinity 0–100 (100 = ideal terrain for that role,
/// ~50 = neutral, low = poor) used during attractor-matching scoring.
/// Integer for D-010 determinism (#955). Source: D-195
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CompatibilityMatrix {
/// Row order: manufacturing, financial, agricultural, extraction, service_mixed,
/// institutional, transit_hub, research, military, residential.
/// Column order: RiverMouth, CoastalAccess, RiverCrossing, ValleyFloor,
/// PassEntrance, LakeShore, PlainCenter.
pub weights: [[i32; 7]; 10],
}
impl CompatibilityMatrix {
/// The canonical D-195 authored compatibility table (#955).
///
/// Integer affinity 0–100: 100 = ideal terrain for the role, ~50 = neutral,
/// low (10–30) = poor fit (graduated, never a hard 0 — D-195). Re-authored
/// on the 0–100 scale from D-195's relative examples (the agricultural→
/// ValleyFloor 3.0 peak maps to the global max 100).
///
/// Rows: manufacturing, financial, agricultural, extraction, service_mixed,
/// institutional, transit_hub, research, military, residential.
/// Cols: RiverMouth, CoastalAccess, RiverCrossing, ValleyFloor,
/// PassEntrance, LakeShore, PlainCenter.
pub fn d195() -> Self {
CompatibilityMatrix {
weights: [
// RMth Coast RXing Vall Pass Lake Plain
[95, 80, 75, 60, 30, 35, 55], // manufacturing
[70, 90, 65, 25, 40, 45, 70], // financial
[85, 30, 55, 100, 10, 70, 90], // agricultural
[30, 35, 40, 65, 95, 30, 25], // extraction
[60, 65, 70, 50, 35, 50, 70], // service_mixed
[65, 65, 60, 45, 20, 55, 90], // institutional
[65, 75, 100, 35, 95, 25, 60], // transit_hub
[40, 70, 35, 75, 40, 95, 60], // research
[60, 75, 80, 55, 100, 25, 40], // military
[70, 85, 50, 65, 20, 85, 70], // residential
],
}
}
}
// ---------------------------------------------------------------------------
// D-229/D-230/D-231/D-232/D-233 shared types (economic-built-world workshop)
// ---------------------------------------------------------------------------
/// Physical access character of a building entrance.
///
/// D-229: Named `BuildingEntryClass`, NOT `AccessTier`, to avoid colliding with
/// the dialogue-layer `AccessTier` (D-028) in `simulation::line_pool`. Both
/// types share the `Public` and `BreachOnly` labels but have orthogonal semantics.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum BuildingEntryClass {
/// Openly accessible — no credential required.
Public,
/// Requires a commercial transaction or active business purpose.
Commercial,
/// Requires explicit invitation, resident status, or employment credential.
Restricted,
/// Normally sealed — only accessible via breach mechanics.
BreachOnly,
}
/// Construction era of a building block (D-229).
///
/// Derived from `founding_age_years + prosperity_baseline_bps + seed`.
/// Reads primarily as **age/wear** via the condition layer (D-217/D-198);
/// not a material-technology ladder (era = maintenance signal, not style signal).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ConstructionEra {
/// Built during the settlement's founding period.
Founding,
/// Built during the established growth phase.
Established,
/// Recent construction.
Modern,
/// Original structure; now degraded beyond economic viability.
Derelict,
}
/// Zone-type identifier — references a RON zone-type definition (D-229, D-142).
///
/// A `Box<str>` newtype matching the `id` field of one of the 31 D-142 zone-type
/// RON files. Using `Box<str>` (heap-intern) rather than `String` to discourage
/// mutation and signal that the value is a stable content identifier.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ZoneTypeId(pub Box<str>);
impl ZoneTypeId {
pub fn new(s: impl Into<Box<str>>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ZoneTypeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
/// Per-floor height specification for a building (D-229, resolves Q-104).
///
/// `Uniform(voxels_per_floor)` covers the common case (3 voxels ≈ 3 m/floor).
/// `Variable` carries per-floor memory only when floors actually differ
/// (e.g. ground-floor retail at 5 voxels, offices at 3 voxels each).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum FloorHeightProfile {
/// All floors have the same height in voxels. Default: `Uniform(3)`.
Uniform(u8),
/// Per-floor heights in voxels, index 0 = `base_floor`.
Variable(Vec<u8>),
}
impl Default for FloorHeightProfile {
fn default() -> Self {
// 3 voxels ≈ 3 m/floor (Jeroen confirmed default).
FloorHeightProfile::Uniform(3)
}
}
/// Floor/basement extent for a building, bridging D-110 floor-index addressing
/// and D-227 physical voxel-z (D-229, resolves Q-104).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct FloorExtent {
/// Index of the bottom floor (negative = basement, per D-110).
pub base_floor: i8,
/// Total number of floors, always ≥ 1.
pub floor_count: u8,
/// Per-floor height specification.
pub heights: FloorHeightProfile,
}
impl FloorExtent {
/// Number of floors above ground (base_floor ≥ 0 inclusive).
pub fn above_ground(&self) -> u8 {
if self.base_floor >= 0 {
self.floor_count
} else {
let basement_floors = (-self.base_floor) as u8;
self.floor_count.saturating_sub(basement_floors)
}
}
/// Map a building-relative voxel-z coordinate to the D-110 floor index that contains it.
///
/// Returns the floor index whose voxel span covers `voxel_z`, or `None` if `voxel_z`
/// falls outside the building's extent (below base floor or above the topmost floor).
///
/// `voxel_z` is building-relative: the base floor maps to z = 0. Callers working in
/// world space must subtract the building's world-base z offset before calling this method.
pub fn floor_at_voxel_z(&self, voxel_z: i32) -> Option<i8> {
let voxel_z_for_base = self.voxel_z_for_floor(self.base_floor)?;
if voxel_z < voxel_z_for_base {
return None;
}
let mut accum: i32 = voxel_z_for_base;
let top_floor = self.base_floor as i16 + self.floor_count as i16 - 1;
for f_offset in 0..self.floor_count {
let floor_idx = self.base_floor as i16 + f_offset as i16;
let h = self.floor_height_voxels(floor_idx as i8);
let next_accum = accum + h as i32;
if voxel_z >= accum && voxel_z < next_accum {
return Some(floor_idx as i8);
}
accum = next_accum;
if floor_idx >= top_floor {
break;
}
}
None
}
/// Map a D-110 floor index to the inclusive voxel-z range `(min_z, max_z)`.
///
/// Returns `None` if `floor_index` is outside the building's extent.
///
/// The returned range is building-relative: the base floor maps to z = 0. Callers working
/// in world space must add the building's world-base z offset to the returned values.
pub fn voxel_range_for_floor(&self, floor_index: i8) -> Option<(i32, i32)> {
let base_z = self.voxel_z_for_floor(self.base_floor)?;
let relative = floor_index as i16 - self.base_floor as i16;
if relative < 0 || relative >= self.floor_count as i16 {
return None;
}
let mut start: i32 = base_z;
for f_offset in 0..relative {
let fi = self.base_floor as i16 + f_offset;
start += self.floor_height_voxels(fi as i8) as i32;
}
let h = self.floor_height_voxels(floor_idx_for_offset(self.base_floor, relative)) as i32;
Some((start, start + h - 1))
}
// ── Private helpers ────────────────────────────────────────────────────
/// Convert a floor index to its cumulative voxel-z offset from the ground-floor
/// voxel origin (ground floor base = 0).
fn voxel_z_for_floor(&self, floor: i8) -> Option<i32> {
let relative = floor as i16 - self.base_floor as i16;
if relative < 0 || relative >= self.floor_count as i16 {
return None;
}
let mut z: i32 = 0;
for f_offset in 0..relative {
let fi = self.base_floor as i16 + f_offset;
z += self.floor_height_voxels(fi as i8) as i32;
}
Some(z)
}
fn floor_height_voxels(&self, floor_index: i8) -> u8 {
match &self.heights {
FloorHeightProfile::Uniform(h) => *h,
FloorHeightProfile::Variable(v) => {
let offset = (floor_index as i16 - self.base_floor as i16) as usize;
v.get(offset).copied().unwrap_or(3)
}
}
}
}
fn floor_idx_for_offset(base: i8, offset: i16) -> i8 {
(base as i16 + offset) as i8
}
/// Axis-aligned rectangle in integer tile space (D-229, D-010 integer-only).
///
/// Represents a building footprint within a 128-tile block.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct TileRect {
/// Origin corner (tile-space x, y within the block, 0-based).
pub origin: (u8, u8),
/// Width and height in tiles (always ≥ 1).
pub size: (u8, u8),
}
impl TileRect {
pub fn new(origin_x: u8, origin_y: u8, width: u8, height: u8) -> Self {
Self {
origin: (origin_x, origin_y),
size: (width, height),
}
}
/// Returns `true` if the point `(x, y)` is inside this rect (inclusive).
pub fn contains(&self, x: u8, y: u8) -> bool {
x >= self.origin.0
&& x < self.origin.0.saturating_add(self.size.0)
&& y >= self.origin.1
&& y < self.origin.1.saturating_add(self.size.1)
}
}
/// Architecture flavor reference into the body's trait-template draw (D-229, D-232).
///
/// Records which template the generator selected at skeleton time for Phase-6 to
/// read cold. The selection mechanism is D-232's three-phase draw (T-994): a body
/// vocabulary K-draw, then a per-district dominant-template pick by `zone_affinity`,
/// then within-template seed picks (not represented here — that's the D-235 visual
/// bundle resolution). The reference is frozen-amber once written.
///
/// Shaped as an enum (rather than a bare index) so the closed-vocabulary case and
/// the future out-of-vocabulary swerve (T-1003, D-232's deviation system) share one
/// wire type without a breaking change when the swerve lands — `T-994` introduces
/// the shape only; swerve *logic* (foreign-import / heritage-callback draws) is not
/// implemented here.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ArchitectureFlavorRef {
/// Index into `CityGenerationContext::trait_selection` (0-based) — the body's
/// closed K-template vocabulary (D-232 phase 1).
InVocabulary(u8),
/// A template tag drawn from *outside* the body's closed vocabulary (T-1003,
/// D-232 deviation system): the rare per-building foreign-import /
/// heritage-callback wildcard (`trait_swerve::roll_building_swerve`), or the
/// sparsity escape hatch at district-dominant time
/// (`trait_swerve::necessity_swerve`). The passive past-vogue holdover is NOT
/// a swerve — it rides the D-217 wear/era condition layer (`era_cause`).
Swerve(String),
}
/// A single building footprint tag — the frozen step-3 output placed on every
/// building footprint at plan time (D-229).
///
/// Written once inside `GenerateSkeleton`; read-only thereafter by FillChunk
/// (D-230), the guarantee audit (D-097), and the Phase-6 interior generator (D-231).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct BuildingPropertyTag {
/// What the building is — matches a D-142 RON zone-type `id`.
pub zone_type_id: ZoneTypeId,
/// Axis-aligned footprint within the 128-tile block.
pub footprint: TileRect,
/// Floor/basement extent (D-110 ↔ D-227 bridge, resolves Q-104).
pub extent: FloorExtent,
/// Physical access character (D-229).
pub entry_class: BuildingEntryClass,
/// Architecture flavor index into the body's trait-template draw (D-232).
pub flavor_ref: ArchitectureFlavorRef,
/// Construction era tag (D-229).
pub era: ConstructionEra,
/// Cause of this block's era classification.
pub era_cause: EraCause,
/// Frozen-amber condition snapshot from `prosperity_baseline_bps` (D-197/D-217).
/// The rolling condition overlay (D-198) paints over this; never mutates the tag.
pub initial_condition: crate::atlas::tile_condition::TileCondition,
/// Doors into / out of this building (D-231). At least one `Main` door.
///
/// Using `Vec<DoorSpec>` rather than `SmallVec<[DoorSpec; 4]>` for now;
/// the vast majority of buildings have 1–4 doors and the heap allocation
/// matches the D-231 intent. SmallVec upgrade tracked as a future
/// micro-optimisation once the fill layer is profiled.
// TODO: SmallVec<[DoorSpec; 4]> per D-231 once the door subsystem is hot.
pub doors: Vec<DoorSpec>,
}
/// Cardinal compass direction (used by DoorSpec.facing, D-231).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum CardinalDirection {
North,
East,
South,
West,
}
/// Door class within a building (D-231).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum DoorClass {
/// Primary public / commercial entrance — at least one per building.
Main,
/// Service or logistics entrance.
Service,
/// Emergency egress (required when `above_ground ≥ 2`, D-231).
Emergency,
/// Hidden entrance (D-106 rooftop/hidden discovery layer).
Hidden,
}
/// Initial state of a door (D-231).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum DoorInitialState {
Open,
Closed,
Locked,
/// Permanently sealed — breach mechanics only.
Sealed,
}
/// Door credential requirement (D-231).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum DoorCredential {
None,
/// Door is only accessible during specified hours (0–23, inclusive range).
TemporalWindow {
open_hour: u8,
close_hour: u8,
},
/// Requires an employment credential from this corporation.
Corporate {
corp_id: String,
},
/// Requires a residence credential in this block.
Resident {
block_id: String,
},
/// Law-enforcement / government authority only.
Authority,
/// Social trust score threshold (basis points; D-010 integer, 10 000 = 1.0).
Social {
trust_threshold_bps: u32,
},
}
/// What a door connects to on the other side (D-231).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum DoorConnectsTo {
/// Opens onto a named street.
Street { street_id: String },
/// Opens into an adjacent building in the same block.
AdjacentBuilding { block_pos: (u8, u8) },
/// Opens into interstitial space (courtyard, alley, gap).
Interstitial,
}
/// The complete Phase-6 seed for an interior (D-231).
///
/// A future interior generator produces a deterministic floor plan from this
/// descriptor + `SeedChain` with **no other system queried**.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct InteriorDescriptor {
pub zone_type_id: ZoneTypeId,
pub entry_class: BuildingEntryClass,
pub floor_extent: FloorExtent,
pub era: ConstructionEra,
pub flavor_ref: ArchitectureFlavorRef,
/// Prosperity baseline (0–10 000 basis points; D-010 integer-only).
pub prosperity_bps: u32,
pub layout_mode: LayoutMode,
}
/// Layout mode for interior generation (D-231, D-096).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum LayoutMode {
/// Grid-planned interior — rectilinear rooms.
Grid,
/// Organic interior — irregular room shapes with offsets.
Organic,
}
/// A single door on a building — the step3→step4 boundary (D-231).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct DoorSpec {
/// Which face of the building footprint this door is on.
pub facing: CardinalDirection,
pub door_class: DoorClass,
pub entry_class: BuildingEntryClass,
pub initial_state: DoorInitialState,
pub credential: DoorCredential,
pub connects_to: DoorConnectsTo,
/// The complete Phase-6 interior seed (D-231).
pub interior_descriptor: InteriorDescriptor,
}
/// Region-level morphology zone — D-239 §6 frozen 17-zone vocabulary (T-1027).
///
/// **FREEZE POINT**: this enum is the canonical freeze per D-239 §6.
/// Adding, renaming, or 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.
///
/// Zones ≠ families. Four zones — TidalFlat, Estuarine, Alpine, Wetland —
/// are derived sub-classifications (family + elevation/water-height), not
/// distinct generator families.
///
/// Shared by all tiles in a region; constrains street geometry (D-234)
/// and acts as a soft weight on cultural-template eligibility (D-232).
/// Carried on `CityGenerationContext` (D-233 amend to D-199).
///
/// Integer-discriminant, D-010 compliant. `repr(u8)` pins values for
/// serialisation stability; append-only.
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum MorphologyZone {
/// Deep-water ocean — hub-and-spoke; perimeter access priority.
OpenOcean = 0,
/// Lake body — hub-and-spoke; perimeter access toward water.
Lake = 1,
/// Tidal flat (sub-classification): low coastal plain exposed at low tide.
/// Derived from CliffCoast/AlluvialPlain family + tidal signal + low elevation.
TidalFlat = 2,
/// Dune strand — ribbon/hub-and-spoke; wind-aligned lot orientation.
DuneStrand = 3,
/// Cliff coast — ribbon only; vertical face at water edge.
CliffCoast = 4,
/// Fjord — ribbon/hub-and-spoke; pier geometry; requires GlaciationGrade ≥ 2.
Fjord = 5,
/// River delta / braided channel — hub-and-spoke following channels; bridges as forced nodes.
Delta = 6,
/// Estuarine (sub-classification): brackish tidal zone at river mouth.
/// Derived from Delta/AlluvialPlain family + tidal signal + low elevation.
Estuarine = 7,
/// Flat alluvial plain — any pattern; primary default for plains settlements.
#[default]
AlluvialPlain = 8,
/// River bank — gentle approach; pier geometry on water-facing edges.
RiverBank = 9,
/// Meandering river reach — any pattern.
MeanderReach = 10,
/// Braided channel plain — hub-and-spoke; gravel/sand substrate.
BraidedPlain = 11,
/// Valley floor — ribbon or mesh; enclosed by flanking slopes.
ValleyFloor = 12,
/// Mountain pass terrain — ribbon only; elevation steps as block boundaries.
MountainPass = 13,
/// Alpine (sub-classification): above treeline high-altitude zone.
/// Derived from IncisedGorge/ValleyFloor family + high elevation.
Alpine = 14,
/// Volcanic — lava field / shield slope; requires TectonicClass::Volcanic.
Volcanic = 15,
/// Wetland (sub-classification): low-gradient, saturated or seasonally flooded.
/// Derived from AlluvialPlain/Delta family + low slope + high moisture.
Wetland = 16,
}
/// Built-form archetype derived from a settlement's dominant commodity (D-233).
///
/// 5 archetypes projected from the 8 cargo-type classifications in `commodities.toml`;
/// drives building-vocabulary pool selection and roofed-coverage fraction.
/// The 8 cargo types stay on the economics model — no fidelity lost.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum BulkClass {
/// Solid bulk commodity (ore, aggregate, grain). Coverage: 0.25–0.40.
/// Vocabulary: mine_head / conveyor_run / tailings_area.
BulkSolid,
/// Liquid bulk commodity (fuel, chemical, water). Coverage: 0.20–0.35.
/// Vocabulary: tank_farm / flare_stack / pump_station.
BulkLiquid,
/// Precision-dense, high-value goods (electronics, manufactured parts).
/// Coverage: 0.75–0.90. Vocabulary: cleanroom_facility / qc_lab / assembly_bay.
PrecisionDense,
/// Perishable goods (food, pharma, biologics). Coverage: 0.50–0.65.
/// Vocabulary: field_shed / cold_store / processing_plant.
Perishable,
/// Non-physical services / information economy. Coverage: 0.85–0.95.
/// Vocabulary: office_tower / civic_hall / data_centre.
NonPhysical,
}
/// Spatial concentration of a settlement's dominant production (D-233).
///
/// Controls how zone-type blocks are spread across the district grid —
/// spread vs cluster — not per-block weighting.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ProductionUbiquity {
/// Production scattered through mixed-use (e.g. ubiquitous water infrastructure).
Ubiquitous,
/// Present in most districts but not dominant (e.g. common agriculture).
Common,
/// Concentrated in a few specialist districts.
Specialist,
/// Contiguous block groups dominate the settlement (the mine IS the city).
MonopolySource,
}
/// Quarter-level world state produced by the plan phase (D-230).
///
/// Output of `GenerateSkeleton` extended to include building-property tags.
/// Stored in `BodyWorldState.quarters: BTreeMap<QuarterId, QuarterWorldState>`.
/// `BTreeMap` for D-010 determinism.
///
/// D-230: `{ skeleton: QuarterSkeleton, block_tags: BTreeMap<(u8,u8), Vec<BuildingPropertyTag>> }`
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub struct QuarterWorldState {
/// Phase 1 skeleton produced by `GenerateSkeleton` (D-230).
pub skeleton: QuarterSkeleton,
/// Building property tags keyed by block position (row, col) within the 4×4 grid.
/// Each entry is a Vec of one tag per building footprint placed in that block.
/// `BTreeMap` for D-010 determinism (no HashMap non-determinism).
pub block_tags: BTreeMap<(u8, u8), Vec<BuildingPropertyTag>>,
}
/// Data contract between build-time (systems.db) and the runtime-background
/// generation tier. Populated from atlas_city_names + bodies at generation
/// dispatch time. All 8 fields are required before a generation task may run.
/// Source: D-200, D-199
///
/// Amended by D-229/D-232/D-233: adds `morphology_zone`, `trait_selection`,
/// `dominant_bulk_class`, `dominant_production_ubiquity`.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct CityGenerationContext {
/// Foreign key into atlas_city_names.id
pub city_id: u64,
pub political_archetype: PoliticalArchetype,
/// Starting economic health seed in basis points (0–10_000; 10_000 = 1.0).
/// Derived per D-197. Integer to avoid f32 non-determinism (D-010).
pub prosperity_baseline_bps: u32,
pub surrounding_biome: SettingType,
/// Compass octants (0=N, 1=NE … 7=NW) where roads enter the city footprint.
pub road_entry_directions: Vec<u8>,
/// City footprint radius in km. Derived from body_radius_km (D-204) + population.
pub footprint_radius_km: f32,
pub founding_orientation: FoundingOrientation,
pub world_tier: WorldTier,
// ── D-233/D-232 additions ─────────────────────────────────────────────
/// Region morphology zone (D-228). Constrains street geometry (D-234) and
/// acts as a soft weight on cultural-template eligibility (D-232).
pub morphology_zone: MorphologyZone,
/// Body vocabulary draw result — K trait-template tags selected at skeleton
/// time (D-232). K is locked to `complexity_tier`: Full=5, Moderate=3,
/// Minimal=1, Empty=0. NOT named `flavor_profile` (that was the round-2 name).
/// Populated by the T-994 phase-1 K-draw; empty when the trait catalog reader
/// is unavailable or `complexity_tier == Empty` (K=0).
pub trait_selection: Vec<String>,
/// Dominant `BulkClass` for this settlement's primary commodity (D-233).
pub dominant_bulk_class: BulkClass,
/// Spatial concentration of dominant production (D-233).
pub dominant_production_ubiquity: ProductionUbiquity,
// ── T-994 additions (D-232 three-phase draw) ───────────────────────────
/// This system's corridor (`star_systems.geographic_sector` —
/// core/north_reach/south_reach/west_reach/east_reach/deep_frontier; `None`
/// if unset). A SOFT weight on the phase-1 body-vocabulary draw only — composes
/// with a template's own `geographic_sector` pool-narrowing column AND its
/// `weight_mods.geographic_sector` map (PR #148 review note: a two-part join,
/// never a hard gate — D-232 "corridors are tendencies, not borders").
pub geographic_sector: Option<String>,
/// Body-level district-type coverage: every `DistrictType` present anywhere
/// among this body's settlements, deduped (T-994). Threaded from the L3→L4
/// dispatch aggregation so the phase-1 K-draw is coverage-aware up front
/// (≥1 eligible template per district type actually present on the body).
pub body_district_type_mix: Vec<DistrictType>,
/// The D-243 2 048 m District cell (`atlas::scale::DistrictPos`) this
/// settlement's quarter falls in (T-994). Two settlements sharing a
/// `settlement_district_pos` independently derive the identical phase-2
/// dominant template for a given `DistrictType` — same body seed + same key,
/// so a coherent 2 048 m area reads as one style with no cross-quarter
/// coordination required.
pub settlement_district_pos: (i32, i32),
/// Phase-2 dominant-template pick (D-232), pre-resolved at L3→L4 dispatch time
/// (T-994) — **not** at `FillChunk` (T-987 keeps fill pure/cache-free) and not
/// even inside the `GenerateSkeleton` Rayon task itself, since the inputs
/// (`trait_selection` + the catalog's `zone_affinity`) are already known at
/// dispatch. One entry per `DistrictType` (all 9, so `assign_block_tags` is a
/// cheap infallible lookup). Usually `InVocabulary` (an index into
/// `trait_selection`); `Swerve` when the sparsity escape hatch fired (T-1003).
/// `BTreeMap` for D-010 determinism.
pub district_dominant_by_type: BTreeMap<DistrictType, ArchitectureFlavorRef>,
// ── T-1003 additions (D-232 deviation/swerve system) ────────────────────
/// Per-building wildcard chances (bps of 10 000): `(foreign_bps,
/// heritage_bps)`, resolved once per settlement at dispatch time from the
/// driver inputs (`trait_swerve::compute_swerve_rates`). Both zero when no
/// catalog reader is wired.
pub swerve_rates_bps: (u32, u32),
/// Foreign-import swerve candidates — hard-gate-eligible templates outside
/// the body vocabulary, from another corridor's grammar or the shared
/// `cross_corridor` pool: `(tag, weight_bps)` (`trait_swerve` module).
pub swerve_foreign_pool: Vec<(String, u32)>,
/// Heritage-callback swerve candidates — the body's own corridor heritage
/// sub-pool: `(tag, weight_bps)`.
pub swerve_heritage_pool: Vec<(String, u32)>,
}
// ---------------------------------------------------------------------------
// Supporting structs
// ---------------------------------------------------------------------------
/// Organic layout placement for a single block.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct BlockPlacement {
/// Offset from grid-aligned position (±16 subtiles per axis max).
pub offset: (i16, i16),
/// Rotation in 15° increments (0–3, max 45°).
pub rotation_steps: u8,
/// Street width in basis points (10000 = 1.0×, range 7500–20000 for 0.75–2.0×).
/// Default 10000 = 4 visual tiles. Uses integer to avoid f32 non-determinism (D-010).
pub street_width_bps: u16,
}
/// Single block within a quarter's 4×4 block grid.
/// Each block = 128×128 subtiles = 2×2 chunks.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub struct BlockSkeleton {
/// Grid position (0–3, 0–3).
pub position: (u8, u8),
pub zoning: ZoningType,
/// The D-194 district-mix `DistrictType` this block was assigned (T-994).
/// `zoning` is the *function* derived from it (`zoning_for_district`); this
/// field keeps the source classification around because the D-232 phase-2
/// dominant-template pick keys `zone_affinity` by `DistrictType`, not
/// `ZoningType` — dropping it here would make the pick unrecoverable.
pub district_type: DistrictType,
/// Which multi-block reservation this block belongs to (if any).
pub reservation: Option<ReservationId>,
pub chunk_layout: ChunkLayout,
pub hosted_sites: Vec<SocialSiteId>,
// Per-block `era: Era` / `era_modifications` String stubs removed (#957, T-984) —
// construction era now lives per-footprint on `BuildingPropertyTag.era` (typed
// `ConstructionEra`); `era_cause` is retained as the block-level cause.
pub era_cause: Option<EraCause>,
/// Build density percentage (0 = open/empty, 100 = fully built-up).
/// Integer to avoid f32 non-determinism (D-010).
pub density_pct: u8,
pub landmark: Option<LandmarkSlot>,
}
/// Floor zone within a multi-level reservation.
///
/// **D-110:** `z_level` is `i8` — negative values represent basements
/// (e.g. `z_level = -1` for a sub-basement). This differs from `z_levels: u8`
/// on the parent reservation which counts total floors (always ≥ 1).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct FloorZone {
/// Absolute z-level of this floor. Negative for sub-ground levels (D-110).
pub z_level: i8,
pub zone_type: ZoningType,
pub zone_palette: ZonePalette,
pub access_tier: ZoneAccessTier,
}
/// Vertical connection spec within a multi-level reservation.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct VerticalCorridorSpec {
/// Which blocks this vertical corridor passes through.
pub block_coords: Vec<(u8, u8)>,
/// Which z-bands (0-indexed band indices, not absolute z-levels) this
/// corridor connects. See D-110 for signed z-level coordinate system.
pub z_bands_connected: Vec<u8>,
pub access_tier: ZoneAccessTier,
pub corridor_type: VerticalCorridorType,
}
/// Visual palette for a zone — base material + contextual modifiers.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct ZonePalette {
pub base: BasePalette,
pub modifiers: Vec<PaletteModifier>,
}
/// Contextual modifier applied on top of a base palette.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum PaletteModifier {
EconomicFunction(EconomicModifier),
Era(Era),
FactionPresence(FactionModifier),
Condition(ConditionModifier),
Season(Season),
}
/// Social site placement within a district.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct SocialSitePlacement {
pub site_id: SocialSiteId,
/// Which blocks this site spans.
pub blocks: Vec<(u8, u8)>,
pub template_tag: String,
pub access_tier: ZoneAccessTier,
pub triangles: Vec<TriangleAssignment>,
pub role_slots: Vec<RoleSlot>,
pub active_phases: Vec<DayPhase>,
}
/// Triangle assignment within a social site.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct TriangleAssignment {
pub template: TriangleTemplate,
pub purposes: Vec<TrianglePurpose>,
pub participants: Vec<RoleSlotId>,
/// Which block provides the primary staging area (optional for multi-block sites).
pub staging_block: Option<(u8, u8)>,
}
/// Mutations applied to an already-generated chunk.
/// Stored alongside the chunk in the save file.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct ChunkMutations {
pub tile_overrides: Vec<TileOverride>,
pub structural_changes: Vec<StructuralChange>,
pub placed_objects: Vec<PlacedObject>,
pub removed_objects: Vec<ObjectId>,
}
/// Single tile override within a chunk mutation.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TileOverride {
/// (x, y, z) within the chunk (z is relative to chunk base, always ≥ 0).
pub position: (u16, u16, u8),
pub new_tile: TileId,
pub cause: MutationCause,
}
/// Rectangular structural change within a chunk.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct StructuralChange {
pub min: (u16, u16, u8),
pub max: (u16, u16, u8),
pub change_type: StructuralChangeType,
}
// ---------------------------------------------------------------------------
// Multi-block reservation
// ---------------------------------------------------------------------------
/// A reservation spanning multiple blocks within a district.
///
/// Used for skyscrapers, parks, transit terminals, plazas, and any structure
/// that requires more than one 128×128 block.
///
/// **D-110:** `base_z: i8` — the lowest floor of this reservation.
/// Negative values represent sub-ground levels (basements, underground complexes).
/// `z_levels: u8` remains unsigned — it counts total floors (always ≥ 1).
///
/// Example: a building with a 3-level basement at ground + 20 floors above:
/// `base_z: -3, z_levels: 24`
/// A deep mine shaft descending 30 floors below ground:
/// `base_z: -30, z_levels: 30`
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct MultiBlockReservation {
/// Which blocks (grid positions) are part of this reservation.
pub blocks: Vec<(u8, u8)>,
pub template_tag: String,
pub function: ReservationFunction,
/// Total number of floors (always ≥ 1, unsigned). D-110.
pub z_levels: u8,
/// Lowest floor's z-level (can be negative for sub-basement). D-110.
pub base_z: i8,
/// Per-floor zone specifications.
pub floor_zones: Vec<FloorZone>,
/// Number of z-bands (logical groupings of floors by zoning).
pub z_band_count: u8,
/// Zone definitions for each z-band.
pub z_band_zones: Vec<ZoneDefinition>,
/// Vertical connection specs (stairs, elevators, ladders).
pub vertical_corridors: Vec<VerticalCorridorSpec>,
pub hosted_sites: Vec<SocialSiteId>,
}
// ---------------------------------------------------------------------------
// Quarter skeleton
// ---------------------------------------------------------------------------
/// Phase 1 generator output for one quarter.
///
/// Produced by the async background generation pipeline.
/// Consumed by Phase 2 (chunk-by-chunk fill, on demand) and by
/// the production simulation startup path.
///
/// ## Z-level naming convention (D-110)
///
/// - `z_levels: u8` — how many floor levels exist in this quarter (count, always ≥ 1)
/// - `base_z` does not appear on `QuarterSkeleton` itself — the quarter's
/// ground level is always 0. Sub-ground structures use [`MultiBlockReservation`]
/// with a negative `base_z` field.
///
/// ## Determinism (D-010)
///
/// All `Vec<_>` fields are stable-ordered at generation time (sorted by a
/// deterministic key). Generation reproduces identical output for the same seed.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub struct QuarterSkeleton {
// ── Identity ──────────────────────────────────────────
pub quarter_id: QuarterId,
/// Deterministic seed (derived from master seed via SeedChain).
pub seed: u64,
/// The quarter's primary functional zone (still a `DistrictType` — the
/// zoning vocabulary is orthogonal to the spatial tier, D-222).
pub quarter_type: DistrictType,
/// World context (system, world, neighboring districts).
pub context: QuarterContext,
// ── Classification ────────────────────────────────────
pub world_tier: WorldTier,
pub complexity: ComplexityTier,
pub setting: SettingType,
pub layout_mode: DistrictLayoutMode,
// ── Spatial Structure ─────────────────────────────────
/// The 4×4 block grid (each block = 128×128 subtiles = 2×2 chunks).
pub blocks: [[BlockSkeleton; 4]; 4],
/// Multi-block reservations (skyscrapers, parks, terminals, plazas).
pub reservations: Vec<MultiBlockReservation>,
/// Corridor spines connecting key access points.
pub corridors: Vec<CorridorSpine>,
/// Number of floor levels in this district (count, unsigned — D-110).
pub z_levels: u8,
// ── Social Structure ──────────────────────────────────
pub social_sites: Vec<SocialSitePlacement>,
pub access_points: Vec<AccessPoint>,
// Cultural / zone String stubs (`society_profile`, `zone_palette`) removed
// (T-984, D-229) — per-building culture and zoning now live on
// `BuildingPropertyTag` (`flavor_ref`, `zone_type_id`) in the district's `block_tags`.
// ── Boundary System ───────────────────────────────────
pub boundaries: QuarterBoundaries,
// ── Validation ────────────────────────────────────────
/// Guarantee audit result. `None` for Empty districts.
pub guarantee_audit: Option<GuaranteeAuditResult>,
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// ── FloorExtent: floor_at_voxel_z and voxel_range_for_floor ─────────────
/// (a) Uniform building at ground level — round-trip voxel↔floor.
///
/// Uniform(3), base_floor=0, floor_count=2 → floors 0,1; heights 3 each.
/// Floor 0 occupies voxels 0–2; floor 1 occupies voxels 3–5.
#[test]
fn floor_extent_uniform_ground_round_trip() {
let extent = FloorExtent {
base_floor: 0,
floor_count: 2,
heights: FloorHeightProfile::Uniform(3),
};
// Range for each floor.
assert_eq!(extent.voxel_range_for_floor(0), Some((0, 2)));
assert_eq!(extent.voxel_range_for_floor(1), Some((3, 5)));
// Round-trip: voxel → floor → range contains voxel.
assert_eq!(extent.floor_at_voxel_z(0), Some(0));
assert_eq!(extent.floor_at_voxel_z(2), Some(0));
assert_eq!(extent.floor_at_voxel_z(3), Some(1));
assert_eq!(extent.floor_at_voxel_z(5), Some(1));
}
/// (b) Basement building — base_floor=-1, floor_count=3 (floors -1, 0, 1).
///
/// Uniform(3): floor -1 → voxels 0–2, floor 0 → 3–5, floor 1 → 6–8.
#[test]
fn floor_extent_basement_building() {
let extent = FloorExtent {
base_floor: -1,
floor_count: 3,
heights: FloorHeightProfile::Uniform(3),
};
assert_eq!(extent.voxel_range_for_floor(-1), Some((0, 2)));
assert_eq!(extent.voxel_range_for_floor(0), Some((3, 5)));
assert_eq!(extent.voxel_range_for_floor(1), Some((6, 8)));
assert_eq!(extent.floor_at_voxel_z(0), Some(-1));
assert_eq!(extent.floor_at_voxel_z(3), Some(0));
assert_eq!(extent.floor_at_voxel_z(6), Some(1));
assert_eq!(extent.floor_at_voxel_z(8), Some(1));
}
/// (c) Variable([5,3,3]) floor heights — confirm each floor's voxel range.
///
/// base_floor=0, floor_count=3: floor 0 → 5 voxels (0–4), floor 1 → 3 voxels (5–7),
/// floor 2 → 3 voxels (8–10).
#[test]
fn floor_extent_variable_heights() {
let extent = FloorExtent {
base_floor: 0,
floor_count: 3,
heights: FloorHeightProfile::Variable(vec![5, 3, 3]),
};
assert_eq!(extent.voxel_range_for_floor(0), Some((0, 4)));
assert_eq!(extent.voxel_range_for_floor(1), Some((5, 7)));
assert_eq!(extent.voxel_range_for_floor(2), Some((8, 10)));
assert_eq!(extent.floor_at_voxel_z(0), Some(0));
assert_eq!(extent.floor_at_voxel_z(4), Some(0));
assert_eq!(extent.floor_at_voxel_z(5), Some(1));
assert_eq!(extent.floor_at_voxel_z(7), Some(1));
assert_eq!(extent.floor_at_voxel_z(8), Some(2));
assert_eq!(extent.floor_at_voxel_z(10), Some(2));
}
/// (d) Boundary — one voxel above the top floor returns None; one below the base returns None.
///
/// Uniform(3), base_floor=0, floor_count=2: valid range [0, 5].
/// voxel_z=6 is one above the top; voxel_z=-1 is one below the base.
#[test]
fn floor_extent_boundary_returns_none() {
let extent = FloorExtent {
base_floor: 0,
floor_count: 2,
heights: FloorHeightProfile::Uniform(3),
};
// One voxel above the top floor (top floor ends at voxel 5).
assert_eq!(
extent.floor_at_voxel_z(6),
None,
"one voxel above top floor must return None"
);
// One voxel below the base floor (base maps to voxel 0).
assert_eq!(
extent.floor_at_voxel_z(-1),
None,
"one voxel below base floor must return None"
);
// Floor index outside extent also returns None from voxel_range_for_floor.
assert_eq!(
extent.voxel_range_for_floor(2),
None,
"floor index beyond floor_count must return None"
);
assert_eq!(
extent.voxel_range_for_floor(-1),
None,
"floor index below base_floor must return None"
);
}
/// D-110: FloorZone.z_level must be i8 (signed) to support negative sub-levels.
#[test]
fn floor_zone_z_level_is_signed() {
let fz = FloorZone {
z_level: -2,
zone_type: ZoningType::Restricted,
zone_palette: ZonePalette {
base: String::new(),
modifiers: vec![],
},
access_tier: ZoneAccessTier::BreachOnly,
};
assert_eq!(fz.z_level, -2, "sub-basement z_level must round-trip as i8");
}
/// D-110: MultiBlockReservation.base_z must be i8 (signed) for underground buildings.
#[test]
fn reservation_base_z_is_signed() {
let r = MultiBlockReservation {
blocks: vec![(0, 0)],
template_tag: "deep-mine".into(),
function: ReservationFunction::UndergroundComplex,
z_levels: 30,
base_z: -30,
floor_zones: vec![],
z_band_count: 5,
z_band_zones: vec![],
vertical_corridors: vec![],
hosted_sites: vec![],
};
assert_eq!(
r.base_z, -30,
"deep mine base_z must be representable as i8"
);
assert_eq!(r.z_levels, 30u8, "z_levels count must remain u8");
}
/// D-110: z_levels (count) remains u8 on QuarterSkeleton.
#[test]
fn quarter_skeleton_z_levels_is_unsigned() {
// Verify z_levels is u8 (cannot hold negative value). Static assertion:
// if this compiled with z_levels = 255u8, the type is correct.
fn assert_u8(_: u8) {}
let z_levels: u8 = 4;
assert_u8(z_levels);
assert!(z_levels > 0, "z_levels is always at least 1");
}
}