- Add D-230 skeleton: DistrictSkeleton field to DistrictWorldState; cascade Default to DistrictSkeleton + contained enums/structs. Box the GenCompletion::SkeletonGenerated state to avoid large_enum_variant. - Derive PartialEq on FloorExtent/FloorHeightProfile/DistrictWorldState/ CityGenerationContext (+ minimal cascade) for downstream assert_eq tests. - Add 4 unit tests for floor_at_voxel_z / voxel_range_for_floor (uniform, basement, variable heights, boundary) — the Q-104 deliverable. - Drop unused smallvec direct dep (stays transitive via bevy_ecs). - Key districts insert by skeleton.district_id, sharpen TODO(#957). clippy --all-targets -D warnings clean; 1263 lib tests pass; fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1345 lines
52 KiB
Rust
1345 lines
52 KiB
Rust
//! World generator data model — district skeleton and mobile chunk types.
|
||
//!
|
||
//! Phase 1 generator output: the `DistrictSkeleton` 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 DistrictId = 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;
|
||
/// Single era modification applied to a block (conversion, overlay, hybrid). Stub.
|
||
pub type EraModification = String;
|
||
/// Landmark slot description within a block. Stub.
|
||
pub type LandmarkSlot = String;
|
||
/// Chunk layout specification within a block (how the 2×2 chunks are arranged). Stub.
|
||
pub type ChunkLayout = String;
|
||
/// Corridor spine connecting district access points. Stub.
|
||
pub type CorridorSpine = String;
|
||
/// District context — neighboring districts, world position, system. Stub.
|
||
pub type DistrictContext = String;
|
||
/// Society profile reference (Miri's cultural ingredients). Stub.
|
||
pub type SocietyProfileRef = String;
|
||
/// Zone definition — base palette + modifiers + zone name. Stub.
|
||
pub type ZoneDefinition = String;
|
||
/// District boundary system — edge descriptors with neighbors. Stub.
|
||
pub type DistrictBoundaries = String;
|
||
/// Guarantee audit result — tier-appropriate spatial invariant checks. Stub.
|
||
pub type GuaranteeAuditResult = String;
|
||
/// District access point (entry/exit to neighboring district). Stub.
|
||
pub type AccessPoint = String;
|
||
/// 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).
|
||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, 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, 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 },
|
||
}
|
||
|
||
/// Territory control status for a province (drainage basin). Priority-ordered derivation.
|
||
/// Source: D-212
|
||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||
pub enum TerritorialStatus {
|
||
/// Commission faction_influence ≥ 0.6 in this province.
|
||
CommissionControlled,
|
||
/// 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,
|
||
/// Cultural corridor has indigenous autonomy flag.
|
||
IndigenousHeld,
|
||
/// Population density < 0.01 AND no faction ≥ 0.1.
|
||
Derelict,
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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,
|
||
/// Normalized strength 0.0–1.0. Derived from flow accumulation or habitability score.
|
||
pub strength: f32,
|
||
/// Fine-grained terrain classification at this position (D-210).
|
||
pub sub_biome: SubBiomeVariant,
|
||
/// Infrastructure-build cost multiplier (1.0 = baseline grassland; higher =
|
||
/// more expensive). Derived from `sub_biome` + local slope. Consumed by the
|
||
/// attractor-matching pipeline (D-211) to penalize marginal cities. (D-210)
|
||
pub terrain_modification_cost: f32,
|
||
}
|
||
|
||
/// Compatibility weights between economic roles and attractor types.
|
||
/// A 10×7 matrix (10 economic_role values × 7 AttractorType variants).
|
||
/// Each cell is a weight multiplier 0.0–3.0 applied during attractor-matching scoring.
|
||
/// 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: [[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
|
||
///
|
||
/// 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 (0.0–1.0). Derived per D-197.
|
||
pub prosperity_baseline: f32,
|
||
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).
|
||
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,
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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 sim tiles 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 district's 4×4 block grid.
|
||
/// Each block = 128×128 sim tiles = 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,
|
||
/// Which multi-block reservation this block belongs to (if any).
|
||
pub reservation: Option<ReservationId>,
|
||
pub chunk_layout: ChunkLayout,
|
||
pub hosted_sites: Vec<SocialSiteId>,
|
||
pub era: Era,
|
||
pub era_modifications: Vec<EraModification>,
|
||
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>,
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// District skeleton
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Phase 1 generator output for one district.
|
||
///
|
||
/// 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 district (count, always ≥ 1)
|
||
/// - `base_z` does not appear on `DistrictSkeleton` itself — the district'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 DistrictSkeleton {
|
||
// ── Identity ──────────────────────────────────────────
|
||
pub district_id: DistrictId,
|
||
/// Deterministic seed (derived from master seed via SeedChain).
|
||
pub seed: u64,
|
||
pub district_type: DistrictType,
|
||
/// World context (system, world, neighboring districts).
|
||
pub context: DistrictContext,
|
||
|
||
// ── 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 sim tiles = 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 / World Context ──────────────────────────
|
||
pub society_profile: SocietyProfileRef,
|
||
pub zone_palette: Vec<ZoneDefinition>,
|
||
|
||
// ── Boundary System ───────────────────────────────────
|
||
pub boundaries: DistrictBoundaries,
|
||
|
||
// ── 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 DistrictSkeleton.
|
||
#[test]
|
||
fn district_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");
|
||
}
|
||
}
|