feat(simulation): foundation shared types + struct plumbing (#1006)
Define the type-definition layer the Phase-4 fill-seam tickets depend on (D-229/D-230/D-231/D-232/D-233), compiling with stubs/defaults; behavior logic lands in #982-985/#998. - New types in generator.rs: BuildingPropertyTag, FloorExtent + FloorHeightProfile (floor_at_voxel_z/voxel_range_for_floor, resolves Q-104), BuildingEntryClass, ConstructionEra, ZoneTypeId, MorphologyZone, BulkClass(5), ProductionUbiquity, DoorSpec, InteriorDescriptor, DistrictWorldState. - Rename spatial AccessTier -> ZoneAccessTier to free the name for the new per-building BuildingEntryClass. - CityGenerationContext: +morphology_zone, +trait_selection, +dominant_bulk_class, +dominant_production_ubiquity. - BodyWorldState: +districts (DistrictWorldState w/ block_tags). - GenCompletion::SkeletonGenerated carries body_id + DistrictWorldState; plugin handler inserts into BodyWorldState.districts. - Add smallvec as a direct dep (DoorSpec list stays Vec for now, TODO). cargo check --all-targets / clippy clean; 1259 lib tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+4
@@ -1492,6 +1492,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_norway",
|
"serde_norway",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"smallvec",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
@@ -1551,6 +1552,9 @@ name = "smallvec"
|
|||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smol_str"
|
name = "smol_str"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ bytemuck = "1"
|
|||||||
png = "0.17"
|
png = "0.17"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
aho-corasick = "1"
|
aho-corasick = "1"
|
||||||
|
smallvec = { version = "1", features = ["serde"] }
|
||||||
# Economics simulation — Leontief + tâtonnement + D-180 event port (#821)
|
# Economics simulation — Leontief + tâtonnement + D-180 event port (#821)
|
||||||
econ-sim = { path = "../tooling/econ-sim" }
|
econ-sim = { path = "../tooling/econ-sim" }
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use bevy_ecs::prelude::Resource;
|
use bevy_ecs::prelude::Resource;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::simulation::generator::GeographicAttractor;
|
use crate::simulation::generator::{DistrictId, DistrictWorldState, GeographicAttractor};
|
||||||
|
|
||||||
/// Simulation tick counter — monotonically increasing u64.
|
/// Simulation tick counter — monotonically increasing u64.
|
||||||
pub type SimTick = u64;
|
pub type SimTick = u64;
|
||||||
@@ -71,6 +71,11 @@ pub struct BodyWorldState {
|
|||||||
pub drainage_basins: Vec<DrainageBasin>,
|
pub drainage_basins: Vec<DrainageBasin>,
|
||||||
/// Geographic attractors (D-195, D-209). Empty until attractor task completes.
|
/// Geographic attractors (D-195, D-209). Empty until attractor task completes.
|
||||||
pub attractors: Vec<GeographicAttractor>,
|
pub attractors: Vec<GeographicAttractor>,
|
||||||
|
/// District-level world state, keyed by `DistrictId` (D-230).
|
||||||
|
///
|
||||||
|
/// Populated by `GenCompletion::SkeletonGenerated` after the plan phase
|
||||||
|
/// completes for each city. `BTreeMap` for D-010 determinism.
|
||||||
|
pub districts: BTreeMap<DistrictId, DistrictWorldState>,
|
||||||
/// Last sim tick this entry was read. Used for LRU eviction.
|
/// Last sim tick this entry was read. Used for LRU eviction.
|
||||||
pub last_accessed: SimTick,
|
pub last_accessed: SimTick,
|
||||||
}
|
}
|
||||||
@@ -129,6 +134,14 @@ impl BodyWorldStateCache {
|
|||||||
self.entries.get(body_id)
|
self.entries.get(body_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference without bumping `last_accessed`.
|
||||||
|
///
|
||||||
|
/// Used by the district-state insertion path (D-230) which writes into
|
||||||
|
/// the cached state without constituting a "read" for LRU purposes.
|
||||||
|
pub fn peek_mut(&mut self, body_id: &str) -> Option<&mut BodyWorldState> {
|
||||||
|
self.entries.get_mut(body_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` if the cache has an entry for `body_id`.
|
/// Returns `true` if the cache has an entry for `body_id`.
|
||||||
pub fn contains(&self, body_id: &str) -> bool {
|
pub fn contains(&self, body_id: &str) -> bool {
|
||||||
self.entries.contains_key(body_id)
|
self.entries.contains_key(body_id)
|
||||||
@@ -183,6 +196,7 @@ mod tests {
|
|||||||
river_network: RiverNetwork::default(),
|
river_network: RiverNetwork::default(),
|
||||||
drainage_basins: vec![],
|
drainage_basins: vec![],
|
||||||
attractors: vec![],
|
attractors: vec![],
|
||||||
|
districts: BTreeMap::new(),
|
||||||
last_accessed: tick,
|
last_accessed: tick,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ impl CascadeSnapshot {
|
|||||||
river_network,
|
river_network,
|
||||||
drainage_basins,
|
drainage_basins,
|
||||||
attractors,
|
attractors,
|
||||||
|
districts: std::collections::BTreeMap::new(),
|
||||||
last_accessed: 0,
|
last_accessed: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use crate::atlas::body_world_state::BodyWorldState;
|
|||||||
use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
|
use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
|
||||||
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
|
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
|
||||||
use crate::seed::SeedChain;
|
use crate::seed::SeedChain;
|
||||||
|
use crate::simulation::generator::DistrictWorldState;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Priority
|
// Priority
|
||||||
@@ -99,6 +100,11 @@ pub enum GenCompletion {
|
|||||||
},
|
},
|
||||||
SkeletonGenerated {
|
SkeletonGenerated {
|
||||||
city_id: u64,
|
city_id: u64,
|
||||||
|
/// The body this skeleton belongs to — used to route state into
|
||||||
|
/// `BodyWorldState.districts` (D-230).
|
||||||
|
body_id: String,
|
||||||
|
/// District-level world state (block tags) produced by the plan phase (D-230).
|
||||||
|
state: DistrictWorldState,
|
||||||
},
|
},
|
||||||
ChunkFilled {
|
ChunkFilled {
|
||||||
district_id: u64,
|
district_id: u64,
|
||||||
@@ -336,7 +342,13 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
GenWorkItem::GenerateSkeleton { city_id } => {
|
GenWorkItem::GenerateSkeleton { city_id } => {
|
||||||
GenCompletion::SkeletonGenerated { city_id: *city_id }
|
// Stub: real skeleton generation (#957) will populate `body_id` from
|
||||||
|
// the CityGenerationContext and `state` from the plan phase (D-230).
|
||||||
|
GenCompletion::SkeletonGenerated {
|
||||||
|
city_id: *city_id,
|
||||||
|
body_id: String::new(),
|
||||||
|
state: DistrictWorldState::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
GenWorkItem::FillChunk {
|
GenWorkItem::FillChunk {
|
||||||
district_id,
|
district_id,
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ mod tests {
|
|||||||
river_network: RiverNetwork::default(),
|
river_network: RiverNetwork::default(),
|
||||||
drainage_basins: vec![],
|
drainage_basins: vec![],
|
||||||
attractors: vec![],
|
attractors: vec![],
|
||||||
|
districts: std::collections::BTreeMap::new(),
|
||||||
last_accessed: 0,
|
last_accessed: 0,
|
||||||
});
|
});
|
||||||
let (_db, resolver) = empty_resolver();
|
let (_db, resolver) = empty_resolver();
|
||||||
|
|||||||
@@ -80,8 +80,26 @@ fn drain_generation_completions(
|
|||||||
GenCompletion::Failed { item, reason } => {
|
GenCompletion::Failed { item, reason } => {
|
||||||
tracing::warn!(?item, %reason, "background generation work item failed");
|
tracing::warn!(?item, %reason, "background generation work item failed");
|
||||||
}
|
}
|
||||||
// Produced once the later layers land (#957 / #959); no consumer yet.
|
// Insert district world state into the matching body's cache entry (D-230).
|
||||||
GenCompletion::SkeletonGenerated { .. } | GenCompletion::ChunkFilled { .. } => {}
|
GenCompletion::SkeletonGenerated {
|
||||||
|
city_id,
|
||||||
|
body_id,
|
||||||
|
state,
|
||||||
|
} => {
|
||||||
|
if !body_id.is_empty() {
|
||||||
|
if let Some(body_state) = cache.peek_mut(&body_id) {
|
||||||
|
body_state.districts.insert(city_id, state);
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
city_id,
|
||||||
|
body_id,
|
||||||
|
"SkeletonGenerated: body not in cache — district state dropped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// body_id empty = stub result from GenerateSkeleton stub; silently ignore.
|
||||||
|
}
|
||||||
|
GenCompletion::ChunkFilled { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -411,7 +411,8 @@ fn derive_reservations(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::simulation::generator::{
|
use crate::simulation::generator::{
|
||||||
CityGenerationContext, FoundingOrientation, PoliticalArchetype, SettingType, WorldTier,
|
BulkClass, CityGenerationContext, FoundingOrientation, MorphologyZone, PoliticalArchetype,
|
||||||
|
ProductionUbiquity, SettingType, WorldTier,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn make_context(archetype: PoliticalArchetype, world_tier: WorldTier) -> CityGenerationContext {
|
fn make_context(archetype: PoliticalArchetype, world_tier: WorldTier) -> CityGenerationContext {
|
||||||
@@ -424,6 +425,11 @@ mod tests {
|
|||||||
footprint_radius_km: 10.0,
|
footprint_radius_km: 10.0,
|
||||||
founding_orientation: FoundingOrientation::Cardinal,
|
founding_orientation: FoundingOrientation::Cardinal,
|
||||||
world_tier,
|
world_tier,
|
||||||
|
// D-229/D-232/D-233 additions — sensible stubs for existing tests.
|
||||||
|
morphology_zone: MorphologyZone::AlluvialPlain,
|
||||||
|
trait_selection: Vec::new(),
|
||||||
|
dominant_bulk_class: BulkClass::NonPhysical,
|
||||||
|
dominant_production_ubiquity: ProductionUbiquity::Common,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
//!
|
//!
|
||||||
//! Threshold values are authored constants (D-217): 0.63, 0.43, 0.23.
|
//! Threshold values are authored constants (D-217): 0.63, 0.43, 0.23.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::simulation::generator::EraCause;
|
use crate::simulation::generator::EraCause;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -30,7 +32,7 @@ use crate::simulation::generator::EraCause;
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Visual condition band for a tile, derived from prosperity_score (D-217).
|
/// Visual condition band for a tile, derived from prosperity_score (D-217).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum TileCondition {
|
pub enum TileCondition {
|
||||||
/// prosperity_score > 0.63. Clean, undamaged, well-maintained.
|
/// prosperity_score > 0.63. Clean, undamaged, well-maintained.
|
||||||
Intact,
|
Intact,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
//!
|
//!
|
||||||
//! Sources: workshop-outcomes.md, tyre-round4.md
|
//! Sources: workshop-outcomes.md, tyre-round4.md
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -196,9 +198,12 @@ pub enum ReservationFunction {
|
|||||||
UndergroundComplex,
|
UndergroundComplex,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access tier — who is allowed into a zone under normal circumstances.
|
/// 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)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum AccessTier {
|
pub enum ZoneAccessTier {
|
||||||
/// Anyone can enter.
|
/// Anyone can enter.
|
||||||
Public,
|
Public,
|
||||||
/// Requires employment or residence credential.
|
/// Requires employment or residence credential.
|
||||||
@@ -445,10 +450,457 @@ pub struct CompatibilityMatrix {
|
|||||||
pub weights: [[f32; 7]; 10],
|
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)]
|
||||||
|
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 D-110 floor index to the first voxel-z value at that floor's base.
|
||||||
|
///
|
||||||
|
/// Returns `None` if `floor_index` is outside `[base_floor, base_floor + floor_count)`.
|
||||||
|
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.
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
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.
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MorphologyZone {
|
||||||
|
fn default() -> Self {
|
||||||
|
MorphologyZone::AlluvialPlain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
|
pub struct DistrictWorldState {
|
||||||
|
/// 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
|
/// Data contract between build-time (systems.db) and the runtime-background
|
||||||
/// generation tier. Populated from atlas_city_names + bodies at generation
|
/// generation tier. Populated from atlas_city_names + bodies at generation
|
||||||
/// dispatch time. All 8 fields are required before a generation task may run.
|
/// dispatch time. All 8 fields are required before a generation task may run.
|
||||||
/// Source: D-200, D-199
|
/// 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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct CityGenerationContext {
|
pub struct CityGenerationContext {
|
||||||
/// Foreign key into atlas_city_names.id
|
/// Foreign key into atlas_city_names.id
|
||||||
@@ -463,6 +915,18 @@ pub struct CityGenerationContext {
|
|||||||
pub footprint_radius_km: f32,
|
pub footprint_radius_km: f32,
|
||||||
pub founding_orientation: FoundingOrientation,
|
pub founding_orientation: FoundingOrientation,
|
||||||
pub world_tier: WorldTier,
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -512,7 +976,7 @@ pub struct FloorZone {
|
|||||||
pub z_level: i8,
|
pub z_level: i8,
|
||||||
pub zone_type: ZoningType,
|
pub zone_type: ZoningType,
|
||||||
pub zone_palette: ZonePalette,
|
pub zone_palette: ZonePalette,
|
||||||
pub access_tier: AccessTier,
|
pub access_tier: ZoneAccessTier,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vertical connection spec within a multi-level reservation.
|
/// Vertical connection spec within a multi-level reservation.
|
||||||
@@ -523,7 +987,7 @@ pub struct VerticalCorridorSpec {
|
|||||||
/// Which z-bands (0-indexed band indices, not absolute z-levels) this
|
/// Which z-bands (0-indexed band indices, not absolute z-levels) this
|
||||||
/// corridor connects. See D-110 for signed z-level coordinate system.
|
/// corridor connects. See D-110 for signed z-level coordinate system.
|
||||||
pub z_bands_connected: Vec<u8>,
|
pub z_bands_connected: Vec<u8>,
|
||||||
pub access_tier: AccessTier,
|
pub access_tier: ZoneAccessTier,
|
||||||
pub corridor_type: VerticalCorridorType,
|
pub corridor_type: VerticalCorridorType,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,7 +1015,7 @@ pub struct SocialSitePlacement {
|
|||||||
/// Which blocks this site spans.
|
/// Which blocks this site spans.
|
||||||
pub blocks: Vec<(u8, u8)>,
|
pub blocks: Vec<(u8, u8)>,
|
||||||
pub template_tag: String,
|
pub template_tag: String,
|
||||||
pub access_tier: AccessTier,
|
pub access_tier: ZoneAccessTier,
|
||||||
pub triangles: Vec<TriangleAssignment>,
|
pub triangles: Vec<TriangleAssignment>,
|
||||||
pub role_slots: Vec<RoleSlot>,
|
pub role_slots: Vec<RoleSlot>,
|
||||||
pub active_phases: Vec<DayPhase>,
|
pub active_phases: Vec<DayPhase>,
|
||||||
@@ -713,7 +1177,7 @@ mod tests {
|
|||||||
base: String::new(),
|
base: String::new(),
|
||||||
modifiers: vec![],
|
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");
|
assert_eq!(fz.z_level, -2, "sub-basement z_level must round-trip as i8");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user