@@ -15,6 +15,8 @@
//!
//! Sources: workshop-outcomes.md, tyre-round4.md
use std ::collections ::BTreeMap ;
use serde ::{ Deserialize , Serialize } ;
// ---------------------------------------------------------------------------
@@ -101,7 +103,7 @@ pub type PlacedObject = String;
/// Determines simulation fidelity budget and NPC complexity ceiling.
///
/// Source: D-218, workshop-outcomes.md
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default ) ]
pub enum WorldTier {
/// Hub system. Full simulation, high faction pressure.
Epicenter ,
@@ -112,12 +114,13 @@ pub enum WorldTier {
/// 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) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default ) ]
pub enum ComplexityTier {
/// Full social architecture. All Tier 1+2 guarantees. 20-80+ NPCs.
Full ,
@@ -126,26 +129,34 @@ pub enum ComplexityTier {
/// 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) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default ) ]
pub enum SettingType {
Station ,
#[ default ]
Urban ,
Agricultural ,
Maritime ,
Wilderness { biome : Biome } ,
Water { water_type : WaterType } ,
Wilderness {
biome : Biome ,
} ,
Water {
water_type : WaterType ,
} ,
Transitional ,
Orbital ,
Specialized { function : SpecializedFunction } ,
Specialized {
function : SpecializedFunction ,
} ,
}
/// Classification of a district (high-level function).
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default ) ]
pub enum DistrictType {
LogisticsHub ,
Residential ,
@@ -153,15 +164,17 @@ pub enum DistrictType {
Industrial ,
Administrative ,
Entertainment ,
#[ default ]
MixedUse ,
Transit ,
Specialized ,
}
/// How blocks are placed within the district's 512× 512 footprint.
#[ derive(Serialize, Deserialize, Clone, Debug) ]
#[ 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 {
@@ -170,7 +183,7 @@ pub enum DistrictLayoutMode {
}
/// Zoning classification for a block or floor zone.
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default ) ]
pub enum ZoningType {
Commercial ,
Residential ,
@@ -179,11 +192,12 @@ pub enum ZoningType {
Transit ,
Recreational ,
Restricted ,
#[ default ]
Mixed ,
}
/// Reservation function — what purpose a multi-block reservation serves.
#[ derive(Serialize, Deserialize, Clone, Debug) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq ) ]
pub enum ReservationFunction {
Skyscraper ,
Park ,
@@ -196,9 +210,12 @@ pub enum ReservationFunction {
UndergroundComplex ,
}
/// A ccess tier — who is allowed into a zone under normal circumstances.
/// Zone a ccess 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 AccessTier {
pub enum ZoneAccessTier {
/// Anyone can enter.
Public ,
/// Requires employment or residence credential.
@@ -212,7 +229,7 @@ pub enum AccessTier {
}
/// Vertical corridor type (how floors connect in a multi-level reservation).
#[ derive(Serialize, Deserialize, Clone, Debug) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq ) ]
pub enum VerticalCorridorType {
Stairwell ,
Elevator ,
@@ -281,7 +298,7 @@ pub enum WallBackside {
}
/// Era cause — why a block has the era tag it has.
#[ derive(Serialize, Deserialize, Clone, Debug) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq ) ]
pub enum EraCause {
Original ,
CorporateMerger ,
@@ -333,7 +350,7 @@ pub enum PoliticalArchetype {
/// 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) ]
#[ 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 } ,
@@ -445,11 +462,473 @@ pub struct CompatibilityMatrix {
pub weights : [ [ f32 ; 7 ] ; 10 ] ,
}
// ---------------------------------------------------------------------------
// 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 + 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 index 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 weighted `allow`/`block` filter;
/// the index is frozen-amber once written.
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq) ]
pub struct ArchitectureFlavorRef {
/// Index into `CityGenerationContext::trait_selection` (0-based).
pub flavor_index : u8 ,
}
/// 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` (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-228, D-232, D-234).
///
/// 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).
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default) ]
pub enum MorphologyZone {
/// Steep-sided inlet — ribbon/hub-and-spoke streets only; pier geometry on water edges.
Fjord ,
/// River delta / braided channel — hub-and-spoke following channels; bridges as forced nodes.
Delta ,
/// Meandering river reach — any pattern.
MeanderReach ,
/// Flat alluvial plain — any pattern; primary default for plains settlements.
#[ default ]
AlluvialPlain ,
/// Open ocean surface (deep-water context) — hub-and-spoke; perimeter access priority.
OpenOcean ,
/// Lake shore — hub-and-spoke; perimeter access toward water.
Lake ,
/// Interior sea body.
Sea ,
/// Mountain pass terrain — ribbon only; elevation steps as block boundaries.
MountainPass ,
/// Coastal lowland — any pattern; pier geometry on water-facing edges.
CoastalLowland ,
/// Island context — hub-and-spoke; perimeter access priority.
Island ,
/// Canyon floor — ribbon or hub-and-spoke only.
Canyon ,
/// Unknown / unclassified — fallback to AlluvialPlain behaviour.
Unknown ,
}
/// 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 ,
}
/// District-level world state produced by the plan phase (D-230).
///
/// Output of `GenerateSkeleton` extended to include building-property tags.
/// Stored in `BodyWorldState.districts: BTreeMap<DistrictId, DistrictWorldState>`.
/// `BTreeMap` for D-010 determinism.
///
/// D-230: `{ skeleton: DistrictSkeleton, block_tags: BTreeMap<(u8,u8), Vec<BuildingPropertyTag>> }`
#[ derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq) ]
pub struct DistrictWorldState {
/// Phase 1 skeleton produced by `GenerateSkeleton` (D-230).
pub skeleton : DistrictSkeleton ,
/// 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
#[ derive(Serialize, Deserialize, Clone, Debug) ]
///
/// 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 ,
@@ -463,6 +942,18 @@ pub struct CityGenerationContext {
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).
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 ,
}
// ---------------------------------------------------------------------------
@@ -470,7 +961,7 @@ pub struct CityGenerationContext {
// ---------------------------------------------------------------------------
/// Organic layout placement for a single block.
#[ derive(Serialize, Deserialize, Clone, Debug) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default ) ]
pub struct BlockPlacement {
/// Offset from grid-aligned position (±16 sim tiles per axis max).
pub offset : ( i16 , i16 ) ,
@@ -483,7 +974,7 @@ pub struct BlockPlacement {
/// Single block within a district's 4× 4 block grid.
/// Each block = 128× 128 sim tiles = 2× 2 chunks.
#[ derive(Serialize, Deserialize, Clone, Debug) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default ) ]
pub struct BlockSkeleton {
/// Grid position (0– 3, 0– 3).
pub position : ( u8 , u8 ) ,
@@ -506,36 +997,36 @@ pub struct BlockSkeleton {
/// **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) ]
#[ 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 : AccessTier ,
pub access_tier : ZoneAccessTier ,
}
/// Vertical connection spec within a multi-level reservation.
#[ derive(Serialize, Deserialize, Clone, Debug) ]
#[ 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 : AccessTier ,
pub access_tier : ZoneAccessTier ,
pub corridor_type : VerticalCorridorType ,
}
/// Visual palette for a zone — base material + contextual modifiers.
#[ derive(Serialize, Deserialize, Clone, Debug) ]
#[ 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) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq ) ]
pub enum PaletteModifier {
EconomicFunction ( EconomicModifier ) ,
Era ( Era ) ,
@@ -545,20 +1036,20 @@ pub enum PaletteModifier {
}
/// Social site placement within a district.
#[ derive(Serialize, Deserialize, Clone, Debug) ]
#[ 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 : AccessTier ,
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) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq ) ]
pub struct TriangleAssignment {
pub template : TriangleTemplate ,
pub purposes : Vec < TrianglePurpose > ,
@@ -611,7 +1102,7 @@ pub struct StructuralChange {
/// `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) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq ) ]
pub struct MultiBlockReservation {
/// Which blocks (grid positions) are part of this reservation.
pub blocks : Vec < ( u8 , u8 ) > ,
@@ -653,7 +1144,7 @@ pub struct MultiBlockReservation {
///
/// 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) ]
#[ derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default ) ]
pub struct DistrictSkeleton {
// ── Identity ──────────────────────────────────────────
pub district_id : DistrictId ,
@@ -703,6 +1194,106 @@ pub struct DistrictSkeleton {
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 ( ) {
@@ -713,7 +1304,7 @@ mod tests {
base : String ::new ( ) ,
modifiers : vec ! [ ] ,
} ,
access_tier : AccessTier ::BreachOnly ,
access_tier : ZoneAccessTier ::BreachOnly ,
} ;
assert_eq! ( fz . z_level , - 2 , " sub-basement z_level must round-trip as i8 " ) ;
}